60303e6714673cc2e4ea911023acf7c823968106
[autoinstalls/wordpress.git] / wp-includes / class-wp-customize-manager.php
1 <?php
2 /**
3  * Customize Manager.
4  *
5  * Bootstraps the Customize experience on the server-side.
6  *
7  * Sets up the theme-switching process if a theme other than the active one is
8  * being previewed and customized.
9  *
10  * Serves as a factory for Customize Controls and Settings, and
11  * instantiates default Customize Controls and Settings.
12  *
13  * @package WordPress
14  * @subpackage Customize
15  * @since 3.4.0
16  */
17 final class WP_Customize_Manager {
18         /**
19          * An instance of the theme being previewed.
20          *
21          * @var WP_Theme
22          */
23         protected $theme;
24
25         /**
26          * The directory name of the previously active theme (within the theme_root).
27          *
28          * @var string
29          */
30         protected $original_stylesheet;
31
32         /**
33          * Whether this is a Customizer pageload.
34          *
35          * @var boolean
36          */
37         protected $previewing = false;
38
39         /**
40          * Methods and properties deailing with managing widgets in the Customizer.
41          *
42          * @var WP_Customize_Widgets
43          */
44         public $widgets;
45
46         protected $settings   = array();
47         protected $containers = array();
48         protected $panels     = array();
49         protected $sections   = array();
50         protected $controls   = array();
51
52         protected $nonce_tick;
53
54         protected $customized;
55
56         /**
57          * Controls that may be rendered from JS templates.
58          *
59          * @since 4.1.0
60          * @access protected
61          * @var array
62          */
63         protected $registered_control_types = array();
64
65         /**
66          * $_POST values for Customize Settings.
67          *
68          * @var array
69          */
70         private $_post_values;
71
72         /**
73          * Constructor.
74          *
75          * @since 3.4.0
76          */
77         public function __construct() {
78                 require( ABSPATH . WPINC . '/class-wp-customize-setting.php' );
79                 require( ABSPATH . WPINC . '/class-wp-customize-panel.php' );
80                 require( ABSPATH . WPINC . '/class-wp-customize-section.php' );
81                 require( ABSPATH . WPINC . '/class-wp-customize-control.php' );
82                 require( ABSPATH . WPINC . '/class-wp-customize-widgets.php' );
83
84                 $this->widgets = new WP_Customize_Widgets( $this );
85
86                 add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) );
87
88                 add_action( 'setup_theme',  array( $this, 'setup_theme' ) );
89                 add_action( 'wp_loaded',    array( $this, 'wp_loaded' ) );
90
91                 // Run wp_redirect_status late to make sure we override the status last.
92                 add_action( 'wp_redirect_status', array( $this, 'wp_redirect_status' ), 1000 );
93
94                 // Do not spawn cron (especially the alternate cron) while running the Customizer.
95                 remove_action( 'init', 'wp_cron' );
96
97                 // Do not run update checks when rendering the controls.
98                 remove_action( 'admin_init', '_maybe_update_core' );
99                 remove_action( 'admin_init', '_maybe_update_plugins' );
100                 remove_action( 'admin_init', '_maybe_update_themes' );
101
102                 add_action( 'wp_ajax_customize_save', array( $this, 'save' ) );
103
104                 add_action( 'customize_register',                 array( $this, 'register_controls' ) );
105                 add_action( 'customize_controls_init',            array( $this, 'prepare_controls' ) );
106                 add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_control_scripts' ) );
107         }
108
109         /**
110          * Return true if it's an AJAX request.
111          *
112          * @since 3.4.0
113          *
114          * @return bool
115          */
116         public function doing_ajax() {
117                 return isset( $_POST['customized'] ) || ( defined( 'DOING_AJAX' ) && DOING_AJAX );
118         }
119
120         /**
121          * Custom wp_die wrapper. Returns either the standard message for UI
122          * or the AJAX message.
123          *
124          * @since 3.4.0
125          *
126          * @param mixed $ajax_message AJAX return
127          * @param mixed $message UI message
128          */
129         protected function wp_die( $ajax_message, $message = null ) {
130                 if ( $this->doing_ajax() )
131                         wp_die( $ajax_message );
132
133                 if ( ! $message )
134                         $message = __( 'Cheatin&#8217; uh?' );
135
136                 wp_die( $message );
137         }
138
139         /**
140          * Return the AJAX wp_die() handler if it's a customized request.
141          *
142          * @since 3.4.0
143          *
144          * @return string
145          */
146         public function wp_die_handler() {
147                 if ( $this->doing_ajax() )
148                         return '_ajax_wp_die_handler';
149
150                 return '_default_wp_die_handler';
151         }
152
153         /**
154          * Start preview and customize theme.
155          *
156          * Check if customize query variable exist. Init filters to filter the current theme.
157          *
158          * @since 3.4.0
159          */
160         public function setup_theme() {
161                 send_origin_headers();
162
163                 if ( is_admin() && ! $this->doing_ajax() )
164                     auth_redirect();
165                 elseif ( $this->doing_ajax() && ! is_user_logged_in() )
166                     $this->wp_die( 0 );
167
168                 show_admin_bar( false );
169
170                 if ( ! current_user_can( 'customize' ) ) {
171                         $this->wp_die( -1 );
172                 }
173
174                 $this->original_stylesheet = get_stylesheet();
175
176                 $this->theme = wp_get_theme( isset( $_REQUEST['theme'] ) ? $_REQUEST['theme'] : null );
177
178                 if ( $this->is_theme_active() ) {
179                         // Once the theme is loaded, we'll validate it.
180                         add_action( 'after_setup_theme', array( $this, 'after_setup_theme' ) );
181                 } else {
182                         // If the requested theme is not the active theme and the user doesn't have the
183                         // switch_themes cap, bail.
184                         if ( ! current_user_can( 'switch_themes' ) )
185                                 $this->wp_die( -1 );
186
187                         // If the theme has errors while loading, bail.
188                         if ( $this->theme()->errors() )
189                                 $this->wp_die( -1 );
190
191                         // If the theme isn't allowed per multisite settings, bail.
192                         if ( ! $this->theme()->is_allowed() )
193                                 $this->wp_die( -1 );
194                 }
195
196                 $this->start_previewing_theme();
197         }
198
199         /**
200          * Callback to validate a theme once it is loaded
201          *
202          * @since 3.4.0
203          */
204         public function after_setup_theme() {
205                 if ( ! $this->doing_ajax() && ! validate_current_theme() ) {
206                         wp_redirect( 'themes.php?broken=true' );
207                         exit;
208                 }
209         }
210
211         /**
212          * If the theme to be previewed isn't the active theme, add filter callbacks
213          * to swap it out at runtime.
214          *
215          * @since 3.4.0
216          */
217         public function start_previewing_theme() {
218                 // Bail if we're already previewing.
219                 if ( $this->is_preview() )
220                         return;
221
222                 $this->previewing = true;
223
224                 if ( ! $this->is_theme_active() ) {
225                         add_filter( 'template', array( $this, 'get_template' ) );
226                         add_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
227                         add_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
228
229                         // @link: https://core.trac.wordpress.org/ticket/20027
230                         add_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
231                         add_filter( 'pre_option_template', array( $this, 'get_template' ) );
232
233                         // Handle custom theme roots.
234                         add_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
235                         add_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
236                 }
237
238                 /**
239                  * Fires once the Customizer theme preview has started.
240                  *
241                  * @since 3.4.0
242                  *
243                  * @param WP_Customize_Manager $this WP_Customize_Manager instance.
244                  */
245                 do_action( 'start_previewing_theme', $this );
246         }
247
248         /**
249          * Stop previewing the selected theme.
250          *
251          * Removes filters to change the current theme.
252          *
253          * @since 3.4.0
254          */
255         public function stop_previewing_theme() {
256                 if ( ! $this->is_preview() )
257                         return;
258
259                 $this->previewing = false;
260
261                 if ( ! $this->is_theme_active() ) {
262                         remove_filter( 'template', array( $this, 'get_template' ) );
263                         remove_filter( 'stylesheet', array( $this, 'get_stylesheet' ) );
264                         remove_filter( 'pre_option_current_theme', array( $this, 'current_theme' ) );
265
266                         // @link: https://core.trac.wordpress.org/ticket/20027
267                         remove_filter( 'pre_option_stylesheet', array( $this, 'get_stylesheet' ) );
268                         remove_filter( 'pre_option_template', array( $this, 'get_template' ) );
269
270                         // Handle custom theme roots.
271                         remove_filter( 'pre_option_stylesheet_root', array( $this, 'get_stylesheet_root' ) );
272                         remove_filter( 'pre_option_template_root', array( $this, 'get_template_root' ) );
273                 }
274
275                 /**
276                  * Fires once the Customizer theme preview has stopped.
277                  *
278                  * @since 3.4.0
279                  *
280                  * @param WP_Customize_Manager $this WP_Customize_Manager instance.
281                  */
282                 do_action( 'stop_previewing_theme', $this );
283         }
284
285         /**
286          * Get the theme being customized.
287          *
288          * @since 3.4.0
289          *
290          * @return WP_Theme
291          */
292         public function theme() {
293                 return $this->theme;
294         }
295
296         /**
297          * Get the registered settings.
298          *
299          * @since 3.4.0
300          *
301          * @return array
302          */
303         public function settings() {
304                 return $this->settings;
305         }
306
307         /**
308          * Get the registered controls.
309          *
310          * @since 3.4.0
311          *
312          * @return array
313          */
314         public function controls() {
315                 return $this->controls;
316         }
317
318         /**
319          * Get the registered containers.
320          *
321          * @since 4.0.0
322          *
323          * @return array
324          */
325         public function containers() {
326                 return $this->containers;
327         }
328
329         /**
330          * Get the registered sections.
331          *
332          * @since 3.4.0
333          *
334          * @return array
335          */
336         public function sections() {
337                 return $this->sections;
338         }
339
340         /**
341          * Get the registered panels.
342          *
343          * @since 4.0.0
344          * @access public
345          *
346          * @return array Panels.
347          */
348         public function panels() {
349                 return $this->panels;
350         }
351
352         /**
353          * Checks if the current theme is active.
354          *
355          * @since 3.4.0
356          *
357          * @return bool
358          */
359         public function is_theme_active() {
360                 return $this->get_stylesheet() == $this->original_stylesheet;
361         }
362
363         /**
364          * Register styles/scripts and initialize the preview of each setting
365          *
366          * @since 3.4.0
367          */
368         public function wp_loaded() {
369
370                 /**
371                  * Fires once WordPress has loaded, allowing scripts and styles to be initialized.
372                  *
373                  * @since 3.4.0
374                  *
375                  * @param WP_Customize_Manager $this WP_Customize_Manager instance.
376                  */
377                 do_action( 'customize_register', $this );
378
379                 if ( $this->is_preview() && ! is_admin() )
380                         $this->customize_preview_init();
381         }
382
383         /**
384          * Prevents AJAX requests from following redirects when previewing a theme
385          * by issuing a 200 response instead of a 30x.
386          *
387          * Instead, the JS will sniff out the location header.
388          *
389          * @since 3.4.0
390          *
391          * @param $status
392          * @return int
393          */
394         public function wp_redirect_status( $status ) {
395                 if ( $this->is_preview() && ! is_admin() )
396                         return 200;
397
398                 return $status;
399         }
400
401         /**
402          * Decode the $_POST['customized'] values for a specific Customize Setting.
403          *
404          * @since 3.4.0
405          *
406          * @param WP_Customize_Setting $setting A WP_Customize_Setting derived object
407          * @return string $post_value Sanitized value
408          */
409         public function post_value( $setting ) {
410                 if ( ! isset( $this->_post_values ) ) {
411                         if ( isset( $_POST['customized'] ) )
412                                 $this->_post_values = json_decode( wp_unslash( $_POST['customized'] ), true );
413                         else
414                                 $this->_post_values = false;
415                 }
416
417                 if ( isset( $this->_post_values[ $setting->id ] ) )
418                         return $setting->sanitize( $this->_post_values[ $setting->id ] );
419         }
420
421         /**
422          * Print JavaScript settings.
423          *
424          * @since 3.4.0
425          */
426         public function customize_preview_init() {
427                 $this->nonce_tick = check_ajax_referer( 'preview-customize_' . $this->get_stylesheet(), 'nonce' );
428
429                 $this->prepare_controls();
430
431                 wp_enqueue_script( 'customize-preview' );
432                 add_action( 'wp', array( $this, 'customize_preview_override_404_status' ) );
433                 add_action( 'wp_head', array( $this, 'customize_preview_base' ) );
434                 add_action( 'wp_head', array( $this, 'customize_preview_html5' ) );
435                 add_action( 'wp_footer', array( $this, 'customize_preview_settings' ), 20 );
436                 add_action( 'shutdown', array( $this, 'customize_preview_signature' ), 1000 );
437                 add_filter( 'wp_die_handler', array( $this, 'remove_preview_signature' ) );
438
439                 foreach ( $this->settings as $setting ) {
440                         $setting->preview();
441                 }
442
443                 /**
444                  * Fires once the Customizer preview has initialized and JavaScript
445                  * settings have been printed.
446                  *
447                  * @since 3.4.0
448                  *
449                  * @param WP_Customize_Manager $this WP_Customize_Manager instance.
450                  */
451                 do_action( 'customize_preview_init', $this );
452         }
453
454         /**
455          * Prevent sending a 404 status when returning the response for the customize
456          * preview, since it causes the jQuery AJAX to fail. Send 200 instead.
457          *
458          * @since 4.0.0
459          * @access public
460          */
461         public function customize_preview_override_404_status() {
462                 if ( is_404() ) {
463                         status_header( 200 );
464                 }
465         }
466
467         /**
468          * Print base element for preview frame.
469          *
470          * @since 3.4.0
471          */
472         public function customize_preview_base() {
473                 ?><base href="<?php echo home_url( '/' ); ?>" /><?php
474         }
475
476         /**
477          * Print a workaround to handle HTML5 tags in IE < 9
478          *
479          * @since 3.4.0
480          */
481         public function customize_preview_html5() { ?>
482                 <!--[if lt IE 9]>
483                 <script type="text/javascript">
484                         var e = [ 'abbr', 'article', 'aside', 'audio', 'canvas', 'datalist', 'details',
485                                 'figure', 'footer', 'header', 'hgroup', 'mark', 'menu', 'meter', 'nav',
486                                 'output', 'progress', 'section', 'time', 'video' ];
487                         for ( var i = 0; i < e.length; i++ ) {
488                                 document.createElement( e[i] );
489                         }
490                 </script>
491                 <![endif]--><?php
492         }
493
494         /**
495          * Print JavaScript settings for preview frame.
496          *
497          * @since 3.4.0
498          */
499         public function customize_preview_settings() {
500                 $settings = array(
501                         'values'  => array(),
502                         'channel' => wp_unslash( $_POST['customize_messenger_channel'] ),
503                         'activePanels' => array(),
504                         'activeSections' => array(),
505                         'activeControls' => array(),
506                 );
507
508                 if ( 2 == $this->nonce_tick ) {
509                         $settings['nonce'] = array(
510                                 'save' => wp_create_nonce( 'save-customize_' . $this->get_stylesheet() ),
511                                 'preview' => wp_create_nonce( 'preview-customize_' . $this->get_stylesheet() )
512                         );
513                 }
514
515                 foreach ( $this->settings as $id => $setting ) {
516                         $settings['values'][ $id ] = $setting->js_value();
517                 }
518                 foreach ( $this->panels as $id => $panel ) {
519                         $settings['activePanels'][ $id ] = $panel->active();
520                         foreach ( $panel->sections as $id => $section ) {
521                                 $settings['activeSections'][ $id ] = $section->active();
522                         }
523                 }
524                 foreach ( $this->sections as $id => $section ) {
525                         $settings['activeSections'][ $id ] = $section->active();
526                 }
527                 foreach ( $this->controls as $id => $control ) {
528                         $settings['activeControls'][ $id ] = $control->active();
529                 }
530
531                 ?>
532                 <script type="text/javascript">
533                         var _wpCustomizeSettings = <?php echo wp_json_encode( $settings ); ?>;
534                 </script>
535                 <?php
536         }
537
538         /**
539          * Prints a signature so we can ensure the Customizer was properly executed.
540          *
541          * @since 3.4.0
542          */
543         public function customize_preview_signature() {
544                 echo 'WP_CUSTOMIZER_SIGNATURE';
545         }
546
547         /**
548          * Removes the signature in case we experience a case where the Customizer was not properly executed.
549          *
550          * @since 3.4.0
551          */
552         public function remove_preview_signature( $return = null ) {
553                 remove_action( 'shutdown', array( $this, 'customize_preview_signature' ), 1000 );
554
555                 return $return;
556         }
557
558         /**
559          * Is it a theme preview?
560          *
561          * @since 3.4.0
562          *
563          * @return bool True if it's a preview, false if not.
564          */
565         public function is_preview() {
566                 return (bool) $this->previewing;
567         }
568
569         /**
570          * Retrieve the template name of the previewed theme.
571          *
572          * @since 3.4.0
573          *
574          * @return string Template name.
575          */
576         public function get_template() {
577                 return $this->theme()->get_template();
578         }
579
580         /**
581          * Retrieve the stylesheet name of the previewed theme.
582          *
583          * @since 3.4.0
584          *
585          * @return string Stylesheet name.
586          */
587         public function get_stylesheet() {
588                 return $this->theme()->get_stylesheet();
589         }
590
591         /**
592          * Retrieve the template root of the previewed theme.
593          *
594          * @since 3.4.0
595          *
596          * @return string Theme root.
597          */
598         public function get_template_root() {
599                 return get_raw_theme_root( $this->get_template(), true );
600         }
601
602         /**
603          * Retrieve the stylesheet root of the previewed theme.
604          *
605          * @since 3.4.0
606          *
607          * @return string Theme root.
608          */
609         public function get_stylesheet_root() {
610                 return get_raw_theme_root( $this->get_stylesheet(), true );
611         }
612
613         /**
614          * Filter the current theme and return the name of the previewed theme.
615          *
616          * @since 3.4.0
617          *
618          * @param $current_theme {@internal Parameter is not used}
619          * @return string Theme name.
620          */
621         public function current_theme( $current_theme ) {
622                 return $this->theme()->display('Name');
623         }
624
625         /**
626          * Switch the theme and trigger the save() method on each setting.
627          *
628          * @since 3.4.0
629          */
630         public function save() {
631                 if ( ! $this->is_preview() )
632                         die;
633
634                 check_ajax_referer( 'save-customize_' . $this->get_stylesheet(), 'nonce' );
635
636                 // Do we have to switch themes?
637                 if ( ! $this->is_theme_active() ) {
638                         // Temporarily stop previewing the theme to allow switch_themes()
639                         // to operate properly.
640                         $this->stop_previewing_theme();
641                         switch_theme( $this->get_stylesheet() );
642                         update_option( 'theme_switched_via_customizer', true );
643                         $this->start_previewing_theme();
644                 }
645
646                 /**
647                  * Fires once the theme has switched in the Customizer, but before settings
648                  * have been saved.
649                  *
650                  * @since 3.4.0
651                  *
652                  * @param WP_Customize_Manager $this WP_Customize_Manager instance.
653                  */
654                 do_action( 'customize_save', $this );
655
656                 foreach ( $this->settings as $setting ) {
657                         $setting->save();
658                 }
659
660                 /**
661                  * Fires after Customize settings have been saved.
662                  *
663                  * @since 3.6.0
664                  *
665                  * @param WP_Customize_Manager $this WP_Customize_Manager instance.
666                  */
667                 do_action( 'customize_save_after', $this );
668
669                 die;
670         }
671
672         /**
673          * Add a customize setting.
674          *
675          * @since 3.4.0
676          *
677          * @param WP_Customize_Setting|string $id Customize Setting object, or ID.
678          * @param array $args                     Setting arguments; passed to WP_Customize_Setting
679          *                                        constructor.
680          */
681         public function add_setting( $id, $args = array() ) {
682                 if ( is_a( $id, 'WP_Customize_Setting' ) )
683                         $setting = $id;
684                 else
685                         $setting = new WP_Customize_Setting( $this, $id, $args );
686
687                 $this->settings[ $setting->id ] = $setting;
688         }
689
690         /**
691          * Retrieve a customize setting.
692          *
693          * @since 3.4.0
694          *
695          * @param string $id Customize Setting ID.
696          * @return WP_Customize_Setting
697          */
698         public function get_setting( $id ) {
699                 if ( isset( $this->settings[ $id ] ) )
700                         return $this->settings[ $id ];
701         }
702
703         /**
704          * Remove a customize setting.
705          *
706          * @since 3.4.0
707          *
708          * @param string $id Customize Setting ID.
709          */
710         public function remove_setting( $id ) {
711                 unset( $this->settings[ $id ] );
712         }
713
714         /**
715          * Add a customize panel.
716          *
717          * @since 4.0.0
718          * @access public
719          *
720          * @param WP_Customize_Panel|string $id   Customize Panel object, or Panel ID.
721          * @param array                     $args Optional. Panel arguments. Default empty array.
722          */
723         public function add_panel( $id, $args = array() ) {
724                 if ( is_a( $id, 'WP_Customize_Panel' ) ) {
725                         $panel = $id;
726                 }
727                 else {
728                         $panel = new WP_Customize_Panel( $this, $id, $args );
729                 }
730
731                 $this->panels[ $panel->id ] = $panel;
732         }
733
734         /**
735          * Retrieve a customize panel.
736          *
737          * @since 4.0.0
738          * @access public
739          *
740          * @param string $id Panel ID to get.
741          * @return WP_Customize_Panel Requested panel instance.
742          */
743         public function get_panel( $id ) {
744                 if ( isset( $this->panels[ $id ] ) ) {
745                         return $this->panels[ $id ];
746                 }
747         }
748
749         /**
750          * Remove a customize panel.
751          *
752          * @since 4.0.0
753          * @access public
754          *
755          * @param string $id Panel ID to remove.
756          */
757         public function remove_panel( $id ) {
758                 unset( $this->panels[ $id ] );
759         }
760
761         /**
762          * Add a customize section.
763          *
764          * @since 3.4.0
765          *
766          * @param WP_Customize_Section|string $id   Customize Section object, or Section ID.
767          * @param array                       $args Section arguments.
768          */
769         public function add_section( $id, $args = array() ) {
770                 if ( is_a( $id, 'WP_Customize_Section' ) )
771                         $section = $id;
772                 else
773                         $section = new WP_Customize_Section( $this, $id, $args );
774
775                 $this->sections[ $section->id ] = $section;
776         }
777
778         /**
779          * Retrieve a customize section.
780          *
781          * @since 3.4.0
782          *
783          * @param string $id Section ID.
784          * @return WP_Customize_Section
785          */
786         public function get_section( $id ) {
787                 if ( isset( $this->sections[ $id ] ) )
788                         return $this->sections[ $id ];
789         }
790
791         /**
792          * Remove a customize section.
793          *
794          * @since 3.4.0
795          *
796          * @param string $id Section ID.
797          */
798         public function remove_section( $id ) {
799                 unset( $this->sections[ $id ] );
800         }
801
802         /**
803          * Add a customize control.
804          *
805          * @since 3.4.0
806          *
807          * @param WP_Customize_Control|string $id   Customize Control object, or ID.
808          * @param array                       $args Control arguments; passed to WP_Customize_Control
809          *                                          constructor.
810          */
811         public function add_control( $id, $args = array() ) {
812                 if ( is_a( $id, 'WP_Customize_Control' ) )
813                         $control = $id;
814                 else
815                         $control = new WP_Customize_Control( $this, $id, $args );
816
817                 $this->controls[ $control->id ] = $control;
818         }
819
820         /**
821          * Retrieve a customize control.
822          *
823          * @since 3.4.0
824          *
825          * @param string $id ID of the control.
826          * @return WP_Customize_Control $control The control object.
827          */
828         public function get_control( $id ) {
829                 if ( isset( $this->controls[ $id ] ) )
830                         return $this->controls[ $id ];
831         }
832
833         /**
834          * Remove a customize control.
835          *
836          * @since 3.4.0
837          *
838          * @param string $id ID of the control.
839          */
840         public function remove_control( $id ) {
841                 unset( $this->controls[ $id ] );
842         }
843
844         /**
845          * Register a customize control type.
846          *
847          * Registered types are eligible to be rendered via JS and created dynamically.
848          *
849          * @since 4.1.0
850          * @access public
851          *
852          * @param string $control Name of a custom control which is a subclass of
853          *                        {@see WP_Customize_Control}.
854          */
855         public function register_control_type( $control ) {
856                 $this->registered_control_types[] = $control;
857         }
858
859         /**
860          * Render JS templates for all registered control types.
861          *
862          * @since 4.1.0
863          * @access public
864          */
865         public function render_control_templates() {
866                 foreach ( $this->registered_control_types as $control_type ) {
867                         $control = new $control_type( $this, 'temp', array() );
868                         $control->print_template();
869                 }
870         }
871
872         /**
873          * Helper function to compare two objects by priority, ensuring sort stability via instance_number.
874          *
875          * @since 3.4.0
876          *
877          * @param {WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control} $a Object A.
878          * @param {WP_Customize_Panel|WP_Customize_Section|WP_Customize_Control} $b Object B.
879          * @return int
880          */
881         protected final function _cmp_priority( $a, $b ) {
882                 if ( $a->priority === $b->priority ) {
883                         return $a->instance_number - $a->instance_number;
884                 } else {
885                         return $a->priority - $b->priority;
886                 }
887         }
888
889         /**
890          * Prepare panels, sections, and controls.
891          *
892          * For each, check if required related components exist,
893          * whether the user has the necessary capabilities,
894          * and sort by priority.
895          *
896          * @since 3.4.0
897          */
898         public function prepare_controls() {
899
900                 $controls = array();
901                 uasort( $this->controls, array( $this, '_cmp_priority' ) );
902
903                 foreach ( $this->controls as $id => $control ) {
904                         if ( ! isset( $this->sections[ $control->section ] ) || ! $control->check_capabilities() ) {
905                                 continue;
906                         }
907
908                         $this->sections[ $control->section ]->controls[] = $control;
909                         $controls[ $id ] = $control;
910                 }
911                 $this->controls = $controls;
912
913                 // Prepare sections.
914                 uasort( $this->sections, array( $this, '_cmp_priority' ) );
915                 $sections = array();
916
917                 foreach ( $this->sections as $section ) {
918                         if ( ! $section->check_capabilities() || ! $section->controls ) {
919                                 continue;
920                         }
921
922                         usort( $section->controls, array( $this, '_cmp_priority' ) );
923
924                         if ( ! $section->panel ) {
925                                 // Top-level section.
926                                 $sections[ $section->id ] = $section;
927                         } else {
928                                 // This section belongs to a panel.
929                                 if ( isset( $this->panels [ $section->panel ] ) ) {
930                                         $this->panels[ $section->panel ]->sections[ $section->id ] = $section;
931                                 }
932                         }
933                 }
934                 $this->sections = $sections;
935
936                 // Prepare panels.
937                 uasort( $this->panels, array( $this, '_cmp_priority' ) );
938                 $panels = array();
939
940                 foreach ( $this->panels as $panel ) {
941                         if ( ! $panel->check_capabilities() || ! $panel->sections ) {
942                                 continue;
943                         }
944
945                         uasort( $panel->sections, array( $this, '_cmp_priority' ) );
946                         $panels[ $panel->id ] = $panel;
947                 }
948                 $this->panels = $panels;
949
950                 // Sort panels and top-level sections together.
951                 $this->containers = array_merge( $this->panels, $this->sections );
952                 uasort( $this->containers, array( $this, '_cmp_priority' ) );
953         }
954
955         /**
956          * Enqueue scripts for customize controls.
957          *
958          * @since 3.4.0
959          */
960         public function enqueue_control_scripts() {
961                 foreach ( $this->controls as $control ) {
962                         $control->enqueue();
963                 }
964         }
965
966         /**
967          * Register some default controls.
968          *
969          * @since 3.4.0
970          */
971         public function register_controls() {
972
973                 /* Control Types (custom control classes) */
974                 $this->register_control_type( 'WP_Customize_Color_Control' );
975                 $this->register_control_type( 'WP_Customize_Upload_Control' );
976                 $this->register_control_type( 'WP_Customize_Image_Control' );
977                 $this->register_control_type( 'WP_Customize_Background_Image_Control' );
978
979                 /* Site Title & Tagline */
980
981                 $this->add_section( 'title_tagline', array(
982                         'title'    => __( 'Site Title & Tagline' ),
983                         'priority' => 20,
984                 ) );
985
986                 $this->add_setting( 'blogname', array(
987                         'default'    => get_option( 'blogname' ),
988                         'type'       => 'option',
989                         'capability' => 'manage_options',
990                 ) );
991
992                 $this->add_control( 'blogname', array(
993                         'label'      => __( 'Site Title' ),
994                         'section'    => 'title_tagline',
995                 ) );
996
997                 $this->add_setting( 'blogdescription', array(
998                         'default'    => get_option( 'blogdescription' ),
999                         'type'       => 'option',
1000                         'capability' => 'manage_options',
1001                 ) );
1002
1003                 $this->add_control( 'blogdescription', array(
1004                         'label'      => __( 'Tagline' ),
1005                         'section'    => 'title_tagline',
1006                 ) );
1007
1008                 /* Colors */
1009
1010                 $this->add_section( 'colors', array(
1011                         'title'          => __( 'Colors' ),
1012                         'priority'       => 40,
1013                 ) );
1014
1015                 $this->add_setting( 'header_textcolor', array(
1016                         'theme_supports' => array( 'custom-header', 'header-text' ),
1017                         'default'        => get_theme_support( 'custom-header', 'default-text-color' ),
1018
1019                         'sanitize_callback'    => array( $this, '_sanitize_header_textcolor' ),
1020                         'sanitize_js_callback' => 'maybe_hash_hex_color',
1021                 ) );
1022
1023                 // Input type: checkbox
1024                 // With custom value
1025                 $this->add_control( 'display_header_text', array(
1026                         'settings' => 'header_textcolor',
1027                         'label'    => __( 'Display Header Text' ),
1028                         'section'  => 'title_tagline',
1029                         'type'     => 'checkbox',
1030                 ) );
1031
1032                 $this->add_control( new WP_Customize_Color_Control( $this, 'header_textcolor', array(
1033                         'label'   => __( 'Header Text Color' ),
1034                         'section' => 'colors',
1035                 ) ) );
1036
1037                 // Input type: Color
1038                 // With sanitize_callback
1039                 $this->add_setting( 'background_color', array(
1040                         'default'        => get_theme_support( 'custom-background', 'default-color' ),
1041                         'theme_supports' => 'custom-background',
1042
1043                         'sanitize_callback'    => 'sanitize_hex_color_no_hash',
1044                         'sanitize_js_callback' => 'maybe_hash_hex_color',
1045                 ) );
1046
1047                 $this->add_control( new WP_Customize_Color_Control( $this, 'background_color', array(
1048                         'label'   => __( 'Background Color' ),
1049                         'section' => 'colors',
1050                 ) ) );
1051
1052
1053                 /* Custom Header */
1054
1055                 $this->add_section( 'header_image', array(
1056                         'title'          => __( 'Header Image' ),
1057                         'theme_supports' => 'custom-header',
1058                         'priority'       => 60,
1059                 ) );
1060
1061                 $this->add_setting( new WP_Customize_Filter_Setting( $this, 'header_image', array(
1062                         'default'        => get_theme_support( 'custom-header', 'default-image' ),
1063                         'theme_supports' => 'custom-header',
1064                 ) ) );
1065
1066                 $this->add_setting( new WP_Customize_Header_Image_Setting( $this, 'header_image_data', array(
1067                         // 'default'        => get_theme_support( 'custom-header', 'default-image' ),
1068                         'theme_supports' => 'custom-header',
1069                 ) ) );
1070
1071                 $this->add_control( new WP_Customize_Header_Image_Control( $this ) );
1072
1073                 /* Custom Background */
1074
1075                 $this->add_section( 'background_image', array(
1076                         'title'          => __( 'Background Image' ),
1077                         'theme_supports' => 'custom-background',
1078                         'priority'       => 80,
1079                 ) );
1080
1081                 $this->add_setting( 'background_image', array(
1082                         'default'        => get_theme_support( 'custom-background', 'default-image' ),
1083                         'theme_supports' => 'custom-background',
1084                 ) );
1085
1086                 $this->add_setting( new WP_Customize_Background_Image_Setting( $this, 'background_image_thumb', array(
1087                         'theme_supports' => 'custom-background',
1088                 ) ) );
1089
1090                 $this->add_control( new WP_Customize_Background_Image_Control( $this ) );
1091
1092                 $this->add_setting( 'background_repeat', array(
1093                         'default'        => get_theme_support( 'custom-background', 'default-repeat' ),
1094                         'theme_supports' => 'custom-background',
1095                 ) );
1096
1097                 $this->add_control( 'background_repeat', array(
1098                         'label'      => __( 'Background Repeat' ),
1099                         'section'    => 'background_image',
1100                         'type'       => 'radio',
1101                         'choices'    => array(
1102                                 'no-repeat'  => __('No Repeat'),
1103                                 'repeat'     => __('Tile'),
1104                                 'repeat-x'   => __('Tile Horizontally'),
1105                                 'repeat-y'   => __('Tile Vertically'),
1106                         ),
1107                 ) );
1108
1109                 $this->add_setting( 'background_position_x', array(
1110                         'default'        => get_theme_support( 'custom-background', 'default-position-x' ),
1111                         'theme_supports' => 'custom-background',
1112                 ) );
1113
1114                 $this->add_control( 'background_position_x', array(
1115                         'label'      => __( 'Background Position' ),
1116                         'section'    => 'background_image',
1117                         'type'       => 'radio',
1118                         'choices'    => array(
1119                                 'left'       => __('Left'),
1120                                 'center'     => __('Center'),
1121                                 'right'      => __('Right'),
1122                         ),
1123                 ) );
1124
1125                 $this->add_setting( 'background_attachment', array(
1126                         'default'        => get_theme_support( 'custom-background', 'default-attachment' ),
1127                         'theme_supports' => 'custom-background',
1128                 ) );
1129
1130                 $this->add_control( 'background_attachment', array(
1131                         'label'      => __( 'Background Attachment' ),
1132                         'section'    => 'background_image',
1133                         'type'       => 'radio',
1134                         'choices'    => array(
1135                                 'scroll'     => __('Scroll'),
1136                                 'fixed'      => __('Fixed'),
1137                         ),
1138                 ) );
1139
1140                 // If the theme is using the default background callback, we can update
1141                 // the background CSS using postMessage.
1142                 if ( get_theme_support( 'custom-background', 'wp-head-callback' ) === '_custom_background_cb' ) {
1143                         foreach ( array( 'color', 'image', 'position_x', 'repeat', 'attachment' ) as $prop ) {
1144                                 $this->get_setting( 'background_' . $prop )->transport = 'postMessage';
1145                         }
1146                 }
1147
1148                 /* Nav Menus */
1149
1150                 $locations      = get_registered_nav_menus();
1151                 $menus          = wp_get_nav_menus();
1152                 $num_locations  = count( array_keys( $locations ) );
1153
1154                 $this->add_section( 'nav', array(
1155                         'title'          => __( 'Navigation' ),
1156                         'theme_supports' => 'menus',
1157                         'priority'       => 100,
1158                         'description'    => sprintf( _n('Your theme supports %s menu. Select which menu you would like to use.', 'Your theme supports %s menus. Select which menu appears in each location.', $num_locations ), number_format_i18n( $num_locations ) ) . "\n\n" . __('You can edit your menu content on the Menus screen in the Appearance section.'),
1159                 ) );
1160
1161                 if ( $menus ) {
1162                         $choices = array( 0 => __( '&mdash; Select &mdash;' ) );
1163                         foreach ( $menus as $menu ) {
1164                                 $choices[ $menu->term_id ] = wp_html_excerpt( $menu->name, 40, '&hellip;' );
1165                         }
1166
1167                         foreach ( $locations as $location => $description ) {
1168                                 $menu_setting_id = "nav_menu_locations[{$location}]";
1169
1170                                 $this->add_setting( $menu_setting_id, array(
1171                                         'sanitize_callback' => 'absint',
1172                                         'theme_supports'    => 'menus',
1173                                 ) );
1174
1175                                 $this->add_control( $menu_setting_id, array(
1176                                         'label'   => $description,
1177                                         'section' => 'nav',
1178                                         'type'    => 'select',
1179                                         'choices' => $choices,
1180                                 ) );
1181                         }
1182                 }
1183
1184                 /* Static Front Page */
1185                 // #WP19627
1186
1187                 $this->add_section( 'static_front_page', array(
1188                         'title'          => __( 'Static Front Page' ),
1189                 //      'theme_supports' => 'static-front-page',
1190                         'priority'       => 120,
1191                         'description'    => __( 'Your theme supports a static front page.' ),
1192                 ) );
1193
1194                 $this->add_setting( 'show_on_front', array(
1195                         'default'        => get_option( 'show_on_front' ),
1196                         'capability'     => 'manage_options',
1197                         'type'           => 'option',
1198                 //      'theme_supports' => 'static-front-page',
1199                 ) );
1200
1201                 $this->add_control( 'show_on_front', array(
1202                         'label'   => __( 'Front page displays' ),
1203                         'section' => 'static_front_page',
1204                         'type'    => 'radio',
1205                         'choices' => array(
1206                                 'posts' => __( 'Your latest posts' ),
1207                                 'page'  => __( 'A static page' ),
1208                         ),
1209                 ) );
1210
1211                 $this->add_setting( 'page_on_front', array(
1212                         'type'       => 'option',
1213                         'capability' => 'manage_options',
1214                 //      'theme_supports' => 'static-front-page',
1215                 ) );
1216
1217                 $this->add_control( 'page_on_front', array(
1218                         'label'      => __( 'Front page' ),
1219                         'section'    => 'static_front_page',
1220                         'type'       => 'dropdown-pages',
1221                 ) );
1222
1223                 $this->add_setting( 'page_for_posts', array(
1224                         'type'           => 'option',
1225                         'capability'     => 'manage_options',
1226                 //      'theme_supports' => 'static-front-page',
1227                 ) );
1228
1229                 $this->add_control( 'page_for_posts', array(
1230                         'label'      => __( 'Posts page' ),
1231                         'section'    => 'static_front_page',
1232                         'type'       => 'dropdown-pages',
1233                 ) );
1234         }
1235
1236         /**
1237          * Callback for validating the header_textcolor value.
1238          *
1239          * Accepts 'blank', and otherwise uses sanitize_hex_color_no_hash().
1240          * Returns default text color if hex color is empty.
1241          *
1242          * @since 3.4.0
1243          *
1244          * @param string $color
1245          * @return string
1246          */
1247         public function _sanitize_header_textcolor( $color ) {
1248                 if ( 'blank' === $color )
1249                         return 'blank';
1250
1251                 $color = sanitize_hex_color_no_hash( $color );
1252                 if ( empty( $color ) )
1253                         $color = get_theme_support( 'custom-header', 'default-text-color' );
1254
1255                 return $color;
1256         }
1257 }
1258
1259 /**
1260  * Sanitizes a hex color.
1261  *
1262  * Returns either '', a 3 or 6 digit hex color (with #), or null.
1263  * For sanitizing values without a #, see sanitize_hex_color_no_hash().
1264  *
1265  * @since 3.4.0
1266  *
1267  * @param string $color
1268  * @return string|null
1269  */
1270 function sanitize_hex_color( $color ) {
1271         if ( '' === $color )
1272                 return '';
1273
1274         // 3 or 6 hex digits, or the empty string.
1275         if ( preg_match('|^#([A-Fa-f0-9]{3}){1,2}$|', $color ) )
1276                 return $color;
1277
1278         return null;
1279 }
1280
1281 /**
1282  * Sanitizes a hex color without a hash. Use sanitize_hex_color() when possible.
1283  *
1284  * Saving hex colors without a hash puts the burden of adding the hash on the
1285  * UI, which makes it difficult to use or upgrade to other color types such as
1286  * rgba, hsl, rgb, and html color names.
1287  *
1288  * Returns either '', a 3 or 6 digit hex color (without a #), or null.
1289  *
1290  * @since 3.4.0
1291  *
1292  * @param string $color
1293  * @return string|null
1294  */
1295 function sanitize_hex_color_no_hash( $color ) {
1296         $color = ltrim( $color, '#' );
1297
1298         if ( '' === $color )
1299                 return '';
1300
1301         return sanitize_hex_color( '#' . $color ) ? $color : null;
1302 }
1303
1304 /**
1305  * Ensures that any hex color is properly hashed.
1306  * Otherwise, returns value untouched.
1307  *
1308  * This method should only be necessary if using sanitize_hex_color_no_hash().
1309  *
1310  * @since 3.4.0
1311  *
1312  * @param string $color
1313  * @return string
1314  */
1315 function maybe_hash_hex_color( $color ) {
1316         if ( $unhashed = sanitize_hex_color_no_hash( $color ) )
1317                 return '#' . $unhashed;
1318
1319         return $color;
1320 }