// Start with directories in the root of the current theme directory.
$dirs = @ scandir( $theme_root );
trigger_error( "$theme_root is not readable", E_USER_NOTICE );
foreach ( $dirs as $dir ) {
if ( ! is_dir( $theme_root . '/' . $dir ) || '.' === $dir[0] || 'CVS' === $dir ) {
if ( file_exists( $theme_root . '/' . $dir . '/style.css' ) ) {
// wp-content/themes/a-single-theme
// wp-content/themes is $theme_root, a-single-theme is $dir.
$found_themes[ $dir ] = array(
'theme_file' => $dir . '/style.css',
'theme_root' => $theme_root,
// wp-content/themes/a-folder-of-themes/*
// wp-content/themes is $theme_root, a-folder-of-themes is $dir, then themes are $sub_dirs.
$sub_dirs = @ scandir( $theme_root . '/' . $dir );
trigger_error( "$theme_root/$dir is not readable", E_USER_NOTICE );
foreach ( $sub_dirs as $sub_dir ) {
if ( ! is_dir( $theme_root . '/' . $dir . '/' . $sub_dir ) || '.' === $dir[0] || 'CVS' === $dir ) {
if ( ! file_exists( $theme_root . '/' . $dir . '/' . $sub_dir . '/style.css' ) ) {
$found_themes[ $dir . '/' . $sub_dir ] = array(
'theme_file' => $dir . '/' . $sub_dir . '/style.css',
'theme_root' => $theme_root,
// Never mind the above, it's just a theme missing a style.css.
// Return it; WP_Theme will catch the error.
$found_themes[ $dir ] = array(
'theme_file' => $dir . '/style.css',
'theme_root' => $theme_root,
$relative_theme_roots = array_flip( $relative_theme_roots );
foreach ( $found_themes as $theme_dir => $theme_data ) {
$theme_roots[ $theme_dir ] = $relative_theme_roots[ $theme_data['theme_root'] ]; // Convert absolute to relative.
if ( get_site_transient( 'theme_roots' ) != $theme_roots ) {
set_site_transient( 'theme_roots', $theme_roots, $cache_expiration );
* Retrieves path to themes directory.
* Does not have trailing slash.
* @global array $wp_theme_directories
* @param string $stylesheet_or_template Optional. The stylesheet or template name of the theme.
* Default is to leverage the main theme root.
* @return string Themes directory path.
function get_theme_root( $stylesheet_or_template = '' ) {
global $wp_theme_directories;
if ( $stylesheet_or_template ) {
$theme_root = get_raw_theme_root( $stylesheet_or_template );
// Always prepend WP_CONTENT_DIR unless the root currently registered as a theme directory.
// This gives relative theme roots the benefit of the doubt when things go haywire.
if ( ! in_array( $theme_root, (array) $wp_theme_directories, true ) ) {
$theme_root = WP_CONTENT_DIR . $theme_root;
$theme_root = WP_CONTENT_DIR . '/themes';
* Filters the absolute path to the themes directory.
* @param string $theme_root Absolute path to themes directory.
return apply_filters( 'theme_root', $theme_root );
* Retrieves URI for themes directory.
* Does not have trailing slash.
* @global array $wp_theme_directories
* @param string $stylesheet_or_template Optional. The stylesheet or template name of the theme.
* Default is to leverage the main theme root.
* @param string $theme_root Optional. The theme root for which calculations will be based,
* preventing the need for a get_raw_theme_root() call. Default empty.
* @return string Themes directory URI.
function get_theme_root_uri( $stylesheet_or_template = '', $theme_root = '' ) {
global $wp_theme_directories;
if ( $stylesheet_or_template && ! $theme_root ) {
$theme_root = get_raw_theme_root( $stylesheet_or_template );
if ( $stylesheet_or_template && $theme_root ) {
if ( in_array( $theme_root, (array) $wp_theme_directories, true ) ) {
// Absolute path. Make an educated guess. YMMV -- but note the filter below.
if ( 0 === strpos( $theme_root, WP_CONTENT_DIR ) ) {
$theme_root_uri = content_url( str_replace( WP_CONTENT_DIR, '', $theme_root ) );
} elseif ( 0 === strpos( $theme_root, ABSPATH ) ) {
$theme_root_uri = site_url( str_replace( ABSPATH, '', $theme_root ) );
} elseif ( 0 === strpos( $theme_root, WP_PLUGIN_DIR ) || 0 === strpos( $theme_root, WPMU_PLUGIN_DIR ) ) {
$theme_root_uri = plugins_url( basename( $theme_root ), $theme_root );
$theme_root_uri = $theme_root;
$theme_root_uri = content_url( $theme_root );
$theme_root_uri = content_url( 'themes' );
* Filters the URI for themes directory.
* @param string $theme_root_uri The URI for themes directory.
* @param string $siteurl WordPress web address which is set in General Options.
* @param string $stylesheet_or_template The stylesheet or template name of the theme.
return apply_filters( 'theme_root_uri', $theme_root_uri, get_option( 'siteurl' ), $stylesheet_or_template );
* Gets the raw theme root relative to the content directory with no filters applied.
* @global array $wp_theme_directories
* @param string $stylesheet_or_template The stylesheet or template name of the theme.
* @param bool $skip_cache Optional. Whether to skip the cache.
* Defaults to false, meaning the cache is used.
* @return string Theme root.
function get_raw_theme_root( $stylesheet_or_template, $skip_cache = false ) {
global $wp_theme_directories;
if ( ! is_array( $wp_theme_directories ) || count( $wp_theme_directories ) <= 1 ) {
// If requesting the root for the current theme, consult options to avoid calling get_theme_roots().
if ( get_option( 'stylesheet' ) == $stylesheet_or_template ) {
$theme_root = get_option( 'stylesheet_root' );
} elseif ( get_option( 'template' ) == $stylesheet_or_template ) {
$theme_root = get_option( 'template_root' );
if ( empty( $theme_root ) ) {
$theme_roots = get_theme_roots();
if ( ! empty( $theme_roots[ $stylesheet_or_template ] ) ) {
$theme_root = $theme_roots[ $stylesheet_or_template ];
* Displays localized stylesheet link element.
function locale_stylesheet() {
$stylesheet = get_locale_stylesheet_uri();
if ( empty( $stylesheet ) ) {
$type_attr = current_theme_supports( 'html5', 'style' ) ? '' : ' type="text/css"';
'<link rel="stylesheet" href="%s"%s media="screen" />',
* Accepts one argument: $stylesheet of the theme. It also accepts an additional function signature
* of two arguments: $template then $stylesheet. This is for backward compatibility.
* @global array $wp_theme_directories
* @global WP_Customize_Manager $wp_customize
* @global array $sidebars_widgets
* @param string $stylesheet Stylesheet name.
function switch_theme( $stylesheet ) {
global $wp_theme_directories, $wp_customize, $sidebars_widgets;
$requirements = validate_theme_requirements( $stylesheet );
if ( is_wp_error( $requirements ) ) {
$_sidebars_widgets = null;
if ( 'wp_ajax_customize_save' === current_action() ) {
$old_sidebars_widgets_data_setting = $wp_customize->get_setting( 'old_sidebars_widgets_data' );
if ( $old_sidebars_widgets_data_setting ) {
$_sidebars_widgets = $wp_customize->post_value( $old_sidebars_widgets_data_setting );
} elseif ( is_array( $sidebars_widgets ) ) {
$_sidebars_widgets = $sidebars_widgets;
if ( is_array( $_sidebars_widgets ) ) {
'data' => $_sidebars_widgets,
$nav_menu_locations = get_theme_mod( 'nav_menu_locations' );
update_option( 'theme_switch_menu_locations', $nav_menu_locations );
if ( func_num_args() > 1 ) {
$stylesheet = func_get_arg( 1 );
$old_theme = wp_get_theme();
$new_theme = wp_get_theme( $stylesheet );
$template = $new_theme->get_template();
if ( wp_is_recovery_mode() ) {
$paused_themes = wp_paused_themes();
$paused_themes->delete( $old_theme->get_stylesheet() );
$paused_themes->delete( $old_theme->get_template() );
update_option( 'template', $template );
update_option( 'stylesheet', $stylesheet );
if ( count( $wp_theme_directories ) > 1 ) {
update_option( 'template_root', get_raw_theme_root( $template, true ) );
update_option( 'stylesheet_root', get_raw_theme_root( $stylesheet, true ) );
delete_option( 'template_root' );
delete_option( 'stylesheet_root' );
$new_name = $new_theme->get( 'Name' );
update_option( 'current_theme', $new_name );
// Migrate from the old mods_{name} option to theme_mods_{slug}.
if ( is_admin() && false === get_option( 'theme_mods_' . $stylesheet ) ) {
$default_theme_mods = (array) get_option( 'mods_' . $new_name );
if ( ! empty( $nav_menu_locations ) && empty( $default_theme_mods['nav_menu_locations'] ) ) {
$default_theme_mods['nav_menu_locations'] = $nav_menu_locations;
add_option( "theme_mods_$stylesheet", $default_theme_mods );
* Since retrieve_widgets() is called when initializing a theme in the Customizer,
* we need to remove the theme mods to avoid overwriting changes made via
* the Customizer when accessing wp-admin/widgets.php.
if ( 'wp_ajax_customize_save' === current_action() ) {
remove_theme_mod( 'sidebars_widgets' );
update_option( 'theme_switched', $old_theme->get_stylesheet() );
* Fires after the theme is switched.
* @since 4.5.0 Introduced the `$old_theme` parameter.
* @param string $new_name Name of the new theme.
* @param WP_Theme $new_theme WP_Theme instance of the new theme.
* @param WP_Theme $old_theme WP_Theme instance of the old theme.
do_action( 'switch_theme', $new_name, $new_theme, $old_theme );
* Checks that the current theme has 'index.php' and 'style.css' files.
* Does not initially check the default theme, which is the fallback and should always exist.
* But if it doesn't exist, it'll fall back to the latest core default theme that does exist.
* Will switch theme to the fallback theme if current theme does not validate.
* You can use the {@see 'validate_current_theme'} filter to return false to disable
function validate_current_theme() {
* Filters whether to validate the current theme.
* @param bool $validate Whether to validate the current theme. Default true.
if ( wp_installing() || ! apply_filters( 'validate_current_theme', true ) ) {
if ( ! file_exists( get_template_directory() . '/index.php' ) ) {
} elseif ( ! file_exists( get_template_directory() . '/style.css' ) ) {
} elseif ( is_child_theme() && ! file_exists( get_stylesheet_directory() . '/style.css' ) ) {
$default = wp_get_theme( WP_DEFAULT_THEME );
if ( $default->exists() ) {
switch_theme( WP_DEFAULT_THEME );
* If we're in an invalid state but WP_DEFAULT_THEME doesn't exist,
* switch to the latest core default theme that's installed.
* If it turns out that this latest core default theme is our current
* theme, then there's nothing we can do about that, so we have to bail,
* rather than going into an infinite loop. (This is why there are
* checks against WP_DEFAULT_THEME above, also.) We also can't do anything
* if it turns out there is no default theme installed. (That's `false`.)
$default = WP_Theme::get_core_default_theme();
if ( false === $default || get_stylesheet() == $default->get_stylesheet() ) {
switch_theme( $default->get_stylesheet() );
* Validates the theme requirements for WordPress version and PHP version.
* Uses the information from `Requires at least` and `Requires PHP` headers
* defined in the theme's `style.css` file.
* If the headers are not present in the theme's stylesheet file,
* `readme.txt` is also checked as a fallback.
* @param string $stylesheet Directory name for the theme.
* @return true|WP_Error True if requirements are met, WP_Error on failure.
function validate_theme_requirements( $stylesheet ) {
$theme = wp_get_theme( $stylesheet );
'requires' => ! empty( $theme->get( 'RequiresWP' ) ) ? $theme->get( 'RequiresWP' ) : '',
'requires_php' => ! empty( $theme->get( 'RequiresPHP' ) ) ? $theme->get( 'RequiresPHP' ) : '',
$readme_file = $theme->theme_root . '/' . $stylesheet . '/readme.txt';
if ( file_exists( $readme_file ) ) {
$readme_headers = get_file_data(
'requires' => 'Requires at least',
'requires_php' => 'Requires PHP',
$requirements = array_merge( $readme_headers, $requirements );
$compatible_wp = is_wp_version_compatible( $requirements['requires'] );
$compatible_php = is_php_version_compatible( $requirements['requires_php'] );
if ( ! $compatible_wp && ! $compatible_php ) {
'theme_wp_php_incompatible',
/* translators: %s: Theme name. */
_x( '<strong>Error:</strong> Current WordPress and PHP versions do not meet minimum requirements for %s.', 'theme' ),
$theme->display( 'Name' )
} elseif ( ! $compatible_php ) {
'theme_php_incompatible',
/* translators: %s: Theme name. */
_x( '<strong>Error:</strong> Current PHP version does not meet minimum requirements for %s.', 'theme' ),
$theme->display( 'Name' )
} elseif ( ! $compatible_wp ) {
/* translators: %s: Theme name. */
_x( '<strong>Error:</strong> Current WordPress version does not meet minimum requirements for %s.', 'theme' ),
$theme->display( 'Name' )
* Retrieves all theme modifications.
* @return array|void Theme modifications.
function get_theme_mods() {
$theme_slug = get_option( 'stylesheet' );
$mods = get_option( "theme_mods_$theme_slug" );
$theme_name = get_option( 'current_theme' );
if ( false === $theme_name ) {
$theme_name = wp_get_theme()->get( 'Name' );
$mods = get_option( "mods_$theme_name" ); // Deprecated location.
if ( is_admin() && false !== $mods ) {
update_option( "theme_mods_$theme_slug", $mods );
delete_option( "mods_$theme_name" );
* Retrieves theme modification value for the current theme.
* If the modification name does not exist, then the $default will be passed
* through {@link https://www.php.net/sprintf sprintf()} PHP function with
* the template directory URI as the first string and the stylesheet directory URI