1<?php
2/**
3 * Theme, template, and stylesheet functions.
4 *
5 * @package WordPress
6 * @subpackage Theme
7 */
8
9/**
10 * Returns an array of WP_Theme objects based on the arguments.
11 *
12 * Despite advances over get_themes(), this function is quite expensive, and grows
13 * linearly with additional themes. Stick to wp_get_theme() if possible.
14 *
15 * @since 3.4.0
16 *
17 * @global string[] $wp_theme_directories
18 *
19 * @param array $args {
20 * Optional. The search arguments.
21 *
22 * @type mixed $errors True to return themes with errors, false to return
23 * themes without errors, null to return all themes.
24 * Default false.
25 * @type mixed $allowed (Multisite) True to return only allowed themes for a site.
26 * False to return only disallowed themes for a site.
27 * 'site' to return only site-allowed themes.
28 * 'network' to return only network-allowed themes.
29 * Null to return all themes. Default null.
30 * @type int $blog_id (Multisite) The blog ID used to calculate which themes
31 * are allowed. Default 0, synonymous for the current blog.
32 * }
33 * @return WP_Theme[] Array of WP_Theme objects.
34 */
35function wp_get_themes( $args = array() ) {
36 global $wp_theme_directories;
37
38 $defaults = array(
39 'errors' => false,
40 'allowed' => null,
41 'blog_id' => 0,
42 );
43 $args = wp_parse_args( $args, $defaults );
44
45 $theme_directories = search_theme_directories();
46
47 if ( is_array( $wp_theme_directories ) && count( $wp_theme_directories ) > 1 ) {
48 /*
49 * Make sure the active theme wins out, in case search_theme_directories() picks the wrong
50 * one in the case of a conflict. (Normally, last registered theme root wins.)
51 */
52 $current_theme = get_stylesheet();
53 if ( isset( $theme_directories[ $current_theme ] ) ) {
54 $root_of_current_theme = get_raw_theme_root( $current_theme );
55 if ( ! in_array( $root_of_current_theme, $wp_theme_directories, true ) ) {
56 $root_of_current_theme = WP_CONTENT_DIR . $root_of_current_theme;
57 }
58 $theme_directories[ $current_theme ]['theme_root'] = $root_of_current_theme;
59 }
60 }
61
62 if ( empty( $theme_directories ) ) {
63 return array();
64 }
65
66 if ( is_multisite() && null !== $args['allowed'] ) {
67 $allowed = $args['allowed'];
68 if ( 'network' === $allowed ) {
69 $theme_directories = array_intersect_key( $theme_directories, WP_Theme::get_allowed_on_network() );
70 } elseif ( 'site' === $allowed ) {
71 $theme_directories = array_intersect_key( $theme_directories, WP_Theme::get_allowed_on_site( $args['blog_id'] ) );
72 } elseif ( $allowed ) {
73 $theme_directories = array_intersect_key( $theme_directories, WP_Theme::get_allowed( $args['blog_id'] ) );
74 } else {
75 $theme_directories = array_diff_key( $theme_directories, WP_Theme::get_allowed( $args['blog_id'] ) );
76 }
77 }
78
79 $themes = array();
80 static $_themes = array();
81
82 foreach ( $theme_directories as $theme => $theme_root ) {
83 if ( isset( $_themes[ $theme_root['theme_root'] . '/' . $theme ] ) ) {
84 $themes[ $theme ] = $_themes[ $theme_root['theme_root'] . '/' . $theme ];
85 } else {
86 $themes[ $theme ] = new WP_Theme( $theme, $theme_root['theme_root'] );
87
88 $_themes[ $theme_root['theme_root'] . '/' . $theme ] = $themes[ $theme ];
89 }
90 }
91
92 if ( null !== $args['errors'] ) {
93 foreach ( $themes as $theme => $wp_theme ) {
94 if ( (bool) $wp_theme->errors() !== $args['errors'] ) {
95 unset( $themes[ $theme ] );
96 }
97 }
98 }
99
100 return $themes;
101}
102
103/**
104 * Gets a WP_Theme object for a theme.
105 *
106 * @since 3.4.0
107 *
108 * @global string[] $wp_theme_directories
109 *
110 * @param string $stylesheet Optional. Directory name for the theme. Defaults to active theme.
111 * @param string $theme_root Optional. Absolute path of the theme root to look in.
112 * If not specified, get_raw_theme_root() is used to calculate
113 * the theme root for the $stylesheet provided (or active theme).
114 * @return WP_Theme Theme object. Be sure to check the object's exists() method
115 * if you need to confirm the theme's existence.
116 */
117function wp_get_theme( $stylesheet = '', $theme_root = '' ) {
118 global $wp_theme_directories;
119
120 if ( empty( $stylesheet ) ) {
121 $stylesheet = get_stylesheet();
122 }
123
124 if ( empty( $theme_root ) ) {
125 $theme_root = get_raw_theme_root( $stylesheet );
126 if ( false === $theme_root ) {
127 $theme_root = WP_CONTENT_DIR . '/themes';
128 } elseif ( ! in_array( $theme_root, (array) $wp_theme_directories, true ) ) {
129 $theme_root = WP_CONTENT_DIR . $theme_root;
130 }
131 }
132
133 return new WP_Theme( $stylesheet, $theme_root );
134}
135
136/**
137 * Clears the cache held by get_theme_roots() and WP_Theme.
138 *
139 * @since 3.5.0
140 * @param bool $clear_update_cache Whether to clear the theme updates cache.
141 */
142function wp_clean_themes_cache( $clear_update_cache = true ) {
143 if ( $clear_update_cache ) {
144 delete_site_transient( 'update_themes' );
145 }
146 search_theme_directories( true );
147 foreach ( wp_get_themes( array( 'errors' => null ) ) as $theme ) {
148 $theme->cache_delete();
149 }
150}
151
152/**
153 * Whether a child theme is in use.
154 *
155 * @since 3.0.0
156 * @since 6.5.0 Makes use of global template variables.
157 *
158 * @global string $wp_stylesheet_path Path to current theme's stylesheet directory.
159 * @global string $wp_template_path Path to current theme's template directory.
160 *
161 * @return bool True if a child theme is in use, false otherwise.
162 */
163function is_child_theme() {
164 global $wp_stylesheet_path, $wp_template_path;
165
166 return $wp_stylesheet_path !== $wp_template_path;
167}
168
169/**
170 * Retrieves name of the current stylesheet.
171 *
172 * The theme name that is currently set as the front end theme.
173 *
174 * For all intents and purposes, the template name and the stylesheet name
175 * are going to be the same for most cases.
176 *
177 * @since 1.5.0
178 *
179 * @return string Stylesheet name.
180 */
181function get_stylesheet() {
182 /**
183 * Filters the name of current stylesheet.
184 *
185 * @since 1.5.0
186 *
187 * @param string $stylesheet Name of the current stylesheet.
188 */
189 return apply_filters( 'stylesheet', get_option( 'stylesheet' ) );
190}
191
192/**
193 * Retrieves stylesheet directory path for the active theme.
194 *
195 * @since 1.5.0
196 * @since 6.4.0 Memoizes filter execution so that it only runs once for the current theme.
197 * @since 6.4.2 Memoization removed.
198 *
199 * @return string Path to active theme's stylesheet directory.
200 */
201function get_stylesheet_directory() {
202 $stylesheet = get_stylesheet();
203 $theme_root = get_theme_root( $stylesheet );
204 $stylesheet_dir = "$theme_root/$stylesheet";
205
206 /**
207 * Filters the stylesheet directory path for the active theme.
208 *
209 * @since 1.5.0
210 *
211 * @param string $stylesheet_dir Absolute path to the active theme.
212 * @param string $stylesheet Directory name of the active theme.
213 * @param string $theme_root Absolute path to themes directory.
214 */
215 return apply_filters( 'stylesheet_directory', $stylesheet_dir, $stylesheet, $theme_root );
216}
217
218/**
219 * Retrieves stylesheet directory URI for the active theme.
220 *
221 * @since 1.5.0
222 *
223 * @return string URI to active theme's stylesheet directory.
224 */
225function get_stylesheet_directory_uri() {
226 $stylesheet = str_replace( '%2F', '/', rawurlencode( get_stylesheet() ) );
227 $theme_root_uri = get_theme_root_uri( $stylesheet );
228 $stylesheet_dir_uri = "$theme_root_uri/$stylesheet";
229
230 /**
231 * Filters the stylesheet directory URI.
232 *
233 * @since 1.5.0
234 *
235 * @param string $stylesheet_dir_uri Stylesheet directory URI.
236 * @param string $stylesheet Name of the activated theme's directory.
237 * @param string $theme_root_uri Themes root URI.
238 */
239 return apply_filters( 'stylesheet_directory_uri', $stylesheet_dir_uri, $stylesheet, $theme_root_uri );
240}
241
242/**
243 * Retrieves stylesheet URI for the active theme.
244 *
245 * The stylesheet file name is 'style.css' which is appended to the stylesheet directory URI path.
246 * See get_stylesheet_directory_uri().
247 *
248 * @since 1.5.0
249 *
250 * @return string URI to active theme's stylesheet.
251 */
252function get_stylesheet_uri() {
253 $stylesheet_dir_uri = get_stylesheet_directory_uri();
254 $stylesheet_uri = $stylesheet_dir_uri . '/style.css';
255 /**
256 * Filters the URI of the active theme stylesheet.
257 *
258 * @since 1.5.0
259 *
260 * @param string $stylesheet_uri Stylesheet URI for the active theme/child theme.
261 * @param string $stylesheet_dir_uri Stylesheet directory URI for the active theme/child theme.
262 */
263 return apply_filters( 'stylesheet_uri', $stylesheet_uri, $stylesheet_dir_uri );
264}
265
266/**
267 * Retrieves the localized stylesheet URI.
268 *
269 * The stylesheet directory for the localized stylesheet files are located, by
270 * default, in the base theme directory. The name of the locale file will be the
271 * locale followed by '.css'. If that does not exist, then the text direction
272 * stylesheet will be checked for existence, for example 'ltr.css'.
273 *
274 * The theme may change the location of the stylesheet directory by either using
275 * the {@see 'stylesheet_directory_uri'} or {@see 'locale_stylesheet_uri'} filters.
276 *
277 * If you want to change the location of the stylesheet files for the entire
278 * WordPress workflow, then change the former. If you just have the locale in a
279 * separate folder, then change the latter.
280 *
281 * @since 2.1.0
282 *
283 * @global WP_Locale $wp_locale WordPress date and time locale object.
284 *
285 * @return string URI to active theme's localized stylesheet.
286 */
287function get_locale_stylesheet_uri() {
288 global $wp_locale;
289 $stylesheet_dir_uri = get_stylesheet_directory_uri();
290 $dir = get_stylesheet_directory();
291 $locale = get_locale();
292 if ( file_exists( "$dir/$locale.css" ) ) {
293 $stylesheet_uri = "$stylesheet_dir_uri/$locale.css";
294 } elseif ( ! empty( $wp_locale->text_direction ) && file_exists( "$dir/{$wp_locale->text_direction}.css" ) ) {
295 $stylesheet_uri = "$stylesheet_dir_uri/{$wp_locale->text_direction}.css";
296 } else {
297 $stylesheet_uri = '';
298 }
299 /**
300 * Filters the localized stylesheet URI.
301 *
302 * @since 2.1.0
303 *
304 * @param string $stylesheet_uri Localized stylesheet URI.
305 * @param string $stylesheet_dir_uri Stylesheet directory URI.
306 */
307 return apply_filters( 'locale_stylesheet_uri', $stylesheet_uri, $stylesheet_dir_uri );
308}
309
310/**
311 * Retrieves name of the active theme.
312 *
313 * @since 1.5.0
314 *
315 * @return string Template name.
316 */
317function get_template() {
318 /**
319 * Filters the name of the active theme.
320 *
321 * @since 1.5.0
322 *
323 * @param string $template active theme's directory name.
324 */
325 return apply_filters( 'template', get_option( 'template' ) );
326}
327
328/**
329 * Retrieves template directory path for the active theme.
330 *
331 * @since 1.5.0
332 * @since 6.4.0 Memoizes filter execution so that it only runs once for the current theme.
333 * @since 6.4.1 Memoization removed.
334 *
335 * @return string Path to active theme's template directory.
336 */
337function get_template_directory() {
338 $template = get_template();
339 $theme_root = get_theme_root( $template );
340 $template_dir = "$theme_root/$template";
341
342 /**
343 * Filters the active theme directory path.
344 *
345 * @since 1.5.0
346 *
347 * @param string $template_dir The path of the active theme directory.
348 * @param string $template Directory name of the active theme.
349 * @param string $theme_root Absolute path to the themes directory.
350 */
351 return apply_filters( 'template_directory', $template_dir, $template, $theme_root );
352}
353
354/**
355 * Retrieves template directory URI for the active theme.
356 *
357 * @since 1.5.0
358 *
359 * @return string URI to active theme's template directory.
360 */
361function get_template_directory_uri() {
362 $template = str_replace( '%2F', '/', rawurlencode( get_template() ) );
363 $theme_root_uri = get_theme_root_uri( $template );
364 $template_dir_uri = "$theme_root_uri/$template";
365
366 /**
367 * Filters the active theme directory URI.
368 *
369 * @since 1.5.0
370 *
371 * @param string $template_dir_uri The URI of the active theme directory.
372 * @param string $template Directory name of the active theme.
373 * @param string $theme_root_uri The themes root URI.
374 */
375 return apply_filters( 'template_directory_uri', $template_dir_uri, $template, $theme_root_uri );
376}
377
378/**
379 * Retrieves theme roots.
380 *
381 * @since 2.9.0
382 *
383 * @global string[] $wp_theme_directories
384 *
385 * @return array|string An array of theme roots keyed by template/stylesheet
386 * or a single theme root if all themes have the same root.
387 */
388function get_theme_roots() {
389 global $wp_theme_directories;
390
391 if ( ! is_array( $wp_theme_directories ) || count( $wp_theme_directories ) <= 1 ) {
392 return '/themes';
393 }
394
395 $theme_roots = get_site_transient( 'theme_roots' );
396 if ( false === $theme_roots ) {
397 search_theme_directories( true ); // Regenerate the transient.
398 $theme_roots = get_site_transient( 'theme_roots' );
399 }
400 return $theme_roots;
401}
402
403/**
404 * Registers a directory that contains themes.
405 *
406 * @since 2.9.0
407 *
408 * @global string[] $wp_theme_directories
409 *
410 * @param string $directory Either the full filesystem path to a theme folder
411 * or a folder within WP_CONTENT_DIR.
412 * @return bool True if successfully registered a directory that contains themes,
413 * false if the directory does not exist.
414 */
415function register_theme_directory( $directory ) {
416 global $wp_theme_directories;
417
418 if ( ! file_exists( $directory ) ) {
419 // Try prepending as the theme directory could be relative to the content directory.
420 $directory = WP_CONTENT_DIR . '/' . $directory;
421 // If this directory does not exist, return and do not register.
422 if ( ! file_exists( $directory ) ) {
423 return false;
424 }
425 }
426
427 if ( ! is_array( $wp_theme_directories ) ) {
428 $wp_theme_directories = array();
429 }
430
431 $untrailed = untrailingslashit( $directory );
432 if ( ! empty( $untrailed ) && ! in_array( $untrailed, $wp_theme_directories, true ) ) {
433 $wp_theme_directories[] = $untrailed;
434 }
435
436 return true;
437}
438
439/**
440 * Searches all registered theme directories for complete and valid themes.
441 *
442 * @since 2.9.0
443 *
444 * @global string[] $wp_theme_directories
445 *
446 * @param bool $force Optional. Whether to force a new directory scan. Default false.
447 * @return array|false Valid themes found on success, false on failure.
448 */
449function search_theme_directories( $force = false ) {
450 global $wp_theme_directories;
451 static $found_themes = null;
452
453 if ( empty( $wp_theme_directories ) ) {
454 return false;
455 }
456
457 if ( ! $force && isset( $found_themes ) ) {
458 return $found_themes;
459 }
460
461 $found_themes = array();
462
463 $wp_theme_directories = (array) $wp_theme_directories;
464 $relative_theme_roots = array();
465
466 /*
467 * Set up maybe-relative, maybe-absolute array of theme directories.
468 * We always want to return absolute, but we need to cache relative
469 * to use in get_theme_root().
470 */
471 foreach ( $wp_theme_directories as $theme_root ) {
472 if ( str_starts_with( $theme_root, WP_CONTENT_DIR ) ) {
473 $relative_theme_roots[ str_replace( WP_CONTENT_DIR, '', $theme_root ) ] = $theme_root;
474 } else {
475 $relative_theme_roots[ $theme_root ] = $theme_root;
476 }
477 }
478
479 /**
480 * Filters whether to get the cache of the registered theme directories.
481 *
482 * @since 3.4.0
483 *
484 * @param bool $cache_expiration Whether to get the cache of the theme directories. Default false.
485 * @param string $context The class or function name calling the filter.
486 */
487 $cache_expiration = apply_filters( 'wp_cache_themes_persistently', false, 'search_theme_directories' );
488
489 if ( $cache_expiration ) {
490 $cached_roots = get_site_transient( 'theme_roots' );
491 if ( is_array( $cached_roots ) ) {
492 foreach ( $cached_roots as $theme_dir => $theme_root ) {
493 // A cached theme root is no longer around, so skip it.
494 if ( ! isset( $relative_theme_roots[ $theme_root ] ) ) {
495 continue;
496 }
497 $found_themes[ $theme_dir ] = array(
498 'theme_file' => $theme_dir . '/style.css',
499 'theme_root' => $relative_theme_roots[ $theme_root ], // Convert relative to absolute.
500 );
501 }
502 return $found_themes;
503 }
504 if ( ! is_int( $cache_expiration ) ) {
505 $cache_expiration = 30 * MINUTE_IN_SECONDS;
506 }
507 } else {
508 $cache_expiration = 30 * MINUTE_IN_SECONDS;
509 }
510
511 /* Loop the registered theme directories and extract all themes */
512 foreach ( $wp_theme_directories as $theme_root ) {
513
514 // Start with directories in the root of the active theme directory.
515 $dirs = @ scandir( $theme_root );
516 if ( ! $dirs ) {
517 wp_trigger_error( __FUNCTION__, "$theme_root is not readable" );
518 continue;
519 }
520 foreach ( $dirs as $dir ) {
521 if ( ! is_dir( $theme_root . '/' . $dir ) || '.' === $dir[0] || 'CVS' === $dir ) {
522 continue;
523 }
524 if ( file_exists( $theme_root . '/' . $dir . '/style.css' ) ) {
525 /*
526 * wp-content/themes/a-single-theme
527 * wp-content/themes is $theme_root, a-single-theme is $dir.
528 */
529 $found_themes[ $dir ] = array(
530 'theme_file' => $dir . '/style.css',
531 'theme_root' => $theme_root,
532 );
533 } else {
534 $found_theme = false;
535 /*
536 * wp-content/themes/a-folder-of-themes/*
537 * wp-content/themes is $theme_root, a-folder-of-themes is $dir, then themes are $sub_dirs.
538 */
539 $sub_dirs = @ scandir( $theme_root . '/' . $dir );
540 if ( ! $sub_dirs ) {
541 wp_trigger_error( __FUNCTION__, "$theme_root/$dir is not readable" );
542 continue;
543 }
544 foreach ( $sub_dirs as $sub_dir ) {
545 if ( ! is_dir( $theme_root . '/' . $dir . '/' . $sub_dir ) || '.' === $dir[0] || 'CVS' === $dir ) {
546 continue;
547 }
548 if ( ! file_exists( $theme_root . '/' . $dir . '/' . $sub_dir . '/style.css' ) ) {
549 continue;
550 }
551 $found_themes[ $dir . '/' . $sub_dir ] = array(
552 'theme_file' => $dir . '/' . $sub_dir . '/style.css',
553 'theme_root' => $theme_root,
554 );
555 $found_theme = true;
556 }
557 /*
558 * Never mind the above, it's just a theme missing a style.css.
559 * Return it; WP_Theme will catch the error.
560 */
561 if ( ! $found_theme ) {
562 $found_themes[ $dir ] = array(
563 'theme_file' => $dir . '/style.css',
564 'theme_root' => $theme_root,
565 );
566 }
567 }
568 }
569 }
570
571 asort( $found_themes );
572
573 $theme_roots = array();
574 $relative_theme_roots = array_flip( $relative_theme_roots );
575
576 foreach ( $found_themes as $theme_dir => $theme_data ) {
577 $theme_roots[ $theme_dir ] = $relative_theme_roots[ $theme_data['theme_root'] ]; // Convert absolute to relative.
578 }
579
580 if ( get_site_transient( 'theme_roots' ) !== $theme_roots ) {
581 set_site_transient( 'theme_roots', $theme_roots, $cache_expiration );
582 }
583
584 return $found_themes;
585}
586
587/**
588 * Retrieves path to themes directory.
589 *
590 * Does not have trailing slash.
591 *
592 * @since 1.5.0
593 *
594 * @global string[] $wp_theme_directories
595 *
596 * @param string $stylesheet_or_template Optional. The stylesheet or template name of the theme.
597 * Default is to leverage the main theme root.
598 * @return string Themes directory path.
599 */
600function get_theme_root( $stylesheet_or_template = '' ) {
601 global $wp_theme_directories;
602
603 $theme_root = '';
604
605 if ( $stylesheet_or_template ) {
606 $theme_root = get_raw_theme_root( $stylesheet_or_template );
607 if ( $theme_root ) {
608 /*
609 * Always prepend WP_CONTENT_DIR unless the root currently registered as a theme directory.
610 * This gives relative theme roots the benefit of the doubt when things go haywire.
611 */
612 if ( ! in_array( $theme_root, (array) $wp_theme_directories, true ) ) {
613 $theme_root = WP_CONTENT_DIR . $theme_root;
614 }
615 }
616 }
617
618 if ( ! $theme_root ) {
619 $theme_root = WP_CONTENT_DIR . '/themes';
620 }
621
622 /**
623 * Filters the absolute path to the themes directory.
624 *
625 * @since 1.5.0
626 *
627 * @param string $theme_root Absolute path to themes directory.
628 */
629 return apply_filters( 'theme_root', $theme_root );
630}
631
632/**
633 * Retrieves URI for themes directory.
634 *
635 * Does not have trailing slash.
636 *
637 * @since 1.5.0
638 *
639 * @global string[] $wp_theme_directories
640 *
641 * @param string $stylesheet_or_template Optional. The stylesheet or template name of the theme.
642 * Default is to leverage the main theme root.
643 * @param string $theme_root Optional. The theme root for which calculations will be based,
644 * preventing the need for a get_raw_theme_root() call. Default empty.
645 * @return string Themes directory URI.
646 */
647function get_theme_root_uri( $stylesheet_or_template = '', $theme_root = '' ) {
648 global $wp_theme_directories;
649
650 if ( $stylesheet_or_template && ! $theme_root ) {
651 $theme_root = get_raw_theme_root( $stylesheet_or_template );
652 }
653
654 if ( $stylesheet_or_template && $theme_root ) {
655 if ( in_array( $theme_root, (array) $wp_theme_directories, true ) ) {
656 // Absolute path. Make an educated guess. YMMV -- but note the filter below.
657 if ( str_starts_with( $theme_root, WP_CONTENT_DIR ) ) {
658 $theme_root_uri = content_url( str_replace( WP_CONTENT_DIR, '', $theme_root ) );
659 } elseif ( str_starts_with( $theme_root, ABSPATH ) ) {
660 $theme_root_uri = site_url( str_replace( ABSPATH, '', $theme_root ) );
661 } elseif ( str_starts_with( $theme_root, WP_PLUGIN_DIR ) || str_starts_with( $theme_root, WPMU_PLUGIN_DIR ) ) {
662 $theme_root_uri = plugins_url( basename( $theme_root ), $theme_root );
663 } else {
664 $theme_root_uri = $theme_root;
665 }
666 } else {
667 $theme_root_uri = content_url( $theme_root );
668 }
669 } else {
670 $theme_root_uri = content_url( 'themes' );
671 }
672
673 /**
674 * Filters the URI for themes directory.
675 *
676 * @since 1.5.0
677 *
678 * @param string $theme_root_uri The URI for themes directory.
679 * @param string $siteurl WordPress web address which is set in General Options.
680 * @param string $stylesheet_or_template The stylesheet or template name of the theme.
681 */
682 return apply_filters( 'theme_root_uri', $theme_root_uri, get_option( 'siteurl' ), $stylesheet_or_template );
683}
684
685/**
686 * Gets the raw theme root relative to the content directory with no filters applied.
687 *
688 * @since 3.1.0
689 *
690 * @global string[] $wp_theme_directories
691 *
692 * @param string $stylesheet_or_template The stylesheet or template name of the theme.
693 * @param bool $skip_cache Optional. Whether to skip the cache.
694 * Defaults to false, meaning the cache is used.
695 * @return string Theme root.
696 */
697function get_raw_theme_root( $stylesheet_or_template, $skip_cache = false ) {
698 global $wp_theme_directories;
699
700 if ( ! is_array( $wp_theme_directories ) || count( $wp_theme_directories ) <= 1 ) {
701 return '/themes';
702 }
703
704 $theme_root = false;
705
706 // If requesting the root for the active theme, consult options to avoid calling get_theme_roots().
707 if ( ! $skip_cache ) {
708 if ( get_option( 'stylesheet' ) === $stylesheet_or_template ) {
709 $theme_root = get_option( 'stylesheet_root' );
710 } elseif ( get_option( 'template' ) === $stylesheet_or_template ) {
711 $theme_root = get_option( 'template_root' );
712 }
713 }
714
715 if ( empty( $theme_root ) ) {
716 $theme_roots = get_theme_roots();
717 if ( ! empty( $theme_roots[ $stylesheet_or_template ] ) ) {
718 $theme_root = $theme_roots[ $stylesheet_or_template ];
719 }
720 }
721
722 return $theme_root;
723}
724
725/**
726 * Displays localized stylesheet link element.
727 *
728 * @since 2.1.0
729 */
730function locale_stylesheet() {
731 $stylesheet = get_locale_stylesheet_uri();
732 if ( empty( $stylesheet ) ) {
733 return;
734 }
735
736 $type_attr = current_theme_supports( 'html5', 'style' ) ? '' : ' type="text/css"';
737
738 printf(
739 '<link rel="stylesheet" href="%s"%s media="screen" />',
740 $stylesheet,
741 $type_attr
742 );
743}
744
745/**
746 * Switches the theme.
747 *
748 * Accepts one argument: $stylesheet of the theme. It also accepts an additional function signature
749 * of two arguments: $template then $stylesheet. This is for backward compatibility.
750 *
751 * @since 2.5.0
752 *
753 * @global string[] $wp_theme_directories
754 * @global WP_Customize_Manager $wp_customize
755 * @global array $sidebars_widgets
756 * @global array $wp_registered_sidebars
757 *
758 * @param string $stylesheet Stylesheet name.
759 */
760function switch_theme( $stylesheet ) {
761 global $wp_theme_directories, $wp_customize, $sidebars_widgets, $wp_registered_sidebars;
762
763 $requirements = validate_theme_requirements( $stylesheet );
764 if ( is_wp_error( $requirements ) ) {
765 wp_die( $requirements );
766 }
767
768 $_sidebars_widgets = null;
769 if ( 'wp_ajax_customize_save' === current_action() ) {
770 $old_sidebars_widgets_data_setting = $wp_customize->get_setting( 'old_sidebars_widgets_data' );
771 if ( $old_sidebars_widgets_data_setting ) {
772 $_sidebars_widgets = $wp_customize->post_value( $old_sidebars_widgets_data_setting );
773 }
774 } elseif ( is_array( $sidebars_widgets ) ) {
775 $_sidebars_widgets = $sidebars_widgets;
776 }
777
778 if ( is_array( $_sidebars_widgets ) ) {
779 set_theme_mod(
780 'sidebars_widgets',
781 array(
782 'time' => time(),
783 'data' => $_sidebars_widgets,
784 )
785 );
786 }
787
788 $nav_menu_locations = get_theme_mod( 'nav_menu_locations' );
789 update_option( 'theme_switch_menu_locations', $nav_menu_locations, true );
790
791 if ( func_num_args() > 1 ) {
792 $stylesheet = func_get_arg( 1 );
793 }
794
795 $old_theme = wp_get_theme();
796 $new_theme = wp_get_theme( $stylesheet );
797 $template = $new_theme->get_template();
798
799 if ( wp_is_recovery_mode() ) {
800 $paused_themes = wp_paused_themes();
801 $paused_themes->delete( $old_theme->get_stylesheet() );
802 $paused_themes->delete( $old_theme->get_template() );
803 }
804
805 update_option( 'template', $template );
806 update_option( 'stylesheet', $stylesheet );
807
808 if ( count( $wp_theme_directories ) > 1 ) {
809 update_option( 'template_root', get_raw_theme_root( $template, true ) );
810 update_option( 'stylesheet_root', get_raw_theme_root( $stylesheet, true ) );
811 } else {
812 delete_option( 'template_root' );
813 delete_option( 'stylesheet_root' );
814 }
815
816 $new_name = $new_theme->get( 'Name' );
817
818 update_option( 'current_theme', $new_name );
819
820 // Migrate from the old mods_{name} option to theme_mods_{slug}.
821 if ( is_admin() && false === get_option( 'theme_mods_' . $stylesheet ) ) {
822 $default_theme_mods = (array) get_option( 'mods_' . $new_name );
823 if ( ! empty( $nav_menu_locations ) && empty( $default_theme_mods['nav_menu_locations'] ) ) {
824 $default_theme_mods['nav_menu_locations'] = $nav_menu_locations;
825 }
826 add_option( "theme_mods_$stylesheet", $default_theme_mods );
827 } else {
828 /*
829 * Since retrieve_widgets() is called when initializing a theme in the Customizer,
830 * we need to remove the theme mods to avoid overwriting changes made via
831 * the Customizer when accessing wp-admin/widgets.php.
832 */
833 if ( 'wp_ajax_customize_save' === current_action() ) {
834 remove_theme_mod( 'sidebars_widgets' );
835 }
836 }
837
838 // Stores classic sidebars for later use by block themes.
839 if ( $new_theme->is_block_theme() ) {
840 set_theme_mod( 'wp_classic_sidebars', $wp_registered_sidebars );
841 }
842
843 update_option( 'theme_switched', $old_theme->get_stylesheet() );
844
845 /*
846 * Reset template globals when switching themes outside of a switched blog
847 * context to ensure templates will be loaded from the new theme.
848 */
849 if ( ! is_multisite() || ! ms_is_switched() ) {
850 wp_set_template_globals();
851 }
852
853 // Clear pattern caches.
854 if ( ! is_multisite() ) {
855 $new_theme->delete_pattern_cache();
856 $old_theme->delete_pattern_cache();
857 }
858
859 // Set autoload=no for the old theme, autoload=yes for the switched theme.
860 $theme_mods_options = array(
861 'theme_mods_' . $stylesheet => 'yes',
862 'theme_mods_' . $old_theme->get_stylesheet() => 'no',
863 );
864 wp_set_option_autoload_values( $theme_mods_options );
865
866 /**
867 * Fires after the theme is switched.
868 *
869 * See {@see 'after_switch_theme'}.
870 *
871 * @since 1.5.0
872 * @since 4.5.0 Introduced the `$old_theme` parameter.
873 *
874 * @param string $new_name Name of the new theme.
875 * @param WP_Theme $new_theme WP_Theme instance of the new theme.
876 * @param WP_Theme $old_theme WP_Theme instance of the old theme.
877 */
878 do_action( 'switch_theme', $new_name, $new_theme, $old_theme );
879}
880
881/**
882 * Checks that the active theme has the required files.
883 *
884 * Standalone themes need to have a `templates/index.html` or `index.php` template file.
885 * Child themes need to have a `Template` header in the `style.css` stylesheet.
886 *
887 * Does not initially check the default theme, which is the fallback and should always exist.
888 * But if it doesn't exist, it'll fall back to the latest core default theme that does exist.
889 * Will switch theme to the fallback theme if active theme does not validate.
890 *
891 * You can use the {@see 'validate_current_theme'} filter to return false to disable
892 * this functionality.
893 *
894 * @since 1.5.0
895 * @since 6.0.0 Removed the requirement for block themes to have an `index.php` template.
896 *
897 * @see WP_DEFAULT_THEME
898 *
899 * @return bool
900 */
901function validate_current_theme() {
902 /**
903 * Filters whether to validate the active theme.
904 *
905 * @since 2.7.0
906 *
907 * @param bool $validate Whether to validate the active theme. Default true.
908 */
909 if ( wp_installing() || ! apply_filters( 'validate_current_theme', true ) ) {
910 return true;
911 }
912
913 if (
914 ! file_exists( get_template_directory() . '/templates/index.html' )
915 && ! file_exists( get_template_directory() . '/block-templates/index.html' ) // Deprecated path support since 5.9.0.
916 && ! file_exists( get_template_directory() . '/index.php' )
917 ) {
918 // Invalid.
919 } elseif ( ! file_exists( get_template_directory() . '/style.css' ) ) {
920 // Invalid.
921 } elseif ( is_child_theme() && ! file_exists( get_stylesheet_directory() . '/style.css' ) ) {
922 // Invalid.
923 } else {
924 // Valid.
925 return true;
926 }
927
928 $default = wp_get_theme( WP_DEFAULT_THEME );
929 if ( $default->exists() ) {
930 switch_theme( WP_DEFAULT_THEME );
931 return false;
932 }
933
934 /**
935 * If we're in an invalid state but WP_DEFAULT_THEME doesn't exist,
936 * switch to the latest core default theme that's installed.
937 *
938 * If it turns out that this latest core default theme is our current
939 * theme, then there's nothing we can do about that, so we have to bail,
940 * rather than going into an infinite loop. (This is why there are
941 * checks against WP_DEFAULT_THEME above, also.) We also can't do anything
942 * if it turns out there is no default theme installed. (That's `false`.)
943 */
944 $default = WP_Theme::get_core_default_theme();
945 if ( false === $default || get_stylesheet() === $default->get_stylesheet() ) {
946 return true;
947 }
948
949 switch_theme( $default->get_stylesheet() );
950 return false;
951}
952
953/**
954 * Validates the theme requirements for WordPress version and PHP version.
955 *
956 * Uses the information from `Requires at least` and `Requires PHP` headers
957 * defined in the theme's `style.css` file.
958 *
959 * @since 5.5.0
960 * @since 5.8.0 Removed support for using `readme.txt` as a fallback.
961 *
962 * @param string $stylesheet Directory name for the theme.
963 * @return true|WP_Error True if requirements are met, WP_Error on failure.
964 */
965function validate_theme_requirements( $stylesheet ) {
966 $theme = wp_get_theme( $stylesheet );
967
968 $requirements = array(
969 'requires' => ! empty( $theme->get( 'RequiresWP' ) ) ? $theme->get( 'RequiresWP' ) : '',
970 'requires_php' => ! empty( $theme->get( 'RequiresPHP' ) ) ? $theme->get( 'RequiresPHP' ) : '',
971 );
972
973 $compatible_wp = is_wp_version_compatible( $requirements['requires'] );
974 $compatible_php = is_php_version_compatible( $requirements['requires_php'] );
975
976 if ( ! $compatible_wp && ! $compatible_php ) {
977 return new WP_Error(
978 'theme_wp_php_incompatible',
979 sprintf(
980 /* translators: %s: Theme name. */
981 _x( '<strong>Error:</strong> Current WordPress and PHP versions do not meet minimum requirements for %s.', 'theme' ),
982 $theme->display( 'Name' )
983 )
984 );
985 } elseif ( ! $compatible_php ) {
986 return new WP_Error(
987 'theme_php_incompatible',
988 sprintf(
989 /* translators: %s: Theme name. */
990 _x( '<strong>Error:</strong> Current PHP version does not meet minimum requirements for %s.', 'theme' ),
991 $theme->display( 'Name' )
992 )
993 );
994 } elseif ( ! $compatible_wp ) {
995 return new WP_Error(
996 'theme_wp_incompatible',
997 sprintf(
998 /* translators: %s: Theme name. */
999 _x( '<strong>Error:</strong> Current WordPress version does not meet minimum requirements for %s.', 'theme' ),
1000 $theme->display( 'Name' )
1001 )
1002 );
1003 }
1004
1005 /**
1006 * Filters the theme requirement validation response.
1007 *
1008 * If a theme fails due to a Core-provided validation (incompatible WP, PHP versions), this
1009 * filter will not fire. A WP_Error response will already be returned.
1010 *
1011 * This filter is intended to add additional validation steps by site administrators.
1012 *
1013 * @since 6.9.0
1014 *
1015 * @param bool|WP_Error $met_requirements True if the theme meets requirements, WP_Error if not.
1016 * @param string $stylesheet Directory name for the theme.
1017 */
1018 return apply_filters( 'validate_theme_requirements', true, $stylesheet );
1019}
1020
1021/**
1022 * Retrieves all theme modifications.
1023 *
1024 * @since 3.1.0
1025 * @since 5.9.0 The return value is always an array.
1026 *
1027 * @return array Theme modifications.
1028 */
1029function get_theme_mods() {
1030 $theme_slug = get_option( 'stylesheet' );
1031 $mods = get_option( "theme_mods_$theme_slug" );
1032
1033 if ( false === $mods ) {
1034 $theme_name = get_option( 'current_theme' );
1035 if ( false === $theme_name ) {
1036 $theme_name = wp_get_theme()->get( 'Name' );
1037 }
1038
1039 $mods = get_option( "mods_$theme_name" ); // Deprecated location.
1040 if ( is_admin() && false !== $mods ) {
1041 update_option( "theme_mods_$theme_slug", $mods );
1042 delete_option( "mods_$theme_name" );
1043 }
1044 }
1045
1046 if ( ! is_array( $mods ) ) {
1047 $mods = array();
1048 }
1049
1050 return $mods;
1051}
1052
1053/**
1054 * Retrieves theme modification value for the active theme.
1055 *
1056 * If the modification name does not exist and `$default_value` is a string, then the
1057 * default will be passed through the {@link https://www.php.net/sprintf sprintf()}
1058 * PHP function with the template directory URI as the first value and the
1059 * stylesheet directory URI as the second value.
1060 *
1061 * @since 2.1.0
1062 *
1063 * @param string $name Theme modification name.
1064 * @param mixed $default_value Optional. Theme modification default value. Default false.
1065 * @return mixed Theme modification value.
1066 */
1067function get_theme_mod( $name, $default_value = false ) {
1068 $mods = get_theme_mods();
1069
1070 if ( isset( $mods[ $name ] ) ) {
1071 /**
1072 * Filters the theme modification, or 'theme_mod', value.
1073 *
1074 * The dynamic portion of the hook name, `$name`, refers to the key name
1075 * of the modification array. For example, 'header_textcolor', 'header_image',
1076 * and so on depending on the theme options.
1077 *
1078 * @since 2.2.0
1079 *
1080 * @param mixed $current_mod The value of the active theme modification.
1081 */
1082 return apply_filters( "theme_mod_{$name}", $mods[ $name ] );
1083 }
1084
1085 if ( is_string( $default_value ) ) {
1086 // Only run the replacement if an sprintf() string format pattern was found.
1087 if ( preg_match( '#(?<!%)%(?:\d+\$?)?s#', $default_value ) ) {
1088 // Remove a single trailing percent sign.
1089 $default_value = preg_replace( '#(?<!%)%$#', '', $default_value );
1090 $default_value = sprintf( $default_value, get_template_directory_uri(), get_stylesheet_directory_uri() );
1091 }
1092 }
1093
1094 /** This filter is documented in wp-includes/theme.php */
1095 return apply_filters( "theme_mod_{$name}", $default_value );
1096}
1097
1098/**
1099 * Updates theme modification value for the active theme.
1100 *
1101 * @since 2.1.0
1102 * @since 5.6.0 A return value was added.
1103 *
1104 * @param string $name Theme modification name.
1105 * @param mixed $value Theme modification value.
1106 * @return bool True if the value was updated, false otherwise.
1107 */
1108function set_theme_mod( $name, $value ) {
1109 $mods = get_theme_mods();
1110 $old_value = isset( $mods[ $name ] ) ? $mods[ $name ] : false;
1111
1112 /**
1113 * Filters the theme modification, or 'theme_mod', value on save.
1114 *
1115 * The dynamic portion of the hook name, `$name`, refers to the key name
1116 * of the modification array. For example, 'header_textcolor', 'header_image',
1117 * and so on depending on the theme options.
1118 *
1119 * @since 3.9.0
1120 *
1121 * @param mixed $value The new value of the theme modification.
1122 * @param mixed $old_value The current value of the theme modification.
1123 */
1124 $mods[ $name ] = apply_filters( "pre_set_theme_mod_{$name}", $value, $old_value );
1125
1126 $theme = get_option( 'stylesheet' );
1127
1128 return update_option( "theme_mods_$theme", $mods );
1129}
1130
1131/**
1132 * Removes theme modification name from active theme list.
1133 *
1134 * If removing the name also removes all elements, then the entire option
1135 * will be removed.
1136 *
1137 * @since 2.1.0
1138 *
1139 * @param string $name Theme modification name.
1140 */
1141function remove_theme_mod( $name ) {
1142 $mods = get_theme_mods();
1143
1144 if ( ! isset( $mods[ $name ] ) ) {
1145 return;
1146 }
1147
1148 unset( $mods[ $name ] );
1149
1150 if ( empty( $mods ) ) {
1151 remove_theme_mods();
1152 return;
1153 }
1154
1155 $theme = get_option( 'stylesheet' );
1156
1157 update_option( "theme_mods_$theme", $mods );
1158}
1159
1160/**
1161 * Removes theme modifications option for the active theme.
1162 *
1163 * @since 2.1.0
1164 */
1165function remove_theme_mods() {
1166 delete_option( 'theme_mods_' . get_option( 'stylesheet' ) );
1167
1168 // Old style.
1169 $theme_name = get_option( 'current_theme' );
1170 if ( false === $theme_name ) {
1171 $theme_name = wp_get_theme()->get( 'Name' );
1172 }
1173
1174 delete_option( 'mods_' . $theme_name );
1175}
1176
1177/**
1178 * Retrieves the custom header text color in 3- or 6-digit hexadecimal form.
1179 *
1180 * @since 2.1.0
1181 *
1182 * @return string Header text color in 3- or 6-digit hexadecimal form (minus the hash symbol).
1183 */
1184function get_header_textcolor() {
1185 return get_theme_mod( 'header_textcolor', get_theme_support( 'custom-header', 'default-text-color' ) );
1186}
1187
1188/**
1189 * Displays the custom header text color in 3- or 6-digit hexadecimal form (minus the hash symbol).
1190 *
1191 * @since 2.1.0
1192 */
1193function header_textcolor() {
1194 echo get_header_textcolor();
1195}
1196
1197/**
1198 * Whether to display the header text.
1199 *
1200 * @since 3.4.0
1201 *
1202 * @return bool
1203 */
1204function display_header_text() {
1205 if ( ! current_theme_supports( 'custom-header', 'header-text' ) ) {
1206 return false;
1207 }
1208
1209 $text_color = get_theme_mod( 'header_textcolor', get_theme_support( 'custom-header', 'default-text-color' ) );
1210 return 'blank' !== $text_color;
1211}
1212
1213/**
1214 * Checks whether a header image is set or not.
1215 *
1216 * @since 4.2.0
1217 *
1218 * @see get_header_image()
1219 *
1220 * @return bool Whether a header image is set or not.
1221 */
1222function has_header_image() {
1223 return (bool) get_header_image();
1224}
1225
1226/**
1227 * Retrieves header image for custom header.
1228 *
1229 * @since 2.1.0
1230 *
1231 * @return string|false
1232 */
1233function get_header_image() {
1234 $url = get_theme_mod( 'header_image', get_theme_support( 'custom-header', 'default-image' ) );
1235
1236 if ( 'remove-header' === $url ) {
1237 return false;
1238 }
1239
1240 if ( is_random_header_image() ) {
1241 $url = get_random_header_image();
1242 }
1243
1244 /**
1245 * Filters the header image URL.
1246 *
1247 * @since 6.1.0
1248 *
1249 * @param string $url Header image URL.
1250 */
1251 $url = apply_filters( 'get_header_image', $url );
1252
1253 if ( ! is_string( $url ) ) {
1254 return false;
1255 }
1256
1257 $url = trim( $url );
1258 return sanitize_url( set_url_scheme( $url ) );
1259}
1260
1261/**
1262 * Creates image tag markup for a custom header image.
1263 *
1264 * @since 4.4.0
1265 *
1266 * @param array $attr Optional. Additional attributes for the image tag. Can be used
1267 * to override the default attributes. Default empty.
1268 * @return string HTML image element markup or empty string on failure.
1269 */
1270function get_header_image_tag( $attr = array() ) {
1271 $header = get_custom_header();
1272 $header->url = get_header_image();
1273
1274 if ( ! $header->url ) {
1275 return '';
1276 }
1277
1278 $width = absint( $header->width );
1279 $height = absint( $header->height );
1280 $alt = '';
1281
1282 // Use alternative text assigned to the image, if available. Otherwise, leave it empty.
1283 if ( ! empty( $header->attachment_id ) ) {
1284 $image_alt = get_post_meta( $header->attachment_id, '_wp_attachment_image_alt', true );
1285
1286 if ( is_string( $image_alt ) ) {
1287 $alt = $image_alt;
1288 }
1289 }
1290
1291 $attr = wp_parse_args(
1292 $attr,
1293 array(
1294 'src' => $header->url,
1295 'width' => $width,
1296 'height' => $height,
1297 'alt' => $alt,
1298 )
1299 );
1300
1301 // Generate 'srcset' and 'sizes' if not already present.
1302 if ( empty( $attr['srcset'] ) && ! empty( $header->attachment_id ) ) {
1303 $image_meta = get_post_meta( $header->attachment_id, '_wp_attachment_metadata', true );
1304 $size_array = array( $width, $height );
1305
1306 if ( is_array( $image_meta ) ) {
1307 $srcset = wp_calculate_image_srcset( $size_array, $header->url, $image_meta, $header->attachment_id );
1308
1309 if ( ! empty( $attr['sizes'] ) ) {
1310 $sizes = $attr['sizes'];
1311 } else {
1312 $sizes = wp_calculate_image_sizes( $size_array, $header->url, $image_meta, $header->attachment_id );
1313 }
1314
1315 if ( $srcset && $sizes ) {
1316 $attr['srcset'] = $srcset;
1317 $attr['sizes'] = $sizes;
1318 }
1319 }
1320 }
1321
1322 $attr = array_merge(
1323 $attr,
1324 wp_get_loading_optimization_attributes( 'img', $attr, 'get_header_image_tag' )
1325 );
1326
1327 /*
1328 * If the default value of `lazy` for the `loading` attribute is overridden
1329 * to omit the attribute for this image, ensure it is not included.
1330 */
1331 if ( isset( $attr['loading'] ) && ! $attr['loading'] ) {
1332 unset( $attr['loading'] );
1333 }
1334
1335 // If the `fetchpriority` attribute is overridden and set to false or an empty string.
1336 if ( isset( $attr['fetchpriority'] ) && ! $attr['fetchpriority'] ) {
1337 unset( $attr['fetchpriority'] );
1338 }
1339
1340 // If the `decoding` attribute is overridden and set to false or an empty string.
1341 if ( isset( $attr['decoding'] ) && ! $attr['decoding'] ) {
1342 unset( $attr['decoding'] );
1343 }
1344
1345 /**
1346 * Filters the list of header image attributes.
1347 *
1348 * @since 5.9.0
1349 *
1350 * @param array $attr Array of the attributes for the image tag.
1351 * @param object $header The custom header object returned by 'get_custom_header()'.
1352 */
1353 $attr = apply_filters( 'get_header_image_tag_attributes', $attr, $header );
1354
1355 $attr = array_map( 'esc_attr', $attr );
1356 $html = '<img';
1357
1358 foreach ( $attr as $name => $value ) {
1359 $html .= ' ' . $name . '="' . $value . '"';
1360 }
1361
1362 $html .= ' />';
1363
1364 /**
1365 * Filters the markup of header images.
1366 *
1367 * @since 4.4.0
1368 *
1369 * @param string $html The HTML image tag markup being filtered.
1370 * @param object $header The custom header object returned by 'get_custom_header()'.
1371 * @param array $attr Array of the attributes for the image tag.
1372 */
1373 return apply_filters( 'get_header_image_tag', $html, $header, $attr );
1374}
1375
1376/**
1377 * Displays the image markup for a custom header image.
1378 *
1379 * @since 4.4.0
1380 *
1381 * @param array $attr Optional. Attributes for the image markup. Default empty.
1382 */
1383function the_header_image_tag( $attr = array() ) {
1384 echo get_header_image_tag( $attr );
1385}
1386
1387/**
1388 * Gets random header image data from registered images in theme.
1389 *
1390 * @since 3.4.0
1391 *
1392 * @access private
1393 *
1394 * @global array $_wp_default_headers
1395 *
1396 * @return object
1397 */
1398function _get_random_header_data() {
1399 global $_wp_default_headers;
1400 static $_wp_random_header = null;
1401
1402 if ( empty( $_wp_random_header ) ) {
1403 $header_image_mod = get_theme_mod( 'header_image', '' );
1404 $headers = array();
1405
1406 if ( 'random-uploaded-image' === $header_image_mod ) {
1407 $headers = get_uploaded_header_images();
1408 } elseif ( ! empty( $_wp_default_headers ) ) {
1409 if ( 'random-default-image' === $header_image_mod ) {
1410 $headers = $_wp_default_headers;
1411 } else {
1412 if ( current_theme_supports( 'custom-header', 'random-default' ) ) {
1413 $headers = $_wp_default_headers;
1414 }
1415 }
1416 }
1417
1418 if ( empty( $headers ) ) {
1419 return new stdClass();
1420 }
1421
1422 $_wp_random_header = (object) $headers[ array_rand( $headers ) ];
1423
1424 $_wp_random_header->url = sprintf(
1425 $_wp_random_header->url,
1426 get_template_directory_uri(),
1427 get_stylesheet_directory_uri()
1428 );
1429
1430 $_wp_random_header->thumbnail_url = sprintf(
1431 $_wp_random_header->thumbnail_url,
1432 get_template_directory_uri(),
1433 get_stylesheet_directory_uri()
1434 );
1435 }
1436
1437 return $_wp_random_header;
1438}
1439
1440/**
1441 * Gets random header image URL from registered images in theme.
1442 *
1443 * @since 3.2.0
1444 *
1445 * @return string Path to header image.
1446 */
1447function get_random_header_image() {
1448 $random_image = _get_random_header_data();
1449
1450 if ( empty( $random_image->url ) ) {
1451 return '';
1452 }
1453
1454 return $random_image->url;
1455}
1456
1457/**
1458 * Checks if random header image is in use.
1459 *
1460 * Always true if user expressly chooses the option in Appearance > Header.
1461 * Also true if theme has multiple header images registered, no specific header image
1462 * is chosen, and theme turns on random headers with add_theme_support().
1463 *
1464 * @since 3.2.0
1465 *
1466 * @param string $type The random pool to use. Possible values include 'any',
1467 * 'default', 'uploaded'. Default 'any'.
1468 * @return bool
1469 */
1470function is_random_header_image( $type = 'any' ) {
1471 $header_image_mod = get_theme_mod( 'header_image', get_theme_support( 'custom-header', 'default-image' ) );
1472
1473 if ( 'any' === $type ) {
1474 if ( 'random-default-image' === $header_image_mod
1475 || 'random-uploaded-image' === $header_image_mod
1476 || ( empty( $header_image_mod ) && '' !== get_random_header_image() )
1477 ) {
1478 return true;
1479 }
1480 } else {
1481 if ( "random-$type-image" === $header_image_mod ) {
1482 return true;
1483 } elseif ( 'default' === $type
1484 && empty( $header_image_mod ) && '' !== get_random_header_image()
1485 ) {
1486 return true;
1487 }
1488 }
1489
1490 return false;
1491}
1492
1493/**
1494 * Displays header image URL.
1495 *
1496 * @since 2.1.0
1497 */
1498function header_image() {
1499 $image = get_header_image();
1500
1501 if ( $image ) {
1502 echo esc_url( $image );
1503 }
1504}
1505
1506/**
1507 * Gets the header images uploaded for the active theme.
1508 *
1509 * @since 3.2.0
1510 *
1511 * @return array
1512 */
1513function get_uploaded_header_images() {
1514 $header_images = array();
1515
1516 $headers = get_posts(
1517 array(
1518 'post_type' => 'attachment',
1519 'meta_key' => '_wp_attachment_is_custom_header',
1520 'meta_value' => get_option( 'stylesheet' ),
1521 'orderby' => 'none',
1522 'nopaging' => true,
1523 )
1524 );
1525
1526 if ( empty( $headers ) ) {
1527 return array();
1528 }
1529
1530 foreach ( (array) $headers as $header ) {
1531 $url = sanitize_url( wp_get_attachment_url( $header->ID ) );
1532 $header_data = wp_get_attachment_metadata( $header->ID );
1533 $header_index = $header->ID;
1534
1535 $header_images[ $header_index ] = array();
1536 $header_images[ $header_index ]['attachment_id'] = $header->ID;
1537 $header_images[ $header_index ]['url'] = $url;
1538 $header_images[ $header_index ]['thumbnail_url'] = $url;
1539 $header_images[ $header_index ]['alt_text'] = get_post_meta( $header->ID, '_wp_attachment_image_alt', true );
1540
1541 if ( isset( $header_data['attachment_parent'] ) ) {
1542 $header_images[ $header_index ]['attachment_parent'] = $header_data['attachment_parent'];
1543 } else {
1544 $header_images[ $header_index ]['attachment_parent'] = '';
1545 }
1546
1547 if ( isset( $header_data['width'] ) ) {
1548 $header_images[ $header_index ]['width'] = $header_data['width'];
1549 }
1550 if ( isset( $header_data['height'] ) ) {
1551 $header_images[ $header_index ]['height'] = $header_data['height'];
1552 }
1553 }
1554
1555 return $header_images;
1556}
1557
1558/**
1559 * Gets the header image data.
1560 *
1561 * @since 3.4.0
1562 *
1563 * @global array $_wp_default_headers
1564 *
1565 * @return object
1566 */
1567function get_custom_header() {
1568 global $_wp_default_headers;
1569
1570 if ( is_random_header_image() ) {
1571 $data = _get_random_header_data();
1572 } else {
1573 $data = get_theme_mod( 'header_image_data' );
1574 if ( ! $data && current_theme_supports( 'custom-header', 'default-image' ) ) {
1575 $directory_args = array( get_template_directory_uri(), get_stylesheet_directory_uri() );
1576 $data = array();
1577 $data['url'] = vsprintf( get_theme_support( 'custom-header', 'default-image' ), $directory_args );
1578 $data['thumbnail_url'] = $data['url'];
1579 if ( ! empty( $_wp_default_headers ) ) {
1580 foreach ( (array) $_wp_default_headers as $default_header ) {
1581 $url = vsprintf( $default_header['url'], $directory_args );
1582 if ( $data['url'] === $url ) {
1583 $data = $default_header;
1584 $data['url'] = $url;
1585 $data['thumbnail_url'] = vsprintf( $data['thumbnail_url'], $directory_args );
1586 break;
1587 }
1588 }
1589 }
1590 }
1591 }
1592
1593 $default = array(
1594 'url' => '',
1595 'thumbnail_url' => '',
1596 'width' => get_theme_support( 'custom-header', 'width' ),
1597 'height' => get_theme_support( 'custom-header', 'height' ),
1598 'video' => get_theme_support( 'custom-header', 'video' ),
1599 );
1600 return (object) wp_parse_args( $data, $default );
1601}
1602
1603/**
1604 * Registers a selection of default headers to be displayed by the custom header admin UI.
1605 *
1606 * @since 3.0.0
1607 *
1608 * @global array $_wp_default_headers
1609 *
1610 * @param array $headers Array of headers keyed by a string ID. The IDs point to arrays
1611 * containing 'url', 'thumbnail_url', and 'description' keys.
1612 */
1613function register_default_headers( $headers ) {
1614 global $_wp_default_headers;
1615
1616 $_wp_default_headers = array_merge( (array) $_wp_default_headers, (array) $headers );
1617}
1618
1619/**
1620 * Unregisters default headers.
1621 *
1622 * This function must be called after register_default_headers() has already added the
1623 * header you want to remove.
1624 *
1625 * @see register_default_headers()
1626 * @since 3.0.0
1627 *
1628 * @global array $_wp_default_headers
1629 *
1630 * @param string|array $header The header string id (key of array) to remove, or an array thereof.
1631 * @return bool|void A single header returns true on success, false on failure.
1632 * There is currently no return value for multiple headers.
1633 */
1634function unregister_default_headers( $header ) {
1635 global $_wp_default_headers;
1636
1637 if ( is_array( $header ) ) {
1638 array_map( 'unregister_default_headers', $header );
1639 } elseif ( isset( $_wp_default_headers[ $header ] ) ) {
1640 unset( $_wp_default_headers[ $header ] );
1641 return true;
1642 } else {
1643 return false;
1644 }
1645}
1646
1647/**
1648 * Checks whether a header video is set or not.
1649 *
1650 * @since 4.7.0
1651 *
1652 * @see get_header_video_url()
1653 *
1654 * @return bool Whether a header video is set or not.
1655 */
1656function has_header_video() {
1657 return (bool) get_header_video_url();
1658}
1659
1660/**
1661 * Retrieves header video URL for custom header.
1662 *
1663 * Uses a local video if present, or falls back to an external video.
1664 *
1665 * @since 4.7.0
1666 *
1667 * @return string|false Header video URL or false if there is no video.
1668 */
1669function get_header_video_url() {
1670 $id = absint( get_theme_mod( 'header_video' ) );
1671
1672 if ( $id ) {
1673 // Get the file URL from the attachment ID.
1674 $url = wp_get_attachment_url( $id );
1675 } else {
1676 $url = get_theme_mod( 'external_header_video' );
1677 }
1678
1679 /**
1680 * Filters the header video URL.
1681 *
1682 * @since 4.7.3
1683 *
1684 * @param string $url Header video URL, if available.
1685 */
1686 $url = apply_filters( 'get_header_video_url', $url );
1687
1688 if ( ! $id && ! $url ) {
1689 return false;
1690 }
1691
1692 return sanitize_url( set_url_scheme( $url ) );
1693}
1694
1695/**
1696 * Displays header video URL.
1697 *
1698 * @since 4.7.0
1699 */
1700function the_header_video_url() {
1701 $video = get_header_video_url();
1702
1703 if ( $video ) {
1704 echo esc_url( $video );
1705 }
1706}
1707
1708/**
1709 * Retrieves header video settings.
1710 *
1711 * @since 4.7.0
1712 *
1713 * @return array
1714 */
1715function get_header_video_settings() {
1716 $header = get_custom_header();
1717 $video_url = get_header_video_url();
1718 $video_type = wp_check_filetype( $video_url, wp_get_mime_types() );
1719
1720 $settings = array(
1721 'mimeType' => '',
1722 'posterUrl' => get_header_image(),
1723 'videoUrl' => $video_url,
1724 'width' => absint( $header->width ),
1725 'height' => absint( $header->height ),
1726 'minWidth' => 900,
1727 'minHeight' => 500,
1728 'l10n' => array(
1729 'pause' => __( 'Pause' ),
1730 'play' => __( 'Play' ),
1731 'pauseSpeak' => __( 'Video is paused.' ),
1732 'playSpeak' => __( 'Video is playing.' ),
1733 ),
1734 );
1735
1736 if ( preg_match( '#^https?://(?:www\.)?(?:youtube\.com/watch|youtu\.be/)#', $video_url ) ) {
1737 $settings['mimeType'] = 'video/x-youtube';
1738 } elseif ( ! empty( $video_type['type'] ) ) {
1739 $settings['mimeType'] = $video_type['type'];
1740 }
1741
1742 /**
1743 * Filters header video settings.
1744 *
1745 * @since 4.7.0
1746 *
1747 * @param array $settings An array of header video settings.
1748 */
1749 return apply_filters( 'header_video_settings', $settings );
1750}
1751
1752/**
1753 * Checks whether a custom header is set or not.
1754 *
1755 * @since 4.7.0
1756 *
1757 * @return bool True if a custom header is set. False if not.
1758 */
1759function has_custom_header() {
1760 if ( has_header_image() || ( has_header_video() && is_header_video_active() ) ) {
1761 return true;
1762 }
1763
1764 return false;
1765}
1766
1767/**
1768 * Checks whether the custom header video is eligible to show on the current page.
1769 *
1770 * @since 4.7.0
1771 *
1772 * @return bool True if the custom header video should be shown. False if not.
1773 */
1774function is_header_video_active() {
1775 if ( ! get_theme_support( 'custom-header', 'video' ) ) {
1776 return false;
1777 }
1778
1779 $video_active_cb = get_theme_support( 'custom-header', 'video-active-callback' );
1780
1781 if ( empty( $video_active_cb ) || ! is_callable( $video_active_cb ) ) {
1782 $show_video = true;
1783 } else {
1784 $show_video = call_user_func( $video_active_cb );
1785 }
1786
1787 /**
1788 * Filters whether the custom header video is eligible to show on the current page.
1789 *
1790 * @since 4.7.0
1791 *
1792 * @param bool $show_video Whether the custom header video should be shown. Returns the value
1793 * of the theme setting for the `custom-header`'s `video-active-callback`.
1794 * If no callback is set, the default value is that of `is_front_page()`.
1795 */
1796 return apply_filters( 'is_header_video_active', $show_video );
1797}
1798
1799/**
1800 * Retrieves the markup for a custom header.
1801 *
1802 * The container div will always be returned in the Customizer preview.
1803 *
1804 * @since 4.7.0
1805 *
1806 * @return string The markup for a custom header on success.
1807 */
1808function get_custom_header_markup() {
1809 if ( ! has_custom_header() && ! is_customize_preview() ) {
1810 return '';
1811 }
1812
1813 return sprintf(
1814 '<div id="wp-custom-header" class="wp-custom-header">%s</div>',
1815 get_header_image_tag()
1816 );
1817}
1818
1819/**
1820 * Prints the markup for a custom header.
1821 *
1822 * A container div will always be printed in the Customizer preview.
1823 *
1824 * @since 4.7.0
1825 */
1826function the_custom_header_markup() {
1827 $custom_header = get_custom_header_markup();
1828 if ( empty( $custom_header ) ) {
1829 return;
1830 }
1831
1832 echo $custom_header;
1833
1834 if ( is_header_video_active() && ( has_header_video() || is_customize_preview() ) ) {
1835 wp_enqueue_script( 'wp-custom-header' );
1836 wp_localize_script( 'wp-custom-header', '_wpCustomHeaderSettings', get_header_video_settings() );
1837 }
1838}
1839
1840/**
1841 * Retrieves background image for custom background.
1842 *
1843 * @since 3.0.0
1844 *
1845 * @return string
1846 */
1847function get_background_image() {
1848 return get_theme_mod( 'background_image', get_theme_support( 'custom-background', 'default-image' ) );
1849}
1850
1851/**
1852 * Displays background image path.
1853 *
1854 * @since 3.0.0
1855 */
1856function background_image() {
1857 echo get_background_image();
1858}
1859
1860/**
1861 * Retrieves value for custom background color.
1862 *
1863 * @since 3.0.0
1864 *
1865 * @return string
1866 */
1867function get_background_color() {
1868 return get_theme_mod( 'background_color', get_theme_support( 'custom-background', 'default-color' ) );
1869}
1870
1871/**
1872 * Displays background color value.
1873 *
1874 * @since 3.0.0
1875 */
1876function background_color() {
1877 echo get_background_color();
1878}
1879
1880/**
1881 * Default custom background callback.
1882 *
1883 * @since 3.0.0
1884 */
1885function _custom_background_cb() {
1886 // $background is the saved custom image, or the default image.
1887 $background = set_url_scheme( get_background_image() );
1888
1889 /*
1890 * $color is the saved custom color.
1891 * A default has to be specified in style.css. It will not be printed here.
1892 */
1893 $color = get_background_color();
1894
1895 if ( get_theme_support( 'custom-background', 'default-color' ) === $color ) {
1896 $color = false;
1897 }
1898
1899 $type_attr = current_theme_supports( 'html5', 'style' ) ? '' : ' type="text/css"';
1900
1901 if ( ! $background && ! $color ) {
1902 if ( is_customize_preview() ) {
1903 printf( '<style%s id="custom-background-css"></style>', $type_attr );
1904 }
1905 return;
1906 }
1907
1908 $style = $color ? 'background-color: ' . maybe_hash_hex_color( $color ) . ';' : '';
1909
1910 if ( $background ) {
1911 $image = ' background-image: url("' . sanitize_url( $background ) . '");';
1912
1913 // Background Position.
1914 $position_x = get_theme_mod( 'background_position_x', get_theme_support( 'custom-background', 'default-position-x' ) );
1915 $position_y = get_theme_mod( 'background_position_y', get_theme_support( 'custom-background', 'default-position-y' ) );
1916
1917 if ( ! in_array( $position_x, array( 'left', 'center', 'right' ), true ) ) {
1918 $position_x = 'left';
1919 }
1920
1921 if ( ! in_array( $position_y, array( 'top', 'center', 'bottom' ), true ) ) {
1922 $position_y = 'top';
1923 }
1924
1925 $position = " background-position: $position_x $position_y;";
1926
1927 // Background Size.
1928 $size = get_theme_mod( 'background_size', get_theme_support( 'custom-background', 'default-size' ) );
1929
1930 if ( ! in_array( $size, array( 'auto', 'contain', 'cover' ), true ) ) {
1931 $size = 'auto';
1932 }
1933
1934 $size = " background-size: $size;";
1935
1936 // Background Repeat.
1937 $repeat = get_theme_mod( 'background_repeat', get_theme_support( 'custom-background', 'default-repeat' ) );
1938
1939 if ( ! in_array( $repeat, array( 'repeat-x', 'repeat-y', 'repeat', 'no-repeat' ), true ) ) {
1940 $repeat = 'repeat';
1941 }
1942
1943 $repeat = " background-repeat: $repeat;";
1944
1945 // Background Scroll.
1946 $attachment = get_theme_mod( 'background_attachment', get_theme_support( 'custom-background', 'default-attachment' ) );
1947
1948 if ( 'fixed' !== $attachment ) {
1949 $attachment = 'scroll';
1950 }
1951
1952 $attachment = " background-attachment: $attachment;";
1953
1954 $style .= $image . $position . $size . $repeat . $attachment;
1955 }
1956 ?>
1957<style<?php echo $type_attr; ?> id="custom-background-css">
1958body.custom-background { <?php echo trim( $style ); ?> }
1959</style>
1960 <?php
1961}
1962
1963/**
1964 * Renders the Custom CSS style element.
1965 *
1966 * @since 4.7.0
1967 */
1968function wp_custom_css_cb() {
1969 $styles = wp_get_custom_css();
1970 if ( $styles || is_customize_preview() ) :
1971 $type_attr = current_theme_supports( 'html5', 'style' ) ? '' : ' type="text/css"';
1972 ?>
1973 <style<?php echo $type_attr; ?> id="wp-custom-css">
1974 <?php
1975 // Note that esc_html() cannot be used because `div > span` is not interpreted properly.
1976 echo strip_tags( $styles );
1977 ?>
1978 </style>
1979 <?php
1980 endif;
1981}
1982
1983/**
1984 * Fetches the `custom_css` post for a given theme.
1985 *
1986 * @since 4.7.0
1987 *
1988 * @param string $stylesheet Optional. A theme object stylesheet name. Defaults to the active theme.
1989 * @return WP_Post|null The custom_css post or null if none exists.
1990 */
1991function wp_get_custom_css_post( $stylesheet = '' ) {
1992 if ( empty( $stylesheet ) ) {
1993 $stylesheet = get_stylesheet();
1994 }
1995
1996 $custom_css_query_vars = array(
1997 'post_type' => 'custom_css',
1998 'post_status' => get_post_stati(),
1999 'name' => sanitize_title( $stylesheet ),
2000 'posts_per_page' => 1,
2001 'no_found_rows' => true,
2002 'cache_results' => true,
2003 'update_post_meta_cache' => false,
2004 'update_post_term_cache' => false,
2005 'lazy_load_term_meta' => false,
2006 );
2007
2008 $post = null;
2009 if ( get_stylesheet() === $stylesheet ) {
2010 $post_id = get_theme_mod( 'custom_css_post_id' );
2011
2012 if ( $post_id > 0 && get_post( $post_id ) ) {
2013 $post = get_post( $post_id );
2014 }
2015
2016 // `-1` indicates no post exists; no query necessary.
2017 if ( ! $post && -1 !== $post_id ) {
2018 $query = new WP_Query( $custom_css_query_vars );
2019 $post = $query->post;
2020 /*
2021 * Cache the lookup. See wp_update_custom_css_post().
2022 * @todo This should get cleared if a custom_css post is added/removed.
2023 */
2024 set_theme_mod( 'custom_css_post_id', $post ? $post->ID : -1 );
2025 }
2026 } else {
2027 $query = new WP_Query( $custom_css_query_vars );
2028 $post = $query->post;
2029 }
2030
2031 return $post;
2032}
2033
2034/**
2035 * Fetches the saved Custom CSS content for rendering.
2036 *
2037 * @since 4.7.0
2038 *
2039 * @param string $stylesheet Optional. A theme object stylesheet name. Defaults to the active theme.
2040 * @return string The Custom CSS Post content.
2041 */
2042function wp_get_custom_css( $stylesheet = '' ) {
2043 $css = '';
2044
2045 if ( empty( $stylesheet ) ) {
2046 $stylesheet = get_stylesheet();
2047 }
2048
2049 $post = wp_get_custom_css_post( $stylesheet );
2050 if ( $post ) {
2051 $css = $post->post_content;
2052 }
2053
2054 /**
2055 * Filters the custom CSS output into the head element.
2056 *
2057 * @since 4.7.0
2058 *
2059 * @param string $css CSS pulled in from the Custom CSS post type.
2060 * @param string $stylesheet The theme stylesheet name.
2061 */
2062 $css = apply_filters( 'wp_get_custom_css', $css, $stylesheet );
2063
2064 return $css;
2065}
2066
2067/**
2068 * Updates the `custom_css` post for a given theme.
2069 *
2070 * Inserts a `custom_css` post when one doesn't yet exist.
2071 *
2072 * @since 4.7.0
2073 *
2074 * @param string $css CSS, stored in `post_content`.
2075 * @param array $args {
2076 * Args.
2077 *
2078 * @type string $preprocessed Optional. Pre-processed CSS, stored in `post_content_filtered`.
2079 * Normally empty string.
2080 * @type string $stylesheet Optional. Stylesheet (child theme) to update.
2081 * Defaults to active theme/stylesheet.
2082 * }
2083 * @return WP_Post|WP_Error Post on success, error on failure.
2084 */
2085function wp_update_custom_css_post( $css, $args = array() ) {
2086 $args = wp_parse_args(
2087 $args,
2088 array(
2089 'preprocessed' => '',
2090 'stylesheet' => get_stylesheet(),
2091 )
2092 );
2093
2094 $data = array(
2095 'css' => $css,
2096 'preprocessed' => $args['preprocessed'],
2097 );
2098
2099 /**
2100 * Filters the `css` (`post_content`) and `preprocessed` (`post_content_filtered`) args
2101 * for a `custom_css` post being updated.
2102 *
2103 * This filter can be used by plugin that offer CSS pre-processors, to store the original
2104 * pre-processed CSS in `post_content_filtered` and then store processed CSS in `post_content`.
2105 * When used in this way, the `post_content_filtered` should be supplied as the setting value
2106 * instead of `post_content` via a the `customize_value_custom_css` filter, for example:
2107 *
2108 * <code>
2109 * add_filter( 'customize_value_custom_css', function( $value, $setting ) {
2110 * $post = wp_get_custom_css_post( $setting->stylesheet );
2111 * if ( $post && ! empty( $post->post_content_filtered ) ) {
2112 * $css = $post->post_content_filtered;
2113 * }
2114 * return $css;
2115 * }, 10, 2 );
2116 * </code>
2117 *
2118 * @since 4.7.0
2119 * @param array $data {
2120 * Custom CSS data.
2121 *
2122 * @type string $css CSS stored in `post_content`.
2123 * @type string $preprocessed Pre-processed CSS stored in `post_content_filtered`.
2124 * Normally empty string.
2125 * }
2126 * @param array $args {
2127 * The args passed into `wp_update_custom_css_post()` merged with defaults.
2128 *
2129 * @type string $css The original CSS passed in to be updated.
2130 * @type string $preprocessed The original preprocessed CSS passed in to be updated.
2131 * @type string $stylesheet The stylesheet (theme) being updated.
2132 * }
2133 */
2134 $data = apply_filters( 'update_custom_css_data', $data, array_merge( $args, compact( 'css' ) ) );
2135
2136 $post_data = array(
2137 'post_title' => $args['stylesheet'],
2138 'post_name' => sanitize_title( $args['stylesheet'] ),
2139 'post_type' => 'custom_css',
2140 'post_status' => 'publish',
2141 'post_content' => $data['css'],
2142 'post_content_filtered' => $data['preprocessed'],
2143 );
2144
2145 // Update post if it already exists, otherwise create a new one.
2146 $post = wp_get_custom_css_post( $args['stylesheet'] );
2147 if ( $post ) {
2148 $post_data['ID'] = $post->ID;
2149 $r = wp_update_post( wp_slash( $post_data ), true );
2150 } else {
2151 $r = wp_insert_post( wp_slash( $post_data ), true );
2152
2153 if ( ! is_wp_error( $r ) ) {
2154 if ( get_stylesheet() === $args['stylesheet'] ) {
2155 set_theme_mod( 'custom_css_post_id', $r );
2156 }
2157
2158 // Trigger creation of a revision. This should be removed once #30854 is resolved.
2159 $revisions = wp_get_latest_revision_id_and_total_count( $r );
2160 if ( ! is_wp_error( $revisions ) && 0 === $revisions['count'] ) {
2161 wp_save_post_revision( $r );
2162 }
2163 }
2164 }
2165
2166 if ( is_wp_error( $r ) ) {
2167 return $r;
2168 }
2169 return get_post( $r );
2170}
2171
2172/**
2173 * Adds callback for custom TinyMCE editor stylesheets.
2174 *
2175 * The parameter $stylesheet is the name of the stylesheet, relative to
2176 * the theme root. It also accepts an array of stylesheets.
2177 * It is optional and defaults to 'editor-style.css'.
2178 *
2179 * This function automatically adds another stylesheet with -rtl prefix, e.g. editor-style-rtl.css.
2180 * If that file doesn't exist, it is removed before adding the stylesheet(s) to TinyMCE.
2181 * If an array of stylesheets is passed to add_editor_style(),
2182 * RTL is only added for the first stylesheet.
2183 *
2184 * Since version 3.4 the TinyMCE body has .rtl CSS class.
2185 * It is a better option to use that class and add any RTL styles to the main stylesheet.
2186 *
2187 * @since 3.0.0
2188 *
2189 * @global array $editor_styles
2190 *
2191 * @param array|string $stylesheet Optional. Stylesheet name or array thereof, relative to theme root.
2192 * Defaults to 'editor-style.css'
2193 */
2194function add_editor_style( $stylesheet = 'editor-style.css' ) {
2195 global $editor_styles;
2196
2197 add_theme_support( 'editor-style' );
2198
2199 $editor_styles = (array) $editor_styles;
2200 $stylesheet = (array) $stylesheet;
2201
2202 if ( is_rtl() ) {
2203 $rtl_stylesheet = str_replace( '.css', '-rtl.css', $stylesheet[0] );
2204 $stylesheet[] = $rtl_stylesheet;
2205 }
2206
2207 $editor_styles = array_merge( $editor_styles, $stylesheet );
2208}
2209
2210/**
2211 * Removes all visual editor stylesheets.
2212 *
2213 * @since 3.1.0
2214 *
2215 * @global array $editor_styles
2216 *
2217 * @return bool True on success, false if there were no stylesheets to remove.
2218 */
2219function remove_editor_styles() {
2220 if ( ! current_theme_supports( 'editor-style' ) ) {
2221 return false;
2222 }
2223 _remove_theme_support( 'editor-style' );
2224 if ( is_admin() ) {
2225 $GLOBALS['editor_styles'] = array();
2226 }
2227 return true;
2228}
2229
2230/**
2231 * Retrieves any registered editor stylesheet URLs.
2232 *
2233 * @since 4.0.0
2234 *
2235 * @global array $editor_styles Registered editor stylesheets
2236 *
2237 * @return string[] If registered, a list of editor stylesheet URLs.
2238 */
2239function get_editor_stylesheets() {
2240 $stylesheets = array();
2241 // Load editor_style.css if the active theme supports it.
2242 if ( ! empty( $GLOBALS['editor_styles'] ) && is_array( $GLOBALS['editor_styles'] ) ) {
2243 $editor_styles = $GLOBALS['editor_styles'];
2244
2245 $editor_styles = array_unique( array_filter( $editor_styles ) );
2246 $style_uri = get_stylesheet_directory_uri();
2247 $style_dir = get_stylesheet_directory();
2248
2249 // Support externally referenced styles (like, say, fonts).
2250 foreach ( $editor_styles as $key => $file ) {
2251 if ( preg_match( '~^(https?:)?//~', $file ) ) {
2252 $stylesheets[] = sanitize_url( $file );
2253 unset( $editor_styles[ $key ] );
2254 }
2255 }
2256
2257 // Look in a parent theme first, that way child theme CSS overrides.
2258 if ( is_child_theme() ) {
2259 $template_uri = get_template_directory_uri();
2260 $template_dir = get_template_directory();
2261
2262 foreach ( $editor_styles as $key => $file ) {
2263 if ( $file && file_exists( "$template_dir/$file" ) ) {
2264 $stylesheets[] = "$template_uri/$file";
2265 }
2266 }
2267 }
2268
2269 foreach ( $editor_styles as $file ) {
2270 if ( $file && file_exists( "$style_dir/$file" ) ) {
2271 $stylesheets[] = "$style_uri/$file";
2272 }
2273 }
2274 }
2275
2276 /**
2277 * Filters the array of URLs of stylesheets applied to the editor.
2278 *
2279 * @since 4.3.0
2280 *
2281 * @param string[] $stylesheets Array of URLs of stylesheets to be applied to the editor.
2282 */
2283 return apply_filters( 'editor_stylesheets', $stylesheets );
2284}
2285
2286/**
2287 * Expands a theme's starter content configuration using core-provided data.
2288 *
2289 * @since 4.7.0
2290 *
2291 * @return array Array of starter content.
2292 */
2293function get_theme_starter_content() {
2294 $theme_support = get_theme_support( 'starter-content' );
2295 if ( is_array( $theme_support ) && ! empty( $theme_support[0] ) && is_array( $theme_support[0] ) ) {
2296 $config = $theme_support[0];
2297 } else {
2298 $config = array();
2299 }
2300
2301 $core_content = array(
2302 'widgets' => array(
2303 'text_business_info' => array(
2304 'text',
2305 array(
2306 'title' => _x( 'Find Us', 'Theme starter content' ),
2307 'text' => implode(
2308 '',
2309 array(
2310 '<strong>' . _x( 'Address', 'Theme starter content' ) . "</strong>\n",
2311 _x( '123 Main Street', 'Theme starter content' ) . "\n",
2312 _x( 'New York, NY 10001', 'Theme starter content' ) . "\n\n",
2313 '<strong>' . _x( 'Hours', 'Theme starter content' ) . "</strong>\n",
2314 _x( 'Monday–Friday: 9:00AM–5:00PM', 'Theme starter content' ) . "\n",
2315 _x( 'Saturday & Sunday: 11:00AM–3:00PM', 'Theme starter content' ),
2316 )
2317 ),
2318 'filter' => true,
2319 'visual' => true,
2320 ),
2321 ),
2322 'text_about' => array(
2323 'text',
2324 array(
2325 'title' => _x( 'About This Site', 'Theme starter content' ),
2326 'text' => _x( 'This may be a good place to introduce yourself and your site or include some credits.', 'Theme starter content' ),
2327 'filter' => true,
2328 'visual' => true,
2329 ),
2330 ),
2331 'archives' => array(
2332 'archives',
2333 array(
2334 'title' => _x( 'Archives', 'Theme starter content' ),
2335 ),
2336 ),
2337 'calendar' => array(
2338 'calendar',
2339 array(
2340 'title' => _x( 'Calendar', 'Theme starter content' ),
2341 ),
2342 ),
2343 'categories' => array(
2344 'categories',
2345 array(
2346 'title' => _x( 'Categories', 'Theme starter content' ),
2347 ),
2348 ),
2349 'meta' => array(
2350 'meta',
2351 array(
2352 'title' => _x( 'Meta', 'Theme starter content' ),
2353 ),
2354 ),
2355 'recent-comments' => array(
2356 'recent-comments',
2357 array(
2358 'title' => _x( 'Recent Comments', 'Theme starter content' ),
2359 ),
2360 ),
2361 'recent-posts' => array(
2362 'recent-posts',
2363 array(
2364 'title' => _x( 'Recent Posts', 'Theme starter content' ),
2365 ),
2366 ),
2367 'search' => array(
2368 'search',
2369 array(
2370 'title' => _x( 'Search', 'Theme starter content' ),
2371 ),
2372 ),
2373 ),
2374 'nav_menus' => array(
2375 'link_home' => array(
2376 'type' => 'custom',
2377 'title' => _x( 'Home', 'Theme starter content' ),
2378 'url' => home_url( '/' ),
2379 ),
2380 'page_home' => array( // Deprecated in favor of 'link_home'.
2381 'type' => 'post_type',
2382 'object' => 'page',
2383 'object_id' => '{{home}}',
2384 ),
2385 'page_about' => array(
2386 'type' => 'post_type',
2387 'object' => 'page',
2388 'object_id' => '{{about}}',
2389 ),
2390 'page_blog' => array(
2391 'type' => 'post_type',
2392 'object' => 'page',
2393 'object_id' => '{{blog}}',
2394 ),
2395 'page_news' => array(
2396 'type' => 'post_type',
2397 'object' => 'page',
2398 'object_id' => '{{news}}',
2399 ),
2400 'page_contact' => array(
2401 'type' => 'post_type',
2402 'object' => 'page',
2403 'object_id' => '{{contact}}',
2404 ),
2405
2406 'link_email' => array(
2407 'title' => _x( 'Email', 'Theme starter content' ),
2408 'url' => 'mailto:wordpress@example.com',
2409 ),
2410 'link_facebook' => array(
2411 'title' => _x( 'Facebook', 'Theme starter content' ),
2412 'url' => 'https://www.facebook.com/wordpress',
2413 ),
2414 'link_foursquare' => array(
2415 'title' => _x( 'Foursquare', 'Theme starter content' ),
2416 'url' => 'https://foursquare.com/',
2417 ),
2418 'link_github' => array(
2419 'title' => _x( 'GitHub', 'Theme starter content' ),
2420 'url' => 'https://github.com/wordpress/',
2421 ),
2422 'link_instagram' => array(
2423 'title' => _x( 'Instagram', 'Theme starter content' ),
2424 'url' => 'https://www.instagram.com/explore/tags/wordcamp/',
2425 ),
2426 'link_linkedin' => array(
2427 'title' => _x( 'LinkedIn', 'Theme starter content' ),
2428 'url' => 'https://www.linkedin.com/company/1089783',
2429 ),
2430 'link_pinterest' => array(
2431 'title' => _x( 'Pinterest', 'Theme starter content' ),
2432 'url' => 'https://www.pinterest.com/',
2433 ),
2434 'link_twitter' => array(
2435 'title' => _x( 'Twitter', 'Theme starter content' ),
2436 'url' => 'https://twitter.com/wordpress',
2437 ),
2438 'link_yelp' => array(
2439 'title' => _x( 'Yelp', 'Theme starter content' ),
2440 'url' => 'https://www.yelp.com',
2441 ),
2442 'link_youtube' => array(
2443 'title' => _x( 'YouTube', 'Theme starter content' ),
2444 'url' => 'https://www.youtube.com/channel/UCdof4Ju7amm1chz1gi1T2ZA',
2445 ),
2446 ),
2447 'posts' => array(
2448 'home' => array(
2449 'post_type' => 'page',
2450 'post_title' => _x( 'Home', 'Theme starter content' ),
2451 'post_content' => sprintf(
2452 "<!-- wp:paragraph -->\n<p>%s</p>\n<!-- /wp:paragraph -->",
2453 _x( 'Welcome to your site! This is your homepage, which is what most visitors will see when they come to your site for the first time.', 'Theme starter content' )
2454 ),
2455 ),
2456 'about' => array(
2457 'post_type' => 'page',
2458 'post_title' => _x( 'About', 'Theme starter content' ),
2459 'post_content' => sprintf(
2460 "<!-- wp:paragraph -->\n<p>%s</p>\n<!-- /wp:paragraph -->",
2461 _x( 'You might be an artist who would like to introduce yourself and your work here or maybe you are a business with a mission to describe.', 'Theme starter content' )
2462 ),
2463 ),
2464 'contact' => array(
2465 'post_type' => 'page',
2466 'post_title' => _x( 'Contact', 'Theme starter content' ),
2467 'post_content' => sprintf(
2468 "<!-- wp:paragraph -->\n<p>%s</p>\n<!-- /wp:paragraph -->",
2469 _x( 'This is a page with some basic contact information, such as an address and phone number. You might also try a plugin to add a contact form.', 'Theme starter content' )
2470 ),
2471 ),
2472 'blog' => array(
2473 'post_type' => 'page',
2474 'post_title' => _x( 'Blog', 'Theme starter content' ),
2475 ),
2476 'news' => array(
2477 'post_type' => 'page',
2478 'post_title' => _x( 'News', 'Theme starter content' ),
2479 ),
2480
2481 'homepage-section' => array(
2482 'post_type' => 'page',
2483 'post_title' => _x( 'A homepage section', 'Theme starter content' ),
2484 'post_content' => sprintf(
2485 "<!-- wp:paragraph -->\n<p>%s</p>\n<!-- /wp:paragraph -->",
2486 _x( 'This is an example of a homepage section. Homepage sections can be any page other than the homepage itself, including the page that shows your latest blog posts.', 'Theme starter content' )
2487 ),
2488 ),
2489 ),
2490 );
2491
2492 $content = array();
2493
2494 foreach ( $config as $type => $args ) {
2495 switch ( $type ) {
2496 // Use options and theme_mods as-is.
2497 case 'options':
2498 case 'theme_mods':
2499 $content[ $type ] = $config[ $type ];
2500 break;
2501
2502 // Widgets are grouped into sidebars.
2503 case 'widgets':
2504 foreach ( $config[ $type ] as $sidebar_id => $widgets ) {
2505 foreach ( $widgets as $id => $widget ) {
2506 if ( is_array( $widget ) ) {
2507
2508 // Item extends core content.
2509 if ( ! empty( $core_content[ $type ][ $id ] ) ) {
2510 $widget = array(
2511 $core_content[ $type ][ $id ][0],
2512 array_merge( $core_content[ $type ][ $id ][1], $widget ),
2513 );
2514 }
2515
2516 $content[ $type ][ $sidebar_id ][] = $widget;
2517 } elseif ( is_string( $widget )
2518 && ! empty( $core_content[ $type ] )
2519 && ! empty( $core_content[ $type ][ $widget ] )
2520 ) {
2521 $content[ $type ][ $sidebar_id ][] = $core_content[ $type ][ $widget ];
2522 }
2523 }
2524 }
2525 break;
2526
2527 // And nav menu items are grouped into nav menus.
2528 case 'nav_menus':
2529 foreach ( $config[ $type ] as $nav_menu_location => $nav_menu ) {
2530
2531 // Ensure nav menus get a name.
2532 if ( empty( $nav_menu['name'] ) ) {
2533 $nav_menu['name'] = $nav_menu_location;
2534 }
2535
2536 $content[ $type ][ $nav_menu_location ]['name'] = $nav_menu['name'];
2537
2538 foreach ( $nav_menu['items'] as $id => $nav_menu_item ) {
2539 if ( is_array( $nav_menu_item ) ) {
2540
2541 // Item extends core content.
2542 if ( ! empty( $core_content[ $type ][ $id ] ) ) {
2543 $nav_menu_item = array_merge( $core_content[ $type ][ $id ], $nav_menu_item );
2544 }
2545
2546 $content[ $type ][ $nav_menu_location ]['items'][] = $nav_menu_item;
2547 } elseif ( is_string( $nav_menu_item )
2548 && ! empty( $core_content[ $type ] )
2549 && ! empty( $core_content[ $type ][ $nav_menu_item ] )
2550 ) {
2551 $content[ $type ][ $nav_menu_location ]['items'][] = $core_content[ $type ][ $nav_menu_item ];
2552 }
2553 }
2554 }
2555 break;
2556
2557 // Attachments are posts but have special treatment.
2558 case 'attachments':
2559 foreach ( $config[ $type ] as $id => $item ) {
2560 if ( ! empty( $item['file'] ) ) {
2561 $content[ $type ][ $id ] = $item;
2562 }
2563 }
2564 break;
2565
2566 /*
2567 * All that's left now are posts (besides attachments).
2568 * Not a default case for the sake of clarity and future work.
2569 */
2570 case 'posts':
2571 foreach ( $config[ $type ] as $id => $item ) {
2572 if ( is_array( $item ) ) {
2573
2574 // Item extends core content.
2575 if ( ! empty( $core_content[ $type ][ $id ] ) ) {
2576 $item = array_merge( $core_content[ $type ][ $id ], $item );
2577 }
2578
2579 // Enforce a subset of fields.
2580 $content[ $type ][ $id ] = wp_array_slice_assoc(
2581 $item,
2582 array(
2583 'post_type',
2584 'post_title',
2585 'post_excerpt',
2586 'post_name',
2587 'post_content',
2588 'menu_order',
2589 'comment_status',
2590 'thumbnail',
2591 'template',
2592 )
2593 );
2594 } elseif ( is_string( $item ) && ! empty( $core_content[ $type ][ $item ] ) ) {
2595 $content[ $type ][ $item ] = $core_content[ $type ][ $item ];
2596 }
2597 }
2598 break;
2599 }
2600 }
2601
2602 /**
2603 * Filters the expanded array of starter content.
2604 *
2605 * @since 4.7.0
2606 *
2607 * @param array $content Array of starter content.
2608 * @param array $config Array of theme-specific starter content configuration.
2609 */
2610 return apply_filters( 'get_theme_starter_content', $content, $config );
2611}
2612
2613/**
2614 * Registers theme support for a given feature.
2615 *
2616 * Must be called in the theme's functions.php file to work.
2617 * If attached to a hook, it must be {@see 'after_setup_theme'}.
2618 * The {@see 'init'} hook may be too late for some features.
2619 *
2620 * Example usage:
2621 *
2622 * add_theme_support( 'title-tag' );
2623 * add_theme_support( 'custom-logo', array(
2624 * 'height' => 480,
2625 * 'width' => 720,
2626 * ) );
2627 *
2628 * @since 2.9.0
2629 * @since 3.4.0 The `custom-header-uploads` feature was deprecated.
2630 * @since 3.6.0 The `html5` feature was added.
2631 * @since 3.6.1 The `html5` feature requires an array of types to be passed. Defaults to
2632 * 'comment-list', 'comment-form', 'search-form' for backward compatibility.
2633 * @since 3.9.0 The `html5` feature now also accepts 'gallery' and 'caption'.
2634 * @since 4.1.0 The `title-tag` feature was added.
2635 * @since 4.5.0 The `customize-selective-refresh-widgets` feature was added.
2636 * @since 4.7.0 The `starter-content` feature was added.
2637 * @since 5.0.0 The `responsive-embeds`, `align-wide`, `dark-editor-style`, `disable-custom-colors`,
2638 * `disable-custom-font-sizes`, `editor-color-palette`, `editor-font-sizes`,
2639 * `editor-styles`, and `wp-block-styles` features were added.
2640 * @since 5.3.0 The `html5` feature now also accepts 'script' and 'style'.
2641 * @since 5.3.0 Formalized the existing and already documented `...$args` parameter
2642 * by adding it to the function signature.
2643 * @since 5.4.0 The `disable-custom-gradients` feature limits to default gradients or gradients added
2644 * through `editor-gradient-presets` theme support.
2645 * @since 5.5.0 The `core-block-patterns` feature was added and is enabled by default.
2646 * @since 5.5.0 The `custom-logo` feature now also accepts 'unlink-homepage-logo'.
2647 * @since 5.6.0 The `post-formats` feature warns if no array is passed as the second parameter.
2648 * @since 5.8.0 The `widgets-block-editor` feature enables the Widgets block editor.
2649 * @since 5.8.0 The `block-templates` feature indicates whether a theme uses block-based templates.
2650 * @since 6.0.0 The `html5` feature warns if no array is passed as the second parameter.
2651 * @since 6.1.0 The `block-template-parts` feature allows to edit any reusable template part from site editor.
2652 * @since 6.1.0 The `disable-layout-styles` feature disables the default layout styles.
2653 * @since 6.3.0 The `link-color` feature allows to enable the link color setting.
2654 * @since 6.3.0 The `border` feature allows themes without theme.json to add border styles to blocks.
2655 * @since 6.5.0 The `appearance-tools` feature enables a few design tools for blocks,
2656 * see `WP_Theme_JSON::APPEARANCE_TOOLS_OPT_INS` for a complete list.
2657 * @since 6.6.0 The `editor-spacing-sizes` feature was added.
2658 *
2659 * @global array $_wp_theme_features
2660 *
2661 * @param string $feature The feature being added. Likely core values include:
2662 * - 'admin-bar'
2663 * - 'align-wide'
2664 * - 'appearance-tools'
2665 * - 'automatic-feed-links'
2666 * - 'block-templates'
2667 * - 'block-template-parts'
2668 * - 'border'
2669 * - 'core-block-patterns'
2670 * - 'custom-background'
2671 * - 'custom-header'
2672 * - 'custom-line-height'
2673 * - 'custom-logo'
2674 * - 'customize-selective-refresh-widgets'
2675 * - 'custom-spacing'
2676 * - 'custom-units'
2677 * - 'dark-editor-style'
2678 * - 'disable-custom-colors'
2679 * - 'disable-custom-font-sizes'
2680 * - 'disable-custom-gradients'
2681 * - 'disable-layout-styles'
2682 * - 'editor-color-palette'
2683 * - 'editor-gradient-presets'
2684 * - 'editor-font-sizes'
2685 * - 'editor-spacing-sizes'
2686 * - 'editor-styles'
2687 * - 'featured-content'
2688 * - 'html5'
2689 * - 'link-color'
2690 * - 'menus'
2691 * - 'post-formats'
2692 * - 'post-thumbnails'
2693 * - 'responsive-embeds'
2694 * - 'starter-content'
2695 * - 'title-tag'
2696 * - 'widgets'
2697 * - 'widgets-block-editor'
2698 * - 'wp-block-styles'
2699 * @param mixed ...$args Optional extra arguments to pass along with certain features.
2700 * @return void|false Void on success, false on failure.
2701 */
2702function add_theme_support( $feature, ...$args ) {
2703 global $_wp_theme_features;
2704
2705 if ( ! $args ) {
2706 $args = true;
2707 }
2708
2709 switch ( $feature ) {
2710 case 'post-thumbnails':
2711 // All post types are already supported.
2712 if ( true === get_theme_support( 'post-thumbnails' ) ) {
2713 return;
2714 }
2715
2716 /*
2717 * Merge post types with any that already declared their support
2718 * for post thumbnails.
2719 */
2720 if ( isset( $args[0] ) && is_array( $args[0] ) && isset( $_wp_theme_features['post-thumbnails'] ) ) {
2721 $args[0] = array_unique( array_merge( $_wp_theme_features['post-thumbnails'][0], $args[0] ) );
2722 }
2723
2724 break;
2725
2726 case 'post-formats':
2727 if ( isset( $args[0] ) && is_array( $args[0] ) ) {
2728 $post_formats = get_post_format_slugs();
2729 unset( $post_formats['standard'] );
2730
2731 $args[0] = array_intersect( $args[0], array_keys( $post_formats ) );
2732 } else {
2733 _doing_it_wrong(
2734 "add_theme_support( 'post-formats' )",
2735 __( 'You need to pass an array of post formats.' ),
2736 '5.6.0'
2737 );
2738 return false;
2739 }
2740 break;
2741
2742 case 'html5':
2743 // You can't just pass 'html5', you need to pass an array of types.
2744 if ( empty( $args[0] ) || ! is_array( $args[0] ) ) {
2745 _doing_it_wrong(
2746 "add_theme_support( 'html5' )",
2747 __( 'You need to pass an array of types.' ),
2748 '3.6.1'
2749 );
2750
2751 if ( ! empty( $args[0] ) && ! is_array( $args[0] ) ) {
2752 return false;
2753 }
2754
2755 // Build an array of types for back-compat.
2756 $args = array( 0 => array( 'comment-list', 'comment-form', 'search-form' ) );
2757 }
2758
2759 // Calling 'html5' again merges, rather than overwrites.
2760 if ( isset( $_wp_theme_features['html5'] ) ) {
2761 $args[0] = array_merge( $_wp_theme_features['html5'][0], $args[0] );
2762 }
2763 break;
2764
2765 case 'custom-logo':
2766 if ( true === $args ) {
2767 $args = array( 0 => array() );
2768 }
2769 $defaults = array(
2770 'width' => null,
2771 'height' => null,
2772 'flex-width' => false,
2773 'flex-height' => false,
2774 'header-text' => '',
2775 'unlink-homepage-logo' => false,
2776 );
2777 $args[0] = wp_parse_args( array_intersect_key( $args[0], $defaults ), $defaults );
2778
2779 // Allow full flexibility if no size is specified.
2780 if ( is_null( $args[0]['width'] ) && is_null( $args[0]['height'] ) ) {
2781 $args[0]['flex-width'] = true;
2782 $args[0]['flex-height'] = true;
2783 }
2784 break;
2785
2786 case 'custom-header-uploads':
2787 return add_theme_support( 'custom-header', array( 'uploads' => true ) );
2788
2789 case 'custom-header':
2790 if ( true === $args ) {
2791 $args = array( 0 => array() );
2792 }
2793
2794 $defaults = array(
2795 'default-image' => '',
2796 'random-default' => false,
2797 'width' => 0,
2798 'height' => 0,
2799 'flex-height' => false,
2800 'flex-width' => false,
2801 'default-text-color' => '',
2802 'header-text' => true,
2803 'uploads' => true,
2804 'wp-head-callback' => '',
2805 'admin-head-callback' => '',
2806 'admin-preview-callback' => '',
2807 'video' => false,
2808 'video-active-callback' => 'is_front_page',
2809 );
2810
2811 $jit = isset( $args[0]['__jit'] );
2812 unset( $args[0]['__jit'] );
2813
2814 /*
2815 * Merge in data from previous add_theme_support() calls.
2816 * The first value registered wins. (A child theme is set up first.)
2817 */
2818 if ( isset( $_wp_theme_features['custom-header'] ) ) {
2819 $args[0] = wp_parse_args( $_wp_theme_features['custom-header'][0], $args[0] );
2820 }
2821
2822 /*
2823 * Load in the defaults at the end, as we need to insure first one wins.
2824 * This will cause all constants to be defined, as each arg will then be set to the default.
2825 */
2826 if ( $jit ) {
2827 $args[0] = wp_parse_args( $args[0], $defaults );
2828 }
2829
2830 /*
2831 * If a constant was defined, use that value. Otherwise, define the constant to ensure
2832 * the constant is always accurate (and is not defined later, overriding our value).
2833 * As stated above, the first value wins.
2834 * Once we get to wp_loaded (just-in-time), define any constants we haven't already.
2835 * Constants should be avoided. Don't reference them. This is just for backward compatibility.
2836 */
2837
2838 if ( defined( 'NO_HEADER_TEXT' ) ) {
2839 $args[0]['header-text'] = ! NO_HEADER_TEXT;
2840 } elseif ( isset( $args[0]['header-text'] ) ) {
2841 define( 'NO_HEADER_TEXT', empty( $args[0]['header-text'] ) );
2842 }
2843
2844 if ( defined( 'HEADER_IMAGE_WIDTH' ) ) {
2845 $args[0]['width'] = (int) HEADER_IMAGE_WIDTH;
2846 } elseif ( isset( $args[0]['width'] ) ) {
2847 define( 'HEADER_IMAGE_WIDTH', (int) $args[0]['width'] );
2848 }
2849
2850 if ( defined( 'HEADER_IMAGE_HEIGHT' ) ) {
2851 $args[0]['height'] = (int) HEADER_IMAGE_HEIGHT;
2852 } elseif ( isset( $args[0]['height'] ) ) {
2853 define( 'HEADER_IMAGE_HEIGHT', (int) $args[0]['height'] );
2854 }
2855
2856 if ( defined( 'HEADER_TEXTCOLOR' ) ) {
2857 $args[0]['default-text-color'] = HEADER_TEXTCOLOR;
2858 } elseif ( isset( $args[0]['default-text-color'] ) ) {
2859 define( 'HEADER_TEXTCOLOR', $args[0]['default-text-color'] );
2860 }
2861
2862 if ( defined( 'HEADER_IMAGE' ) ) {
2863 $args[0]['default-image'] = HEADER_IMAGE;
2864 } elseif ( isset( $args[0]['default-image'] ) ) {
2865 define( 'HEADER_IMAGE', $args[0]['default-image'] );
2866 }
2867
2868 if ( $jit && ! empty( $args[0]['default-image'] ) ) {
2869 $args[0]['random-default'] = false;
2870 }
2871
2872 /*
2873 * If headers are supported, and we still don't have a defined width or height,
2874 * we have implicit flex sizes.
2875 */
2876 if ( $jit ) {
2877 if ( empty( $args[0]['width'] ) && empty( $args[0]['flex-width'] ) ) {
2878 $args[0]['flex-width'] = true;
2879 }
2880 if ( empty( $args[0]['height'] ) && empty( $args[0]['flex-height'] ) ) {
2881 $args[0]['flex-height'] = true;
2882 }
2883 }
2884
2885 break;
2886
2887 case 'custom-background':
2888 if ( true === $args ) {
2889 $args = array( 0 => array() );
2890 }
2891
2892 $defaults = array(
2893 'default-image' => '',
2894 'default-preset' => 'default',
2895 'default-position-x' => 'left',
2896 'default-position-y' => 'top',
2897 'default-size' => 'auto',
2898 'default-repeat' => 'repeat',
2899 'default-attachment' => 'scroll',
2900 'default-color' => '',
2901 'wp-head-callback' => '_custom_background_cb',
2902 'admin-head-callback' => '',
2903 'admin-preview-callback' => '',
2904 );
2905
2906 $jit = isset( $args[0]['__jit'] );
2907 unset( $args[0]['__jit'] );
2908
2909 // Merge in data from previous add_theme_support() calls. The first value registered wins.
2910 if ( isset( $_wp_theme_features['custom-background'] ) ) {
2911 $args[0] = wp_parse_args( $_wp_theme_features['custom-background'][0], $args[0] );
2912 }
2913
2914 if ( $jit ) {
2915 $args[0] = wp_parse_args( $args[0], $defaults );
2916 }
2917
2918 if ( defined( 'BACKGROUND_COLOR' ) ) {
2919 $args[0]['default-color'] = BACKGROUND_COLOR;
2920 } elseif ( isset( $args[0]['default-color'] ) || $jit ) {
2921 define( 'BACKGROUND_COLOR', $args[0]['default-color'] );
2922 }
2923
2924 if ( defined( 'BACKGROUND_IMAGE' ) ) {
2925 $args[0]['default-image'] = BACKGROUND_IMAGE;
2926 } elseif ( isset( $args[0]['default-image'] ) || $jit ) {
2927 define( 'BACKGROUND_IMAGE', $args[0]['default-image'] );
2928 }
2929
2930 break;
2931
2932 // Ensure that 'title-tag' is accessible in the admin.
2933 case 'title-tag':
2934 // Can be called in functions.php but must happen before wp_loaded, i.e. not in header.php.
2935 if ( did_action( 'wp_loaded' ) ) {
2936 _doing_it_wrong(
2937 "add_theme_support( 'title-tag' )",
2938 sprintf(
2939 /* translators: 1: title-tag, 2: wp_loaded */
2940 __( 'Theme support for %1$s should be registered before the %2$s hook.' ),
2941 '<code>title-tag</code>',
2942 '<code>wp_loaded</code>'
2943 ),
2944 '4.1.0'
2945 );
2946
2947 return false;
2948 }
2949 }
2950
2951 $_wp_theme_features[ $feature ] = $args;
2952}
2953
2954/**
2955 * Registers the internal custom header and background routines.
2956 *
2957 * @since 3.4.0
2958 * @access private
2959 *
2960 * @global Custom_Image_Header $custom_image_header
2961 * @global Custom_Background $custom_background
2962 */
2963function _custom_header_background_just_in_time() {
2964 global $custom_image_header, $custom_background;
2965
2966 if ( current_theme_supports( 'custom-header' ) ) {
2967 // In case any constants were defined after an add_custom_image_header() call, re-run.
2968 add_theme_support( 'custom-header', array( '__jit' => true ) );
2969
2970 $args = get_theme_support( 'custom-header' );
2971 if ( $args[0]['wp-head-callback'] ) {
2972 add_action( 'wp_head', $args[0]['wp-head-callback'] );
2973 }
2974
2975 if ( is_admin() ) {
2976 require_once ABSPATH . 'wp-admin/includes/class-custom-image-header.php';
2977 $custom_image_header = new Custom_Image_Header( $args[0]['admin-head-callback'], $args[0]['admin-preview-callback'] );
2978 }
2979 }
2980
2981 if ( current_theme_supports( 'custom-background' ) ) {
2982 // In case any constants were defined after an add_custom_background() call, re-run.
2983 add_theme_support( 'custom-background', array( '__jit' => true ) );
2984
2985 $args = get_theme_support( 'custom-background' );
2986 add_action( 'wp_head', $args[0]['wp-head-callback'] );
2987
2988 if ( is_admin() ) {
2989 require_once ABSPATH . 'wp-admin/includes/class-custom-background.php';
2990 $custom_background = new Custom_Background( $args[0]['admin-head-callback'], $args[0]['admin-preview-callback'] );
2991 }
2992 }
2993}
2994
2995/**
2996 * Adds CSS to hide header text for custom logo, based on Customizer setting.
2997 *
2998 * @since 4.5.0
2999 * @access private
3000 */
3001function _custom_logo_header_styles() {
3002 if ( ! current_theme_supports( 'custom-header', 'header-text' )
3003 && get_theme_support( 'custom-logo', 'header-text' )
3004 && ! get_theme_mod( 'header_text', true )
3005 ) {
3006 $classes = (array) get_theme_support( 'custom-logo', 'header-text' );
3007 $classes = array_map( 'sanitize_html_class', $classes );
3008 $classes = '.' . implode( ', .', $classes );
3009
3010 $type_attr = current_theme_supports( 'html5', 'style' ) ? '' : ' type="text/css"';
3011 ?>
3012 <!-- Custom Logo: hide header text -->
3013 <style id="custom-logo-css"<?php echo $type_attr; ?>>
3014 <?php echo $classes; ?> {
3015 position: absolute;
3016 clip-path: inset(50%);
3017 }
3018 </style>
3019 <?php
3020 }
3021}
3022
3023/**
3024 * Gets the theme support arguments passed when registering that support.
3025 *
3026 * Example usage:
3027 *
3028 * get_theme_support( 'custom-logo' );
3029 * get_theme_support( 'custom-header', 'width' );
3030 *
3031 * @since 3.1.0
3032 * @since 5.3.0 Formalized the existing and already documented `...$args` parameter
3033 * by adding it to the function signature.
3034 *
3035 * @global array $_wp_theme_features
3036 *
3037 * @param string $feature The feature to check. See add_theme_support() for the list
3038 * of possible values.
3039 * @param mixed ...$args Optional extra arguments to be checked against certain features.
3040 * @return mixed The array of extra arguments or the value for the registered feature.
3041 */
3042function get_theme_support( $feature, ...$args ) {
3043 global $_wp_theme_features;
3044
3045 if ( ! isset( $_wp_theme_features[ $feature ] ) ) {
3046 return false;
3047 }
3048
3049 if ( ! $args ) {
3050 return $_wp_theme_features[ $feature ];
3051 }
3052
3053 switch ( $feature ) {
3054 case 'custom-logo':
3055 case 'custom-header':
3056 case 'custom-background':
3057 if ( isset( $_wp_theme_features[ $feature ][0][ $args[0] ] ) ) {
3058 return $_wp_theme_features[ $feature ][0][ $args[0] ];
3059 }
3060 return false;
3061
3062 default:
3063 return $_wp_theme_features[ $feature ];
3064 }
3065}
3066
3067/**
3068 * Allows a theme to de-register its support of a certain feature
3069 *
3070 * Should be called in the theme's functions.php file. Generally would
3071 * be used for child themes to override support from the parent theme.
3072 *
3073 * @since 3.0.0
3074 *
3075 * @see add_theme_support()
3076 *
3077 * @param string $feature The feature being removed. See add_theme_support() for the list
3078 * of possible values.
3079 * @return bool|void Whether feature was removed.
3080 */
3081function remove_theme_support( $feature ) {
3082 // Do not remove internal registrations that are not used directly by themes.
3083 if ( in_array( $feature, array( 'editor-style', 'widgets', 'menus' ), true ) ) {
3084 return false;
3085 }
3086
3087 return _remove_theme_support( $feature );
3088}
3089
3090/**
3091 * Do not use. Removes theme support internally without knowledge of those not used
3092 * by themes directly.
3093 *
3094 * @access private
3095 * @since 3.1.0
3096 * @global array $_wp_theme_features
3097 * @global Custom_Image_Header $custom_image_header
3098 * @global Custom_Background $custom_background
3099 *
3100 * @param string $feature The feature being removed. See add_theme_support() for the list
3101 * of possible values.
3102 * @return bool True if support was removed, false if the feature was not registered.
3103 */
3104function _remove_theme_support( $feature ) {
3105 global $_wp_theme_features;
3106
3107 switch ( $feature ) {
3108 case 'custom-header-uploads':
3109 if ( ! isset( $_wp_theme_features['custom-header'] ) ) {
3110 return false;
3111 }
3112 add_theme_support( 'custom-header', array( 'uploads' => false ) );
3113 return; // Do not continue - custom-header-uploads no longer exists.
3114 }
3115
3116 if ( ! isset( $_wp_theme_features[ $feature ] ) ) {
3117 return false;
3118 }
3119
3120 switch ( $feature ) {
3121 case 'custom-header':
3122 if ( ! did_action( 'wp_loaded' ) ) {
3123 break;
3124 }
3125 $support = get_theme_support( 'custom-header' );
3126 if ( isset( $support[0]['wp-head-callback'] ) ) {
3127 remove_action( 'wp_head', $support[0]['wp-head-callback'] );
3128 }
3129 if ( isset( $GLOBALS['custom_image_header'] ) ) {
3130 remove_action( 'admin_menu', array( $GLOBALS['custom_image_header'], 'init' ) );
3131 unset( $GLOBALS['custom_image_header'] );
3132 }
3133 break;
3134
3135 case 'custom-background':
3136 if ( ! did_action( 'wp_loaded' ) ) {
3137 break;
3138 }
3139 $support = get_theme_support( 'custom-background' );
3140 if ( isset( $support[0]['wp-head-callback'] ) ) {
3141 remove_action( 'wp_head', $support[0]['wp-head-callback'] );
3142 }
3143 remove_action( 'admin_menu', array( $GLOBALS['custom_background'], 'init' ) );
3144 unset( $GLOBALS['custom_background'] );
3145 break;
3146 }
3147
3148 unset( $_wp_theme_features[ $feature ] );
3149
3150 return true;
3151}
3152
3153/**
3154 * Checks a theme's support for a given feature.
3155 *
3156 * Example usage:
3157 *
3158 * current_theme_supports( 'custom-logo' );
3159 * current_theme_supports( 'html5', 'comment-form' );
3160 *
3161 * @since 2.9.0
3162 * @since 5.3.0 Formalized the existing and already documented `...$args` parameter
3163 * by adding it to the function signature.
3164 *
3165 * @global array $_wp_theme_features
3166 *
3167 * @param string $feature The feature being checked. See add_theme_support() for the list
3168 * of possible values.
3169 * @param mixed ...$args Optional extra arguments to be checked against certain features.
3170 * @return bool True if the active theme supports the feature, false otherwise.
3171 */
3172function current_theme_supports( $feature, ...$args ) {
3173 global $_wp_theme_features;
3174
3175 if ( 'custom-header-uploads' === $feature ) {
3176 return current_theme_supports( 'custom-header', 'uploads' );
3177 }
3178
3179 if ( ! isset( $_wp_theme_features[ $feature ] ) ) {
3180 return false;
3181 }
3182
3183 // If no args passed then no extra checks need to be performed.
3184 if ( ! $args ) {
3185 /** This filter is documented in wp-includes/theme.php */
3186 return apply_filters( "current_theme_supports-{$feature}", true, $args, $_wp_theme_features[ $feature ] ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
3187 }
3188
3189 switch ( $feature ) {
3190 case 'post-thumbnails':
3191 /*
3192 * post-thumbnails can be registered for only certain content/post types
3193 * by passing an array of types to add_theme_support().
3194 * If no array was passed, then any type is accepted.
3195 */
3196 if ( true === $_wp_theme_features[ $feature ] ) { // Registered for all types.
3197 return true;
3198 }
3199 $content_type = $args[0];
3200 return in_array( $content_type, $_wp_theme_features[ $feature ][0], true );
3201
3202 case 'html5':
3203 case 'post-formats':
3204 /*
3205 * Specific post formats can be registered by passing an array of types
3206 * to add_theme_support().
3207 *
3208 * Specific areas of HTML5 support *must* be passed via an array to add_theme_support().
3209 */
3210 $type = $args[0];
3211 return in_array( $type, $_wp_theme_features[ $feature ][0], true );
3212
3213 case 'custom-logo':
3214 case 'custom-header':
3215 case 'custom-background':
3216 // Specific capabilities can be registered by passing an array to add_theme_support().
3217 return ( isset( $_wp_theme_features[ $feature ][0][ $args[0] ] ) && $_wp_theme_features[ $feature ][0][ $args[0] ] );
3218 }
3219
3220 /**
3221 * Filters whether the active theme supports a specific feature.
3222 *
3223 * The dynamic portion of the hook name, `$feature`, refers to the specific
3224 * theme feature. See add_theme_support() for the list of possible values.
3225 *
3226 * @since 3.4.0
3227 *
3228 * @param bool $supports Whether the active theme supports the given feature. Default true.
3229 * @param array $args Array of arguments for the feature.
3230 * @param string $feature The theme feature.
3231 */
3232 return apply_filters( "current_theme_supports-{$feature}", true, $args, $_wp_theme_features[ $feature ] ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
3233}
3234
3235/**
3236 * Checks a theme's support for a given feature before loading the functions which implement it.
3237 *
3238 * @since 2.9.0
3239 *
3240 * @param string $feature The feature being checked. See add_theme_support() for the list
3241 * of possible values.
3242 * @param string $file Path to the file.
3243 * @return bool True if the active theme supports the supplied feature, false otherwise.
3244 */
3245function require_if_theme_supports( $feature, $file ) {
3246 if ( current_theme_supports( $feature ) ) {
3247 require $file;
3248 return true;
3249 }
3250 return false;
3251}
3252
3253/**
3254 * Registers a theme feature for use in add_theme_support().
3255 *
3256 * This does not indicate that the active theme supports the feature, it only describes
3257 * the feature's supported options.
3258 *
3259 * @since 5.5.0
3260 *
3261 * @see add_theme_support()
3262 *
3263 * @global array $_wp_registered_theme_features
3264 *
3265 * @param string $feature The name uniquely identifying the feature. See add_theme_support()
3266 * for the list of possible values.
3267 * @param array $args {
3268 * Data used to describe the theme.
3269 *
3270 * @type string $type The type of data associated with this feature.
3271 * Valid values are 'string', 'boolean', 'integer',
3272 * 'number', 'array', and 'object'. Defaults to 'boolean'.
3273 * @type bool $variadic Does this feature utilize the variadic support
3274 * of add_theme_support(), or are all arguments specified
3275 * as the second parameter. Must be used with the "array" type.
3276 * @type string $description A short description of the feature. Included in
3277 * the Themes REST API schema. Intended for developers.
3278 * @type bool|array $show_in_rest {
3279 * Whether this feature should be included in the Themes REST API endpoint.
3280 * Defaults to not being included. When registering an 'array' or 'object' type,
3281 * this argument must be an array with the 'schema' key.
3282 *
3283 * @type array $schema Specifies the JSON Schema definition describing
3284 * the feature. If any objects in the schema do not include
3285 * the 'additionalProperties' keyword, it is set to false.
3286 * @type string $name An alternate name to be used as the property name
3287 * in the REST API.
3288 * @type callable $prepare_callback A function used to format the theme support in the REST API.
3289 * Receives the raw theme support value.
3290 * }
3291 * }
3292 * @return true|WP_Error True if the theme feature was successfully registered, a WP_Error object if not.
3293 */
3294function register_theme_feature( $feature, $args = array() ) {
3295 global $_wp_registered_theme_features;
3296
3297 if ( ! is_array( $_wp_registered_theme_features ) ) {
3298 $_wp_registered_theme_features = array();
3299 }
3300
3301 $defaults = array(
3302 'type' => 'boolean',
3303 'variadic' => false,
3304 'description' => '',
3305 'show_in_rest' => false,
3306 );
3307
3308 $args = wp_parse_args( $args, $defaults );
3309
3310 if ( true === $args['show_in_rest'] ) {
3311 $args['show_in_rest'] = array();
3312 }
3313
3314 if ( is_array( $args['show_in_rest'] ) ) {
3315 $args['show_in_rest'] = wp_parse_args(
3316 $args['show_in_rest'],
3317 array(
3318 'schema' => array(),
3319 'name' => $feature,
3320 'prepare_callback' => null,
3321 )
3322 );
3323 }
3324
3325 if ( ! in_array( $args['type'], array( 'string', 'boolean', 'integer', 'number', 'array', 'object' ), true ) ) {
3326 return new WP_Error(
3327 'invalid_type',
3328 __( 'The feature "type" is not valid JSON Schema type.' )
3329 );
3330 }
3331
3332 if ( true === $args['variadic'] && 'array' !== $args['type'] ) {
3333 return new WP_Error(
3334 'variadic_must_be_array',
3335 __( 'When registering a "variadic" theme feature, the "type" must be an "array".' )
3336 );
3337 }
3338
3339 if ( false !== $args['show_in_rest'] && in_array( $args['type'], array( 'array', 'object' ), true ) ) {
3340 if ( ! is_array( $args['show_in_rest'] ) || empty( $args['show_in_rest']['schema'] ) ) {
3341 return new WP_Error(
3342 'missing_schema',
3343 __( 'When registering an "array" or "object" feature to show in the REST API, the feature\'s schema must also be defined.' )
3344 );
3345 }
3346
3347 if ( 'array' === $args['type'] && ! isset( $args['show_in_rest']['schema']['items'] ) ) {
3348 return new WP_Error(
3349 'missing_schema_items',
3350 __( 'When registering an "array" feature, the feature\'s schema must include the "items" keyword.' )
3351 );
3352 }
3353
3354 if ( 'object' === $args['type'] && ! isset( $args['show_in_rest']['schema']['properties'] ) ) {
3355 return new WP_Error(
3356 'missing_schema_properties',
3357 __( 'When registering an "object" feature, the feature\'s schema must include the "properties" keyword.' )
3358 );
3359 }
3360 }
3361
3362 if ( is_array( $args['show_in_rest'] ) ) {
3363 if ( isset( $args['show_in_rest']['prepare_callback'] )
3364 && ! is_callable( $args['show_in_rest']['prepare_callback'] )
3365 ) {
3366 return new WP_Error(
3367 'invalid_rest_prepare_callback',
3368 sprintf(
3369 /* translators: %s: prepare_callback */
3370 __( 'The "%s" must be a callable function.' ),
3371 'prepare_callback'
3372 )
3373 );
3374 }
3375
3376 $args['show_in_rest']['schema'] = wp_parse_args(
3377 $args['show_in_rest']['schema'],
3378 array(
3379 'description' => $args['description'],
3380 'type' => $args['type'],
3381 'default' => false,
3382 )
3383 );
3384
3385 if ( is_bool( $args['show_in_rest']['schema']['default'] )
3386 && ! in_array( 'boolean', (array) $args['show_in_rest']['schema']['type'], true )
3387 ) {
3388 // Automatically include the "boolean" type when the default value is a boolean.
3389 $args['show_in_rest']['schema']['type'] = (array) $args['show_in_rest']['schema']['type'];
3390 array_unshift( $args['show_in_rest']['schema']['type'], 'boolean' );
3391 }
3392
3393 $args['show_in_rest']['schema'] = rest_default_additional_properties_to_false( $args['show_in_rest']['schema'] );
3394 }
3395
3396 $_wp_registered_theme_features[ $feature ] = $args;
3397
3398 return true;
3399}
3400
3401/**
3402 * Gets the list of registered theme features.
3403 *
3404 * @since 5.5.0
3405 *
3406 * @global array $_wp_registered_theme_features
3407 *
3408 * @return array[] List of theme features, keyed by their name.
3409 */
3410function get_registered_theme_features() {
3411 global $_wp_registered_theme_features;
3412
3413 if ( ! is_array( $_wp_registered_theme_features ) ) {
3414 return array();
3415 }
3416
3417 return $_wp_registered_theme_features;
3418}
3419
3420/**
3421 * Gets the registration config for a theme feature.
3422 *
3423 * @since 5.5.0
3424 *
3425 * @global array $_wp_registered_theme_features
3426 *
3427 * @param string $feature The feature name. See add_theme_support() for the list
3428 * of possible values.
3429 * @return array|null The registration args, or null if the feature was not registered.
3430 */
3431function get_registered_theme_feature( $feature ) {
3432 global $_wp_registered_theme_features;
3433
3434 if ( ! is_array( $_wp_registered_theme_features ) ) {
3435 return null;
3436 }
3437
3438 return isset( $_wp_registered_theme_features[ $feature ] ) ? $_wp_registered_theme_features[ $feature ] : null;
3439}
3440
3441/**
3442 * Checks an attachment being deleted to see if it's a header or background image.
3443 *
3444 * If true it removes the theme modification which would be pointing at the deleted
3445 * attachment.
3446 *
3447 * @access private
3448 * @since 3.0.0
3449 * @since 4.3.0 Also removes `header_image_data`.
3450 * @since 4.5.0 Also removes custom logo theme mods.
3451 * @since 6.6.0 Also removes `site_logo` option set by the site logo block.
3452 *
3453 * @param int $id The attachment ID.
3454 */
3455function _delete_attachment_theme_mod( $id ) {
3456 $attachment_image = wp_get_attachment_url( $id );
3457 $header_image = get_header_image();
3458 $background_image = get_background_image();
3459 $custom_logo_id = (int) get_theme_mod( 'custom_logo' );
3460 $site_logo_id = (int) get_option( 'site_logo' );
3461
3462 if ( $custom_logo_id && $custom_logo_id === $id ) {
3463 remove_theme_mod( 'custom_logo' );
3464 remove_theme_mod( 'header_text' );
3465 }
3466
3467 if ( $site_logo_id && $site_logo_id === $id ) {
3468 delete_option( 'site_logo' );
3469 }
3470
3471 if ( $header_image && $header_image === $attachment_image ) {
3472 remove_theme_mod( 'header_image' );
3473 remove_theme_mod( 'header_image_data' );
3474 }
3475
3476 if ( $background_image && $background_image === $attachment_image ) {
3477 remove_theme_mod( 'background_image' );
3478 }
3479}
3480
3481/**
3482 * Checks if a theme has been changed and runs 'after_switch_theme' hook on the next WP load.
3483 *
3484 * See {@see 'after_switch_theme'}.
3485 *
3486 * @since 3.3.0
3487 */
3488function check_theme_switched() {
3489 $stylesheet = get_option( 'theme_switched' );
3490
3491 if ( $stylesheet ) {
3492 $old_theme = wp_get_theme( $stylesheet );
3493
3494 // Prevent widget & menu mapping from running since Customizer already called it up front.
3495 if ( get_option( 'theme_switched_via_customizer' ) ) {
3496 remove_action( 'after_switch_theme', '_wp_menus_changed' );
3497 remove_action( 'after_switch_theme', '_wp_sidebars_changed' );
3498 update_option( 'theme_switched_via_customizer', false );
3499 }
3500
3501 if ( $old_theme->exists() ) {
3502 /**
3503 * Fires on the next WP load after the theme has been switched.
3504 *
3505 * The parameters differ according to whether the old theme exists or not.
3506 * If the old theme is missing, the old name will instead be the slug
3507 * of the old theme.
3508 *
3509 * See {@see 'switch_theme'}.
3510 *
3511 * @since 3.3.0
3512 *
3513 * @param string $old_name Old theme name.
3514 * @param WP_Theme $old_theme WP_Theme instance of the old theme.
3515 */
3516 do_action( 'after_switch_theme', $old_theme->get( 'Name' ), $old_theme );
3517 } else {
3518 /** This action is documented in wp-includes/theme.php */
3519 do_action( 'after_switch_theme', $stylesheet, $old_theme );
3520 }
3521
3522 flush_rewrite_rules();
3523
3524 update_option( 'theme_switched', false );
3525 }
3526}
3527
3528/**
3529 * Includes and instantiates the WP_Customize_Manager class.
3530 *
3531 * Loads the Customizer at plugins_loaded when accessing the customize.php admin
3532 * page or when any request includes a wp_customize=on param or a customize_changeset
3533 * param (a UUID). This param is a signal for whether to bootstrap the Customizer when
3534 * WordPress is loading, especially in the Customizer preview
3535 * or when making Customizer Ajax requests for widgets or menus.
3536 *
3537 * @since 3.4.0
3538 *
3539 * @global WP_Customize_Manager $wp_customize
3540 */
3541function _wp_customize_include() {
3542
3543 $is_customize_admin_page = ( is_admin() && 'customize.php' === basename( $_SERVER['PHP_SELF'] ) );
3544 $should_include = (
3545 $is_customize_admin_page
3546 ||
3547 ( isset( $_REQUEST['wp_customize'] ) && 'on' === $_REQUEST['wp_customize'] )
3548 ||
3549 ( ! empty( $_GET['customize_changeset_uuid'] ) || ! empty( $_POST['customize_changeset_uuid'] ) )
3550 );
3551
3552 if ( ! $should_include ) {
3553 return;
3554 }
3555
3556 /*
3557 * Note that wp_unslash() is not being used on the input vars because it is
3558 * called before wp_magic_quotes() gets called. Besides this fact, none of
3559 * the values should contain any characters needing slashes anyway.
3560 */
3561 $keys = array(
3562 'changeset_uuid',
3563 'customize_changeset_uuid',
3564 'customize_theme',
3565 'theme',
3566 'customize_messenger_channel',
3567 'customize_autosaved',
3568 );
3569 $input_vars = array_merge(
3570 wp_array_slice_assoc( $_GET, $keys ),
3571 wp_array_slice_assoc( $_POST, $keys )
3572 );
3573
3574 $theme = null;
3575 $autosaved = null;
3576 $messenger_channel = null;
3577
3578 /*
3579 * Value false indicates UUID should be determined after_setup_theme
3580 * to either re-use existing saved changeset or else generate a new UUID if none exists.
3581 */
3582 $changeset_uuid = false;
3583
3584 /*
3585 * Set initially to false since defaults to true for back-compat;
3586 * can be overridden via the customize_changeset_branching filter.
3587 */
3588 $branching = false;
3589
3590 if ( $is_customize_admin_page && isset( $input_vars['changeset_uuid'] ) ) {
3591 $changeset_uuid = sanitize_key( $input_vars['changeset_uuid'] );
3592 } elseif ( ! empty( $input_vars['customize_changeset_uuid'] ) ) {
3593 $changeset_uuid = sanitize_key( $input_vars['customize_changeset_uuid'] );
3594 }
3595
3596 // Note that theme will be sanitized via WP_Theme.
3597 if ( $is_customize_admin_page && isset( $input_vars['theme'] ) ) {
3598 $theme = $input_vars['theme'];
3599 } elseif ( isset( $input_vars['customize_theme'] ) ) {
3600 $theme = $input_vars['customize_theme'];
3601 }
3602
3603 if ( ! empty( $input_vars['customize_autosaved'] ) ) {
3604 $autosaved = true;
3605 }
3606
3607 if ( isset( $input_vars['customize_messenger_channel'] ) ) {
3608 $messenger_channel = sanitize_key( $input_vars['customize_messenger_channel'] );
3609 }
3610
3611 /*
3612 * Note that settings must be previewed even outside the customizer preview
3613 * and also in the customizer pane itself. This is to enable loading an existing
3614 * changeset into the customizer. Previewing the settings only has to be prevented
3615 * here in the case of a customize_save action because this will cause WP to think
3616 * there is nothing changed that needs to be saved.
3617 */
3618 $is_customize_save_action = (
3619 wp_doing_ajax()
3620 &&
3621 isset( $_REQUEST['action'] )
3622 &&
3623 'customize_save' === wp_unslash( $_REQUEST['action'] )
3624 );
3625 $settings_previewed = ! $is_customize_save_action;
3626
3627 require_once ABSPATH . WPINC . '/class-wp-customize-manager.php';
3628 $GLOBALS['wp_customize'] = new WP_Customize_Manager(
3629 compact(
3630 'changeset_uuid',
3631 'theme',
3632 'messenger_channel',
3633 'settings_previewed',
3634 'autosaved',
3635 'branching'
3636 )
3637 );
3638}
3639
3640/**
3641 * Publishes a snapshot's changes.
3642 *
3643 * @since 4.7.0
3644 * @access private
3645 *
3646 * @global WP_Customize_Manager $wp_customize Customizer instance.
3647 *
3648 * @param string $new_status New post status.
3649 * @param string $old_status Old post status.
3650 * @param WP_Post $changeset_post Changeset post object.
3651 */
3652function _wp_customize_publish_changeset( $new_status, $old_status, $changeset_post ) {
3653 global $wp_customize;
3654
3655 $is_publishing_changeset = (
3656 'customize_changeset' === $changeset_post->post_type
3657 &&
3658 'publish' === $new_status
3659 &&
3660 'publish' !== $old_status
3661 );
3662 if ( ! $is_publishing_changeset ) {
3663 return;
3664 }
3665
3666 if ( empty( $wp_customize ) ) {
3667 require_once ABSPATH . WPINC . '/class-wp-customize-manager.php';
3668 $wp_customize = new WP_Customize_Manager(
3669 array(
3670 'changeset_uuid' => $changeset_post->post_name,
3671 'settings_previewed' => false,
3672 )
3673 );
3674 }
3675
3676 if ( ! did_action( 'customize_register' ) ) {
3677 /*
3678 * When running from CLI or Cron, the customize_register action will need
3679 * to be triggered in order for core, themes, and plugins to register their
3680 * settings. Normally core will add_action( 'customize_register' ) at
3681 * priority 10 to register the core settings, and if any themes/plugins
3682 * also add_action( 'customize_register' ) at the same priority, they
3683 * will have a $wp_customize with those settings registered since they
3684 * call add_action() afterward, normally. However, when manually doing
3685 * the customize_register action after the setup_theme, then the order
3686 * will be reversed for two actions added at priority 10, resulting in
3687 * the core settings no longer being available as expected to themes/plugins.
3688 * So the following manually calls the method that registers the core
3689 * settings up front before doing the action.
3690 */
3691 remove_action( 'customize_register', array( $wp_customize, 'register_controls' ) );
3692 $wp_customize->register_controls();
3693
3694 /** This filter is documented in wp-includes/class-wp-customize-manager.php */
3695 do_action( 'customize_register', $wp_customize );
3696 }
3697 $wp_customize->_publish_changeset_values( $changeset_post->ID );
3698
3699 /*
3700 * Trash the changeset post if revisions are not enabled. Unpublished
3701 * changesets by default get garbage collected due to the auto-draft status.
3702 * When a changeset post is published, however, it would no longer get cleaned
3703 * out. This is a problem when the changeset posts are never displayed anywhere,
3704 * since they would just be endlessly piling up. So here we use the revisions
3705 * feature to indicate whether or not a published changeset should get trashed
3706 * and thus garbage collected.
3707 */
3708 if ( ! wp_revisions_enabled( $changeset_post ) ) {
3709 $wp_customize->trash_changeset_post( $changeset_post->ID );
3710 }
3711}
3712
3713/**
3714 * Filters changeset post data upon insert to ensure post_name is intact.
3715 *
3716 * This is needed to prevent the post_name from being dropped when the post is
3717 * transitioned into pending status by a contributor.
3718 *
3719 * @since 4.7.0
3720 *
3721 * @see wp_insert_post()
3722 *
3723 * @param array $post_data An array of slashed post data.
3724 * @param array $supplied_post_data An array of sanitized, but otherwise unmodified post data.
3725 * @return array Filtered data.
3726 */
3727function _wp_customize_changeset_filter_insert_post_data( $post_data, $supplied_post_data ) {
3728 if ( isset( $post_data['post_type'] ) && 'customize_changeset' === $post_data['post_type'] ) {
3729
3730 // Prevent post_name from being dropped, such as when contributor saves a changeset post as pending.
3731 if ( empty( $post_data['post_name'] ) && ! empty( $supplied_post_data['post_name'] ) ) {
3732 $post_data['post_name'] = $supplied_post_data['post_name'];
3733 }
3734 }
3735 return $post_data;
3736}
3737
3738/**
3739 * Adds settings for the customize-loader script.
3740 *
3741 * @since 3.4.0
3742 */
3743function _wp_customize_loader_settings() {
3744 $admin_origin = parse_url( admin_url() );
3745 $home_origin = parse_url( home_url() );
3746 $cross_domain = ( strtolower( $admin_origin['host'] ) !== strtolower( $home_origin['host'] ) );
3747
3748 $browser = array(
3749 'mobile' => wp_is_mobile(),
3750 'ios' => wp_is_mobile() && preg_match( '/iPad|iPod|iPhone/', $_SERVER['HTTP_USER_AGENT'] ),
3751 );
3752
3753 $settings = array(
3754 'url' => esc_url( admin_url( 'customize.php' ) ),
3755 'isCrossDomain' => $cross_domain,
3756 'browser' => $browser,
3757 'l10n' => array(
3758 'saveAlert' => __( 'The changes you made will be lost if you navigate away from this page.' ),
3759 'mainIframeTitle' => __( 'Customizer' ),
3760 ),
3761 );
3762
3763 $script = 'var _wpCustomizeLoaderSettings = ' . wp_json_encode( $settings, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ) . ';';
3764
3765 $wp_scripts = wp_scripts();
3766 $data = $wp_scripts->get_data( 'customize-loader', 'data' );
3767 if ( $data ) {
3768 $script = "$data\n$script";
3769 }
3770
3771 $wp_scripts->add_data( 'customize-loader', 'data', $script );
3772}
3773
3774/**
3775 * Returns a URL to load the Customizer.
3776 *
3777 * @since 3.4.0
3778 *
3779 * @param string $stylesheet Optional. Theme to customize. Defaults to active theme.
3780 * The theme's stylesheet will be urlencoded if necessary.
3781 * @return string
3782 */
3783function wp_customize_url( $stylesheet = '' ) {
3784 $url = admin_url( 'customize.php' );
3785 if ( $stylesheet ) {
3786 $url = add_query_arg( 'theme', urlencode( $stylesheet ), $url );
3787 }
3788 return esc_url( $url );
3789}
3790
3791/**
3792 * Prints a script to check whether or not the Customizer is supported,
3793 * and apply either the no-customize-support or customize-support class
3794 * to the body.
3795 *
3796 * This function MUST be called inside the body tag.
3797 *
3798 * Ideally, call this function immediately after the body tag is opened.
3799 * This prevents a flash of unstyled content.
3800 *
3801 * It is also recommended that you add the "no-customize-support" class
3802 * to the body tag by default.
3803 *
3804 * @since 3.4.0
3805 * @since 4.7.0 Support for IE8 and below is explicitly removed via conditional comments.
3806 * @since 5.5.0 IE8 and older are no longer supported.
3807 */
3808function wp_customize_support_script() {
3809 $admin_origin = parse_url( admin_url() );
3810 $home_origin = parse_url( home_url() );
3811 $cross_domain = ( strtolower( $admin_origin['host'] ) !== strtolower( $home_origin['host'] ) );
3812 ob_start();
3813 ?>
3814 <script>
3815 (function() {
3816 var request, b = document.body, c = 'className', cs = 'customize-support', rcs = new RegExp('(^|\\s+)(no-)?'+cs+'(\\s+|$)');
3817
3818 <?php if ( $cross_domain ) : ?>
3819 request = (function(){ var xhr = new XMLHttpRequest(); return ('withCredentials' in xhr); })();
3820 <?php else : ?>
3821 request = true;
3822 <?php endif; ?>
3823
3824 b[c] = b[c].replace( rcs, ' ' );
3825 // The customizer requires postMessage and CORS (if the site is cross domain).
3826 b[c] += ( window.postMessage && request ? ' ' : ' no-' ) + cs;
3827 }());
3828 </script>
3829 <?php
3830 wp_print_inline_script_tag( wp_remove_surrounding_empty_script_tags( ob_get_clean() ) . "\n//# sourceURL=" . rawurlencode( __FUNCTION__ ) );
3831}
3832
3833/**
3834 * Whether the site is being previewed in the Customizer.
3835 *
3836 * @since 4.0.0
3837 *
3838 * @global WP_Customize_Manager $wp_customize Customizer instance.
3839 *
3840 * @return bool True if the site is being previewed in the Customizer, false otherwise.
3841 */
3842function is_customize_preview() {
3843 global $wp_customize;
3844
3845 return ( $wp_customize instanceof WP_Customize_Manager ) && $wp_customize->is_preview();
3846}
3847
3848/**
3849 * Makes sure that auto-draft posts get their post_date bumped or status changed
3850 * to draft to prevent premature garbage-collection.
3851 *
3852 * When a changeset is updated but remains an auto-draft, ensure the post_date
3853 * for the auto-draft posts remains the same so that it will be
3854 * garbage-collected at the same time by `wp_delete_auto_drafts()`. Otherwise,
3855 * if the changeset is updated to be a draft then update the posts
3856 * to have a far-future post_date so that they will never be garbage collected
3857 * unless the changeset post itself is deleted.
3858 *
3859 * When a changeset is updated to be a persistent draft or to be scheduled for
3860 * publishing, then transition any dependent auto-drafts to a draft status so
3861 * that they likewise will not be garbage-collected but also so that they can
3862 * be edited in the admin before publishing since there is not yet a post/page
3863 * editing flow in the Customizer. See #39752.
3864 *
3865 * @link https://core.trac.wordpress.org/ticket/39752
3866 *
3867 * @since 4.8.0
3868 * @access private
3869 * @see wp_delete_auto_drafts()
3870 *
3871 * @global wpdb $wpdb WordPress database abstraction object.
3872 *
3873 * @param string $new_status Transition to this post status.
3874 * @param string $old_status Previous post status.
3875 * @param \WP_Post $post Post data.
3876 */
3877function _wp_keep_alive_customize_changeset_dependent_auto_drafts( $new_status, $old_status, $post ) {
3878 global $wpdb;
3879 unset( $old_status );
3880
3881 // Short-circuit if not a changeset or if the changeset was published.
3882 if ( 'customize_changeset' !== $post->post_type || 'publish' === $new_status ) {
3883 return;
3884 }
3885
3886 $data = json_decode( $post->post_content, true );
3887 if ( empty( $data['nav_menus_created_posts']['value'] ) ) {
3888 return;
3889 }
3890
3891 /*
3892 * Actually, in lieu of keeping alive, trash any customization drafts here if the changeset itself is
3893 * getting trashed. This is needed because when a changeset transitions to a draft, then any of the
3894 * dependent auto-draft post/page stubs will also get transitioned to customization drafts which
3895 * are then visible in the WP Admin. We cannot wait for the deletion of the changeset in which
3896 * _wp_delete_customize_changeset_dependent_auto_drafts() will be called, since they need to be
3897 * trashed to remove from visibility immediately.
3898 */
3899 if ( 'trash' === $new_status ) {
3900 foreach ( $data['nav_menus_created_posts']['value'] as $post_id ) {
3901 if ( ! empty( $post_id ) && 'draft' === get_post_status( $post_id ) ) {
3902 wp_trash_post( $post_id );
3903 }
3904 }
3905 return;
3906 }
3907
3908 $post_args = array();
3909 if ( 'auto-draft' === $new_status ) {
3910 /*
3911 * Keep the post date for the post matching the changeset
3912 * so that it will not be garbage-collected before the changeset.
3913 */
3914 $post_args['post_date'] = $post->post_date; // Note wp_delete_auto_drafts() only looks at this date.
3915 } else {
3916 /*
3917 * Since the changeset no longer has an auto-draft (and it is not published)
3918 * it is now a persistent changeset, a long-lived draft, and so any
3919 * associated auto-draft posts should likewise transition into having a draft
3920 * status. These drafts will be treated differently than regular drafts in
3921 * that they will be tied to the given changeset. The publish meta box is
3922 * replaced with a notice about how the post is part of a set of customized changes
3923 * which will be published when the changeset is published.
3924 */
3925 $post_args['post_status'] = 'draft';
3926 }
3927
3928 foreach ( $data['nav_menus_created_posts']['value'] as $post_id ) {
3929 if ( empty( $post_id ) || 'auto-draft' !== get_post_status( $post_id ) ) {
3930 continue;
3931 }
3932 $wpdb->update(
3933 $wpdb->posts,
3934 $post_args,
3935 array( 'ID' => $post_id )
3936 );
3937 clean_post_cache( $post_id );
3938 }
3939}
3940
3941/**
3942 * Creates the initial theme features when the 'setup_theme' action is fired.
3943 *
3944 * See {@see 'setup_theme'}.
3945 *
3946 * @since 5.5.0
3947 * @since 6.0.1 The `block-templates` feature was added.
3948 */
3949function create_initial_theme_features() {
3950 register_theme_feature(
3951 'align-wide',
3952 array(
3953 'description' => __( 'Whether theme opts in to wide alignment CSS class.' ),
3954 'show_in_rest' => true,
3955 )
3956 );
3957 register_theme_feature(
3958 'automatic-feed-links',
3959 array(
3960 'description' => __( 'Whether posts and comments RSS feed links are added to head.' ),
3961 'show_in_rest' => true,
3962 )
3963 );
3964 register_theme_feature(
3965 'block-templates',
3966 array(
3967 'description' => __( 'Whether a theme uses block-based templates.' ),
3968 'show_in_rest' => true,
3969 )
3970 );
3971 register_theme_feature(
3972 'block-template-parts',
3973 array(
3974 'description' => __( 'Whether a theme uses block-based template parts.' ),
3975 'show_in_rest' => true,
3976 )
3977 );
3978 register_theme_feature(
3979 'custom-background',
3980 array(
3981 'description' => __( 'Custom background if defined by the theme.' ),
3982 'type' => 'object',
3983 'show_in_rest' => array(
3984 'schema' => array(
3985 'properties' => array(
3986 'default-image' => array(
3987 'type' => 'string',
3988 'format' => 'uri',
3989 ),
3990 'default-preset' => array(
3991 'type' => 'string',
3992 'enum' => array(
3993 'default',
3994 'fill',
3995 'fit',
3996 'repeat',
3997 'custom',
3998 ),
3999 ),
4000 'default-position-x' => array(
4001 'type' => 'string',
4002 'enum' => array(
4003 'left',
4004 'center',
4005 'right',
4006 ),
4007 ),
4008 'default-position-y' => array(
4009 'type' => 'string',
4010 'enum' => array(
4011 'left',
4012 'center',
4013 'right',
4014 ),
4015 ),
4016 'default-size' => array(
4017 'type' => 'string',
4018 'enum' => array(
4019 'auto',
4020 'contain',
4021 'cover',
4022 ),
4023 ),
4024 'default-repeat' => array(
4025 'type' => 'string',
4026 'enum' => array(
4027 'repeat-x',
4028 'repeat-y',
4029 'repeat',
4030 'no-repeat',
4031 ),
4032 ),
4033 'default-attachment' => array(
4034 'type' => 'string',
4035 'enum' => array(
4036 'scroll',
4037 'fixed',
4038 ),
4039 ),
4040 'default-color' => array(
4041 'type' => 'string',
4042 ),
4043 ),
4044 ),
4045 ),
4046 )
4047 );
4048 register_theme_feature(
4049 'custom-header',
4050 array(
4051 'description' => __( 'Custom header if defined by the theme.' ),
4052 'type' => 'object',
4053 'show_in_rest' => array(
4054 'schema' => array(
4055 'properties' => array(
4056 'default-image' => array(
4057 'type' => 'string',
4058 'format' => 'uri',
4059 ),
4060 'random-default' => array(
4061 'type' => 'boolean',
4062 ),
4063 'width' => array(
4064 'type' => 'integer',
4065 ),
4066 'height' => array(
4067 'type' => 'integer',
4068 ),
4069 'flex-height' => array(
4070 'type' => 'boolean',
4071 ),
4072 'flex-width' => array(
4073 'type' => 'boolean',
4074 ),
4075 'default-text-color' => array(
4076 'type' => 'string',
4077 ),
4078 'header-text' => array(
4079 'type' => 'boolean',
4080 ),
4081 'uploads' => array(
4082 'type' => 'boolean',
4083 ),
4084 'video' => array(
4085 'type' => 'boolean',
4086 ),
4087 ),
4088 ),
4089 ),
4090 )
4091 );
4092 register_theme_feature(
4093 'custom-logo',
4094 array(
4095 'type' => 'object',
4096 'description' => __( 'Custom logo if defined by the theme.' ),
4097 'show_in_rest' => array(
4098 'schema' => array(
4099 'properties' => array(
4100 'width' => array(
4101 'type' => 'integer',
4102 ),
4103 'height' => array(
4104 'type' => 'integer',
4105 ),
4106 'flex-width' => array(
4107 'type' => 'boolean',
4108 ),
4109 'flex-height' => array(
4110 'type' => 'boolean',
4111 ),
4112 'header-text' => array(
4113 'type' => 'array',
4114 'items' => array(
4115 'type' => 'string',
4116 ),
4117 ),
4118 'unlink-homepage-logo' => array(
4119 'type' => 'boolean',
4120 ),
4121 ),
4122 ),
4123 ),
4124 )
4125 );
4126 register_theme_feature(
4127 'customize-selective-refresh-widgets',
4128 array(
4129 'description' => __( 'Whether the theme enables Selective Refresh for Widgets being managed with the Customizer.' ),
4130 'show_in_rest' => true,
4131 )
4132 );
4133 register_theme_feature(
4134 'dark-editor-style',
4135 array(
4136 'description' => __( 'Whether theme opts in to the dark editor style UI.' ),
4137 'show_in_rest' => true,
4138 )
4139 );
4140 register_theme_feature(
4141 'disable-custom-colors',
4142 array(
4143 'description' => __( 'Whether the theme disables custom colors.' ),
4144 'show_in_rest' => true,
4145 )
4146 );
4147 register_theme_feature(
4148 'disable-custom-font-sizes',
4149 array(
4150 'description' => __( 'Whether the theme disables custom font sizes.' ),
4151 'show_in_rest' => true,
4152 )
4153 );
4154 register_theme_feature(
4155 'disable-custom-gradients',
4156 array(
4157 'description' => __( 'Whether the theme disables custom gradients.' ),
4158 'show_in_rest' => true,
4159 )
4160 );
4161 register_theme_feature(
4162 'disable-layout-styles',
4163 array(
4164 'description' => __( 'Whether the theme disables generated layout styles.' ),
4165 'show_in_rest' => true,
4166 )
4167 );
4168 register_theme_feature(
4169 'editor-color-palette',
4170 array(
4171 'type' => 'array',
4172 'description' => __( 'Custom color palette if defined by the theme.' ),
4173 'show_in_rest' => array(
4174 'schema' => array(
4175 'items' => array(
4176 'type' => 'object',
4177 'properties' => array(
4178 'name' => array(
4179 'type' => 'string',
4180 ),
4181 'slug' => array(
4182 'type' => 'string',
4183 ),
4184 'color' => array(
4185 'type' => 'string',
4186 ),
4187 ),
4188 ),
4189 ),
4190 ),
4191 )
4192 );
4193 register_theme_feature(
4194 'editor-font-sizes',
4195 array(
4196 'type' => 'array',
4197 'description' => __( 'Custom font sizes if defined by the theme.' ),
4198 'show_in_rest' => array(
4199 'schema' => array(
4200 'items' => array(
4201 'type' => 'object',
4202 'properties' => array(
4203 'name' => array(
4204 'type' => 'string',
4205 ),
4206 'size' => array(
4207 'type' => 'number',
4208 ),
4209 'slug' => array(
4210 'type' => 'string',
4211 ),
4212 ),
4213 ),
4214 ),
4215 ),
4216 )
4217 );
4218 register_theme_feature(
4219 'editor-gradient-presets',
4220 array(
4221 'type' => 'array',
4222 'description' => __( 'Custom gradient presets if defined by the theme.' ),
4223 'show_in_rest' => array(
4224 'schema' => array(
4225 'items' => array(
4226 'type' => 'object',
4227 'properties' => array(
4228 'name' => array(
4229 'type' => 'string',
4230 ),
4231 'gradient' => array(
4232 'type' => 'string',
4233 ),
4234 'slug' => array(
4235 'type' => 'string',
4236 ),
4237 ),
4238 ),
4239 ),
4240 ),
4241 )
4242 );
4243 register_theme_feature(
4244 'editor-spacing-sizes',
4245 array(
4246 'type' => 'array',
4247 'description' => __( 'Custom spacing sizes if defined by the theme.' ),
4248 'show_in_rest' => array(
4249 'schema' => array(
4250 'items' => array(
4251 'type' => 'object',
4252 'properties' => array(
4253 'name' => array(
4254 'type' => 'string',
4255 ),
4256 'size' => array(
4257 'type' => 'string',
4258 ),
4259 'slug' => array(
4260 'type' => 'string',
4261 ),
4262 ),
4263 ),
4264 ),
4265 ),
4266 )
4267 );
4268 register_theme_feature(
4269 'editor-styles',
4270 array(
4271 'description' => __( 'Whether theme opts in to the editor styles CSS wrapper.' ),
4272 'show_in_rest' => true,
4273 )
4274 );
4275 register_theme_feature(
4276 'html5',
4277 array(
4278 'type' => 'array',
4279 'description' => __( 'Allows use of HTML5 markup for search forms, comment forms, comment lists, gallery, and caption.' ),
4280 'show_in_rest' => array(
4281 'schema' => array(
4282 'items' => array(
4283 'type' => 'string',
4284 'enum' => array(
4285 'search-form',
4286 'comment-form',
4287 'comment-list',
4288 'gallery',
4289 'caption',
4290 'script',
4291 'style',
4292 ),
4293 ),
4294 ),
4295 ),
4296 )
4297 );
4298 register_theme_feature(
4299 'post-formats',
4300 array(
4301 'type' => 'array',
4302 'description' => __( 'Post formats supported.' ),
4303 'show_in_rest' => array(
4304 'name' => 'formats',
4305 'schema' => array(
4306 'items' => array(
4307 'type' => 'string',
4308 'enum' => get_post_format_slugs(),
4309 ),
4310 'default' => array( 'standard' ),
4311 ),
4312 'prepare_callback' => static function ( $formats ) {
4313 $formats = is_array( $formats ) ? array_values( $formats[0] ) : array();
4314 $formats = array_merge( array( 'standard' ), $formats );
4315
4316 return $formats;
4317 },
4318 ),
4319 )
4320 );
4321 register_theme_feature(
4322 'post-thumbnails',
4323 array(
4324 'type' => 'array',
4325 'description' => __( 'The post types that support thumbnails or true if all post types are supported.' ),
4326 'show_in_rest' => array(
4327 'type' => array( 'boolean', 'array' ),
4328 'schema' => array(
4329 'items' => array(
4330 'type' => 'string',
4331 ),
4332 ),
4333 ),
4334 )
4335 );
4336 register_theme_feature(
4337 'responsive-embeds',
4338 array(
4339 'description' => __( 'Whether the theme supports responsive embedded content.' ),
4340 'show_in_rest' => true,
4341 )
4342 );
4343 register_theme_feature(
4344 'title-tag',
4345 array(
4346 'description' => __( 'Whether the theme can manage the document title tag.' ),
4347 'show_in_rest' => true,
4348 )
4349 );
4350 register_theme_feature(
4351 'wp-block-styles',
4352 array(
4353 'description' => __( 'Whether theme opts in to default WordPress block styles for viewing.' ),
4354 'show_in_rest' => true,
4355 )
4356 );
4357}
4358
4359/**
4360 * Returns whether the active theme is a block-based theme or not.
4361 *
4362 * @since 5.9.0
4363 *
4364 * @global string[] $wp_theme_directories
4365 *
4366 * @return bool Whether the active theme is a block-based theme or not.
4367 */
4368function wp_is_block_theme() {
4369 if ( empty( $GLOBALS['wp_theme_directories'] ) ) {
4370 _doing_it_wrong( __FUNCTION__, __( 'This function should not be called before the theme directory is registered.' ), '6.8.0' );
4371 return false;
4372 }
4373
4374 return wp_get_theme()->is_block_theme();
4375}
4376
4377/**
4378 * Given an element name, returns a class name.
4379 *
4380 * Alias of WP_Theme_JSON::get_element_class_name.
4381 *
4382 * @since 6.1.0
4383 *
4384 * @param string $element The name of the element.
4385 *
4386 * @return string The name of the class.
4387 */
4388function wp_theme_get_element_class_name( $element ) {
4389 return WP_Theme_JSON::get_element_class_name( $element );
4390}
4391
4392/**
4393 * Adds default theme supports for block themes when the 'after_setup_theme' action fires.
4394 *
4395 * See {@see 'after_setup_theme'}.
4396 *
4397 * @since 5.9.0
4398 * @access private
4399 */
4400function _add_default_theme_supports() {
4401 if ( ! wp_is_block_theme() ) {
4402 return;
4403 }
4404
4405 add_theme_support( 'post-thumbnails' );
4406 add_theme_support( 'responsive-embeds' );
4407 add_theme_support( 'editor-styles' );
4408 /*
4409 * Makes block themes support HTML5 by default for the comment block and search form
4410 * (which use default template functions) and `[caption]` and `[gallery]` shortcodes.
4411 * Other blocks contain their own HTML5 markup.
4412 */
4413 add_theme_support( 'html5', array( 'comment-form', 'comment-list', 'search-form', 'gallery', 'caption', 'style', 'script' ) );
4414 add_theme_support( 'automatic-feed-links' );
4415
4416 add_filter( 'should_load_separate_core_block_assets', '__return_true' );
4417 add_filter( 'should_load_block_assets_on_demand', '__return_true' );
4418
4419 /*
4420 * Remove the Customizer's Menus panel when block theme is active.
4421 */
4422 add_filter(
4423 'customize_panel_active',
4424 static function ( $active, WP_Customize_Panel $panel ) {
4425 if (
4426 'nav_menus' === $panel->id &&
4427 ! current_theme_supports( 'menus' ) &&
4428 ! current_theme_supports( 'widgets' )
4429 ) {
4430 $active = false;
4431 }
4432 return $active;
4433 },
4434 10,
4435 2
4436 );
4437}
4438