@@ -19,6 +19,30 @@ final class WP_Theme implements ArrayAccess {
1919 */
2020 public $ update = false ;
2121
22+ /**
23+ * Mapping of theme.json metadata keys to internal header keys.
24+ *
25+ * Used when reading theme metadata from the `metadata` property in theme.json
26+ * as an alternative to style.css file headers.
27+ *
28+ * @since 7.0.0
29+ * @var string[]
30+ */
31+ private static $ json_metadata_keys = array (
32+ 'name ' => 'Name ' ,
33+ 'uri ' => 'ThemeURI ' ,
34+ 'description ' => 'Description ' ,
35+ 'author ' => 'Author ' ,
36+ 'authorUri ' => 'AuthorURI ' ,
37+ 'version ' => 'Version ' ,
38+ 'template ' => 'Template ' ,
39+ 'status ' => 'Status ' ,
40+ 'tags ' => 'Tags ' ,
41+ 'textDomain ' => 'TextDomain ' ,
42+ 'domainPath ' => 'DomainPath ' ,
43+ 'updateUri ' => 'UpdateURI ' ,
44+ );
45+
2246 /**
2347 * Headers for style.css files.
2448 *
@@ -297,37 +321,63 @@ public function __construct( $theme_dir, $theme_root, $_child = null ) {
297321 $ theme_root_template = $ cache ['theme_root_template ' ];
298322 }
299323 } elseif ( ! file_exists ( $ this ->theme_root . '/ ' . $ theme_file ) ) {
300- $ this ->headers ['Name ' ] = $ this ->stylesheet ;
301324 if ( ! file_exists ( $ this ->theme_root . '/ ' . $ this ->stylesheet ) ) {
302- $ this ->errors = new WP_Error (
325+ $ this ->headers ['Name ' ] = $ this ->stylesheet ;
326+ $ this ->errors = new WP_Error (
303327 'theme_not_found ' ,
304328 sprintf (
305329 /* translators: %s: Theme directory name. */
306330 __ ( 'The theme directory "%s" does not exist. ' ),
307331 esc_html ( $ this ->stylesheet )
308332 )
309333 );
310- } else {
311- $ this ->errors = new WP_Error ( 'theme_no_stylesheet ' , __ ( 'Stylesheet is missing. ' ) );
334+ $ this ->template = $ this ->stylesheet ;
335+ $ this ->block_theme = false ;
336+ $ this ->block_template_folders = $ this ->default_template_folders ;
337+ $ this ->cache_add (
338+ 'theme ' ,
339+ array (
340+ 'block_template_folders ' => $ this ->block_template_folders ,
341+ 'block_theme ' => $ this ->block_theme ,
342+ 'headers ' => $ this ->headers ,
343+ 'errors ' => $ this ->errors ,
344+ 'stylesheet ' => $ this ->stylesheet ,
345+ 'template ' => $ this ->template ,
346+ )
347+ );
348+ if ( ! file_exists ( $ this ->theme_root ) ) { // Don't cache this one.
349+ $ this ->errors ->add ( 'theme_root_missing ' , __ ( '<strong>Error:</strong> The themes directory is either empty or does not exist. Please check your installation. ' ) );
350+ }
351+ return ;
312352 }
313- $ this ->template = $ this ->stylesheet ;
314- $ this ->block_theme = false ;
315- $ this ->block_template_folders = $ this ->default_template_folders ;
316- $ this ->cache_add (
317- 'theme ' ,
318- array (
319- 'block_template_folders ' => $ this ->block_template_folders ,
320- 'block_theme ' => $ this ->block_theme ,
321- 'headers ' => $ this ->headers ,
322- 'errors ' => $ this ->errors ,
323- 'stylesheet ' => $ this ->stylesheet ,
324- 'template ' => $ this ->template ,
325- )
326- );
327- if ( ! file_exists ( $ this ->theme_root ) ) { // Don't cache this one.
328- $ this ->errors ->add ( 'theme_root_missing ' , __ ( '<strong>Error:</strong> The themes directory is either empty or does not exist. Please check your installation. ' ) );
353+
354+ /*
355+ * The theme directory exists but style.css is missing.
356+ * Try reading metadata from theme.json before treating this as an error.
357+ */
358+ $ json_headers = $ this ->read_json_metadata ();
359+
360+ if ( ! $ json_headers ) {
361+ $ this ->headers ['Name ' ] = $ this ->stylesheet ;
362+ $ this ->errors = new WP_Error ( 'theme_no_stylesheet ' , __ ( 'Stylesheet is missing. ' ) );
363+ $ this ->template = $ this ->stylesheet ;
364+ $ this ->block_theme = false ;
365+ $ this ->block_template_folders = $ this ->default_template_folders ;
366+ $ this ->cache_add (
367+ 'theme ' ,
368+ array (
369+ 'block_template_folders ' => $ this ->block_template_folders ,
370+ 'block_theme ' => $ this ->block_theme ,
371+ 'headers ' => $ this ->headers ,
372+ 'errors ' => $ this ->errors ,
373+ 'stylesheet ' => $ this ->stylesheet ,
374+ 'template ' => $ this ->template ,
375+ )
376+ );
377+ return ;
329378 }
330- return ;
379+
380+ $ this ->headers = $ json_headers ;
331381 } elseif ( ! is_readable ( $ this ->theme_root . '/ ' . $ theme_file ) ) {
332382 $ this ->headers ['Name ' ] = $ this ->stylesheet ;
333383 $ this ->errors = new WP_Error ( 'theme_stylesheet_not_readable ' , __ ( 'Stylesheet is not readable. ' ) );
@@ -347,7 +397,12 @@ public function __construct( $theme_dir, $theme_root, $_child = null ) {
347397 );
348398 return ;
349399 } else {
350- $ this ->headers = get_file_data ( $ this ->theme_root . '/ ' . $ theme_file , self ::$ file_headers , 'theme ' );
400+ $ this ->headers = $ this ->read_json_metadata ();
401+
402+ if ( ! $ this ->headers ) {
403+ $ this ->headers = get_file_data ( $ this ->theme_root . '/ ' . $ theme_file , self ::$ file_headers , 'theme ' );
404+ }
405+
351406 /*
352407 * Default themes always trump their pretenders.
353408 * Properly identify default themes that are inside a directory within wp-content/themes.
@@ -799,6 +854,82 @@ public function __wakeup() {
799854 $ this ->headers_sanitized = array ();
800855 }
801856
857+ /**
858+ * Reads theme metadata from the `metadata` property in theme.json.
859+ *
860+ * Provides a structured JSON alternative to parsing CSS comment headers
861+ * in style.css. When a theme.json file contains a `metadata` property,
862+ * its values are mapped to the internal header format used by WP_Theme.
863+ *
864+ * @since 7.0.0
865+ *
866+ * @return array|false Array of theme headers keyed by header name, or false
867+ * if theme.json does not exist or has no metadata property.
868+ */
869+ private function read_json_metadata () {
870+ $ theme_json_file = $ this ->theme_root . '/ ' . $ this ->stylesheet . '/theme.json ' ;
871+
872+ if ( ! is_readable ( $ theme_json_file ) ) {
873+ return false ;
874+ }
875+
876+ /*
877+ * Read and decode JSON directly rather than using wp_json_file_decode().
878+ *
879+ * This method is called during theme enumeration for every theme that has
880+ * a theme.json file, including themes without a `metadata` property and
881+ * themes with malformed JSON. Using wp_json_file_decode() would trigger
882+ * wp_trigger_error() for invalid JSON, which is undesirable here since
883+ * the existing WP_Theme_JSON_Resolver handles JSON errors when it needs
884+ * to parse theme settings. This method should fail silently and fall back
885+ * to style.css headers.
886+ */
887+ $ contents = @file_get_contents ( $ theme_json_file );
888+
889+ if ( false === $ contents ) {
890+ return false ;
891+ }
892+
893+ $ theme_json_data = json_decode ( $ contents , true );
894+
895+ if ( ! is_array ( $ theme_json_data ) || ! isset ( $ theme_json_data ['metadata ' ] ) || ! is_array ( $ theme_json_data ['metadata ' ] ) ) {
896+ return false ;
897+ }
898+
899+ $ metadata = $ theme_json_data ['metadata ' ];
900+
901+ // Initialize all headers to empty strings (matching get_file_data() behavior).
902+ $ headers = array_fill_keys ( array_keys ( self ::$ file_headers ), '' );
903+
904+ // Map JSON metadata keys to internal header keys.
905+ foreach ( self ::$ json_metadata_keys as $ json_key => $ header_key ) {
906+ if ( isset ( $ metadata [ $ json_key ] ) ) {
907+ if ( 'tags ' === $ json_key && is_array ( $ metadata [ $ json_key ] ) ) {
908+ $ headers [ $ header_key ] = implode ( ', ' , $ metadata [ $ json_key ] );
909+ } else {
910+ $ headers [ $ header_key ] = (string ) $ metadata [ $ json_key ];
911+ }
912+ }
913+ }
914+
915+ // Map nested requires object.
916+ if ( isset ( $ metadata ['requires ' ] ) && is_array ( $ metadata ['requires ' ] ) ) {
917+ if ( isset ( $ metadata ['requires ' ]['wordpress ' ] ) ) {
918+ $ headers ['RequiresWP ' ] = (string ) $ metadata ['requires ' ]['wordpress ' ];
919+ }
920+ if ( isset ( $ metadata ['requires ' ]['php ' ] ) ) {
921+ $ headers ['RequiresPHP ' ] = (string ) $ metadata ['requires ' ]['php ' ];
922+ }
923+ }
924+
925+ // Only return headers if at least a name was provided.
926+ if ( empty ( $ headers ['Name ' ] ) ) {
927+ return false ;
928+ }
929+
930+ return $ headers ;
931+ }
932+
802933 /**
803934 * Adds theme data to cache.
804935 *
0 commit comments