#[AllowDynamicProperties]
final class WP_Theme implements ArrayAccess {
* Whether the theme has been marked as updateable.
* @see WP_MS_Themes_List_Table
* Headers for style.css files.
* @since 5.4.0 Added `Requires at least` and `Requires PHP` headers.
* @since 6.1.0 Added `Update URI` header.
private static $file_headers = array(
'ThemeURI' => 'Theme URI',
'Description' => 'Description',
'AuthorURI' => 'Author URI',
'Template' => 'Template',
'TextDomain' => 'Text Domain',
'DomainPath' => 'Domain Path',
'RequiresWP' => 'Requires at least',
'RequiresPHP' => 'Requires PHP',
'UpdateURI' => 'Update URI',
* @since 3.5.0 Added the Twenty Twelve theme.
* @since 3.6.0 Added the Twenty Thirteen theme.
* @since 3.8.0 Added the Twenty Fourteen theme.
* @since 4.1.0 Added the Twenty Fifteen theme.
* @since 4.4.0 Added the Twenty Sixteen theme.
* @since 4.7.0 Added the Twenty Seventeen theme.
* @since 5.0.0 Added the Twenty Nineteen theme.
* @since 5.3.0 Added the Twenty Twenty theme.
* @since 5.6.0 Added the Twenty Twenty-One theme.
* @since 5.9.0 Added the Twenty Twenty-Two theme.
* @since 6.1.0 Added the Twenty Twenty-Three theme.
* @since 6.4.0 Added the Twenty Twenty-Four theme.
private static $default_themes = array(
'classic' => 'WordPress Classic',
'default' => 'WordPress Default',
'twentyten' => 'Twenty Ten',
'twentyeleven' => 'Twenty Eleven',
'twentytwelve' => 'Twenty Twelve',
'twentythirteen' => 'Twenty Thirteen',
'twentyfourteen' => 'Twenty Fourteen',
'twentyfifteen' => 'Twenty Fifteen',
'twentysixteen' => 'Twenty Sixteen',
'twentyseventeen' => 'Twenty Seventeen',
'twentynineteen' => 'Twenty Nineteen',
'twentytwenty' => 'Twenty Twenty',
'twentytwentyone' => 'Twenty Twenty-One',
'twentytwentytwo' => 'Twenty Twenty-Two',
'twentytwentythree' => 'Twenty Twenty-Three',
'twentytwentyfour' => 'Twenty Twenty-Four',
private static $tag_map = array(
'fixed-width' => 'fixed-layout',
'flexible-width' => 'fluid-layout',
* Absolute path to the theme root, usually wp-content/themes
* Header data from the theme's style.css file.
private $headers = array();
* Header data from the theme's style.css file after being sanitized.
private $headers_sanitized;
* Is this theme a block theme.
* Header name from the theme's style.css after being translated.
* Cached due to sorting functions running over the translated name.
private $name_translated;
* Errors encountered when initializing the theme.
* The directory name of the theme's files, inside the theme root.
* In the case of a child theme, this is directory name of the child theme.
* Otherwise, 'stylesheet' is the same as 'template'.
* The directory name of the theme's files, inside the theme root.
* In the case of a child theme, this is the directory name of the parent theme.
* Otherwise, 'template' is the same as 'stylesheet'.
* A reference to the parent theme, in the case of a child theme.
* URL to the theme root, usually an absolute URL to wp-content/themes
* Flag for whether the theme's textdomain is loaded.
private $textdomain_loaded;
* Stores an md5 hash of the theme root, to function as the cache key.
* Block template folders.
private $block_template_folders;
* Default values for template folders.
private $default_template_folders = array(
'wp_template' => 'templates',
'wp_template_part' => 'parts',
* Flag for whether the themes cache bucket should be persistently cached.
* Default is false. Can be set with the {@see 'wp_cache_themes_persistently'} filter.
private static $persistently_cache;
* Expiration time for the themes cache bucket.
* By default the bucket is not cached, so this value is useless.
private static $cache_expiration = 1800;
* Constructor for WP_Theme.
* @global array $wp_theme_directories
* @param string $theme_dir Directory of the theme within the theme_root.
* @param string $theme_root Theme root.
* @param WP_Theme|null $_child If this theme is a parent theme, the child may be passed for validation purposes.
public function __construct( $theme_dir, $theme_root, $_child = null ) {
global $wp_theme_directories;
// Initialize caching on first run.
if ( ! isset( self::$persistently_cache ) ) {
/** This action is documented in wp-includes/theme.php */
self::$persistently_cache = apply_filters( 'wp_cache_themes_persistently', false, 'WP_Theme' );
if ( self::$persistently_cache ) {
wp_cache_add_global_groups( 'themes' );
if ( is_int( self::$persistently_cache ) ) {
self::$cache_expiration = self::$persistently_cache;
wp_cache_add_non_persistent_groups( 'themes' );
// Handle a numeric theme directory as a string.
$theme_dir = (string) $theme_dir;
$this->theme_root = $theme_root;
$this->stylesheet = $theme_dir;
// Correct a situation where the theme is 'some-directory/some-theme' but 'some-directory' was passed in as part of the theme root instead.
if ( ! in_array( $theme_root, (array) $wp_theme_directories, true )
&& in_array( dirname( $theme_root ), (array) $wp_theme_directories, true )
$this->stylesheet = basename( $this->theme_root ) . '/' . $this->stylesheet;
$this->theme_root = dirname( $theme_root );
$this->cache_hash = md5( $this->theme_root . '/' . $this->stylesheet );
$theme_file = $this->stylesheet . '/style.css';
$cache = $this->cache_get( 'theme' );
if ( is_array( $cache ) ) {
foreach ( array( 'block_template_folders', 'block_theme', 'errors', 'headers', 'template' ) as $key ) {
if ( isset( $cache[ $key ] ) ) {
$this->$key = $cache[ $key ];
if ( isset( $cache['theme_root_template'] ) ) {
$theme_root_template = $cache['theme_root_template'];
} elseif ( ! file_exists( $this->theme_root . '/' . $theme_file ) ) {
$this->headers['Name'] = $this->stylesheet;
if ( ! file_exists( $this->theme_root . '/' . $this->stylesheet ) ) {
$this->errors = new WP_Error(
/* translators: %s: Theme directory name. */
__( 'The theme directory "%s" does not exist.' ),
esc_html( $this->stylesheet )
$this->errors = new WP_Error( 'theme_no_stylesheet', __( 'Stylesheet is missing.' ) );
$this->template = $this->stylesheet;
$this->block_theme = false;
$this->block_template_folders = $this->default_template_folders;
'block_template_folders' => $this->block_template_folders,
'block_theme' => $this->block_theme,
'headers' => $this->headers,
'errors' => $this->errors,
'stylesheet' => $this->stylesheet,
'template' => $this->template,
if ( ! file_exists( $this->theme_root ) ) { // Don't cache this one.
$this->errors->add( 'theme_root_missing', __( '<strong>Error:</strong> The themes directory is either empty or does not exist. Please check your installation.' ) );
} elseif ( ! is_readable( $this->theme_root . '/' . $theme_file ) ) {
$this->headers['Name'] = $this->stylesheet;
$this->errors = new WP_Error( 'theme_stylesheet_not_readable', __( 'Stylesheet is not readable.' ) );
$this->template = $this->stylesheet;
$this->block_theme = false;
$this->block_template_folders = $this->default_template_folders;
'block_template_folders' => $this->block_template_folders,
'block_theme' => $this->block_theme,
'headers' => $this->headers,
'errors' => $this->errors,
'stylesheet' => $this->stylesheet,
'template' => $this->template,
$this->headers = get_file_data( $this->theme_root . '/' . $theme_file, self::$file_headers, 'theme' );
* Default themes always trump their pretenders.
* Properly identify default themes that are inside a directory within wp-content/themes.
$default_theme_slug = array_search( $this->headers['Name'], self::$default_themes, true );
if ( $default_theme_slug ) {
if ( basename( $this->stylesheet ) !== $default_theme_slug ) {
$this->headers['Name'] .= '/' . $this->stylesheet;
if ( ! $this->template && $this->stylesheet === $this->headers['Template'] ) {
$this->errors = new WP_Error(
/* translators: %s: Template. */
__( 'The theme defines itself as its parent theme. Please check the %s header.' ),
'block_template_folders' => $this->get_block_template_folders(),
'block_theme' => $this->is_block_theme(),
'headers' => $this->headers,
'errors' => $this->errors,
'stylesheet' => $this->stylesheet,
// (If template is set from cache [and there are no errors], we know it's good.)
if ( ! $this->template ) {
$this->template = $this->headers['Template'];
if ( ! $this->template ) {
$this->template = $this->stylesheet;
$theme_path = $this->theme_root . '/' . $this->stylesheet;
if ( ! $this->is_block_theme() && ! file_exists( $theme_path . '/index.php' ) ) {
$error_message = sprintf(
/* translators: 1: templates/index.html, 2: index.php, 3: Documentation URL, 4: Template, 5: style.css */
__( 'Template is missing. Standalone themes need to have a %1$s or %2$s template file. <a href="%3$s">Child themes</a> need to have a %4$s header in the %5$s stylesheet.' ),
'<code>templates/index.html</code>',
'<code>index.php</code>',
__( 'https://developer.wordpress.org/themes/advanced-topics/child-themes/' ),
$this->errors = new WP_Error( 'theme_no_index', $error_message );
'block_template_folders' => $this->get_block_template_folders(),
'block_theme' => $this->block_theme,
'headers' => $this->headers,
'errors' => $this->errors,
'stylesheet' => $this->stylesheet,
'template' => $this->template,
// If we got our data from cache, we can assume that 'template' is pointing to the right place.
if ( ! is_array( $cache )
&& $this->template !== $this->stylesheet
&& ! file_exists( $this->theme_root . '/' . $this->template . '/index.php' )
* If we're in a directory of themes inside /themes, look for the parent nearby.
* wp-content/themes/directory-of-themes/*
$parent_dir = dirname( $this->stylesheet );
$directories = search_theme_directories();
&& file_exists( $this->theme_root . '/' . $parent_dir . '/' . $this->template . '/index.php' )
$this->template = $parent_dir . '/' . $this->template;
} elseif ( $directories && isset( $directories[ $this->template ] ) ) {
* Look for the template in the search_theme_directories() results, in case it is in another theme root.
* We don't look into directories of themes, just the theme root.
$theme_root_template = $directories[ $this->template ]['theme_root'];
// Parent theme is missing.
$this->errors = new WP_Error(
/* translators: %s: Theme directory name. */
__( 'The parent theme is missing. Please install the "%s" parent theme.' ),
esc_html( $this->template )
'block_template_folders' => $this->get_block_template_folders(),
'block_theme' => $this->is_block_theme(),
'headers' => $this->headers,
'errors' => $this->errors,
'stylesheet' => $this->stylesheet,
'template' => $this->template,
$this->parent = new WP_Theme( $this->template, $this->theme_root, $this );
// Set the parent, if we're a child theme.
if ( $this->template !== $this->stylesheet ) {
// If we are a parent, then there is a problem. Only two generations allowed! Cancel things out.
if ( $_child instanceof WP_Theme && $_child->template === $this->stylesheet ) {
$_child->errors = new WP_Error(
/* translators: %s: Theme directory name. */
__( 'The "%s" theme is not a valid parent theme.' ),
esc_html( $_child->template )
'block_template_folders' => $_child->get_block_template_folders(),
'block_theme' => $_child->is_block_theme(),
'headers' => $_child->headers,
'errors' => $_child->errors,
'stylesheet' => $_child->stylesheet,
'template' => $_child->template,
// The two themes actually reference each other with the Template header.
if ( $_child->stylesheet === $this->template ) {
$this->errors = new WP_Error(
/* translators: %s: Theme directory name. */
__( 'The "%s" theme is not a valid parent theme.' ),
esc_html( $this->template )