| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * WP_Theme Class 4 * 5 * @package WordPress 6 * @subpackage Theme 7 * @since 3.4.0 8 * 9 * @phpstan-type Theme_Key 'Name'|'Version'|'Status'|'Title'|'Author'|'Author Name'|'Author URI'|'Description'|'Template'|'Stylesheet'|'Template Files'|'Stylesheet Files'|'Template Dir'|'Stylesheet Dir'|'Screenshot'|'Tags'|'Theme Root'|'Theme Root URI'|'Parent Theme' 10 */ 11 #[AllowDynamicProperties] 12 final class WP_Theme implements ArrayAccess { 13 14 /** 15 * Whether the theme has been marked as updateable. 16 * 17 * @since 4.4.0 18 * @var bool 19 * 20 * @see WP_MS_Themes_List_Table 21 */ 22 public $update = false; 23 24 /** 25 * Headers for style.css files. 26 * 27 * @since 3.4.0 28 * @since 5.4.0 Added `Requires at least` and `Requires PHP` headers. 29 * @since 6.1.0 Added `Update URI` header. 30 * @var string[] 31 */ 32 private static $file_headers = array( 33 'Name' => 'Theme Name', 34 'ThemeURI' => 'Theme URI', 35 'Description' => 'Description', 36 'Author' => 'Author', 37 'AuthorURI' => 'Author URI', 38 'Version' => 'Version', 39 'Template' => 'Template', 40 'Status' => 'Status', 41 'Tags' => 'Tags', 42 'TextDomain' => 'Text Domain', 43 'DomainPath' => 'Domain Path', 44 'RequiresWP' => 'Requires at least', 45 'RequiresPHP' => 'Requires PHP', 46 'UpdateURI' => 'Update URI', 47 ); 48 49 /** 50 * Default themes. 51 * 52 * @since 3.4.0 53 * @since 3.5.0 Added the Twenty Twelve theme. 54 * @since 3.6.0 Added the Twenty Thirteen theme. 55 * @since 3.8.0 Added the Twenty Fourteen theme. 56 * @since 4.1.0 Added the Twenty Fifteen theme. 57 * @since 4.4.0 Added the Twenty Sixteen theme. 58 * @since 4.7.0 Added the Twenty Seventeen theme. 59 * @since 5.0.0 Added the Twenty Nineteen theme. 60 * @since 5.3.0 Added the Twenty Twenty theme. 61 * @since 5.6.0 Added the Twenty Twenty-One theme. 62 * @since 5.9.0 Added the Twenty Twenty-Two theme. 63 * @since 6.1.0 Added the Twenty Twenty-Three theme. 64 * @since 6.4.0 Added the Twenty Twenty-Four theme. 65 * @since 6.7.0 Added the Twenty Twenty-Five theme. 66 * @var string[] 67 */ 68 private static $default_themes = array( 69 'classic' => 'WordPress Classic', 70 'default' => 'WordPress Default', 71 'twentyten' => 'Twenty Ten', 72 'twentyeleven' => 'Twenty Eleven', 73 'twentytwelve' => 'Twenty Twelve', 74 'twentythirteen' => 'Twenty Thirteen', 75 'twentyfourteen' => 'Twenty Fourteen', 76 'twentyfifteen' => 'Twenty Fifteen', 77 'twentysixteen' => 'Twenty Sixteen', 78 'twentyseventeen' => 'Twenty Seventeen', 79 'twentynineteen' => 'Twenty Nineteen', 80 'twentytwenty' => 'Twenty Twenty', 81 'twentytwentyone' => 'Twenty Twenty-One', 82 'twentytwentytwo' => 'Twenty Twenty-Two', 83 'twentytwentythree' => 'Twenty Twenty-Three', 84 'twentytwentyfour' => 'Twenty Twenty-Four', 85 'twentytwentyfive' => 'Twenty Twenty-Five', 86 ); 87 88 /** 89 * Renamed theme tags. 90 * 91 * @since 3.8.0 92 * @var string[] 93 */ 94 private static $tag_map = array( 95 'fixed-width' => 'fixed-layout', 96 'flexible-width' => 'fluid-layout', 97 ); 98 99 /** 100 * Absolute path to the theme root, usually wp-content/themes 101 * 102 * @since 3.4.0 103 * @var string 104 */ 105 private $theme_root; 106 107 /** 108 * Header data from the theme's style.css file. 109 * 110 * @since 3.4.0 111 * @var array 112 */ 113 private $headers = array(); 114 115 /** 116 * Header data from the theme's style.css file after being sanitized. 117 * 118 * @since 3.4.0 119 * @var ?array 120 */ 121 private $headers_sanitized; 122 123 /** 124 * Is this theme a block theme. 125 * 126 * @since 6.2.0 127 * @var ?bool 128 */ 129 private $block_theme; 130 131 /** 132 * Header name from the theme's style.css after being translated. 133 * 134 * Cached due to sorting functions running over the translated name. 135 * 136 * @since 3.4.0 137 * @var ?string 138 */ 139 private $name_translated; 140 141 /** 142 * Errors encountered when initializing the theme. 143 * 144 * @since 3.4.0 145 * @var ?WP_Error 146 */ 147 private $errors; 148 149 /** 150 * The directory name of the theme's files, inside the theme root. 151 * 152 * In the case of a child theme, this is directory name of the child theme. 153 * Otherwise, 'stylesheet' is the same as 'template'. 154 * 155 * @since 3.4.0 156 * @var string 157 */ 158 private $stylesheet; 159 160 /** 161 * The directory name of the theme's files, inside the theme root. 162 * 163 * In the case of a child theme, this is the directory name of the parent theme. 164 * Otherwise, 'template' is the same as 'stylesheet'. 165 * 166 * @since 3.4.0 167 * @var ?string 168 */ 169 private $template; 170 171 /** 172 * A reference to the parent theme, in the case of a child theme. 173 * 174 * @since 3.4.0 175 * @var ?WP_Theme 176 */ 177 private $parent; 178 179 /** 180 * URL to the theme root, usually an absolute URL to wp-content/themes 181 * 182 * @since 3.4.0 183 * @var ?string 184 */ 185 private $theme_root_uri; 186 187 /** 188 * Flag for whether the theme's textdomain is loaded. 189 * 190 * @since 3.4.0 191 * @var ?bool 192 */ 193 private $textdomain_loaded; 194 195 /** 196 * Stores an md5 hash of the theme root, to function as the cache key. 197 * 198 * @since 3.4.0 199 * @var string 200 */ 201 private $cache_hash; 202 203 /** 204 * Block template folders. 205 * 206 * @since 6.4.0 207 * @var ?string[] 208 */ 209 private $block_template_folders; 210 211 /** 212 * Default values for template folders. 213 * 214 * @since 6.4.0 215 * @var string[] 216 */ 217 private $default_template_folders = array( 218 'wp_template' => 'templates', 219 'wp_template_part' => 'parts', 220 ); 221 222 /** 223 * Flag for whether the themes cache bucket should be persistently cached. 224 * 225 * Default is false. Can be set with the {@see 'wp_cache_themes_persistently'} filter. 226 * 227 * @since 3.4.0 228 * @var bool 229 */ 230 private static $persistently_cache; 231 232 /** 233 * Expiration time for the themes cache bucket. 234 * 235 * By default the bucket is not cached, so this value is useless. 236 * 237 * @since 3.4.0 238 * @var int 239 */ 240 private static $cache_expiration = 1800; 241 242 /** 243 * Constructor for WP_Theme. 244 * 245 * @since 3.4.0 246 * 247 * @global string[] $wp_theme_directories 248 * 249 * @param string $theme_dir Directory of the theme within the theme_root. 250 * @param string $theme_root Theme root. 251 * @param WP_Theme|null $_child If this theme is a parent theme, the child may be passed for validation purposes. 252 */ 253 public function __construct( $theme_dir, $theme_root, $_child = null ) { 254 global $wp_theme_directories; 255 256 // Initialize caching on first run. 257 if ( ! isset( self::$persistently_cache ) ) { 258 /** This action is documented in wp-includes/theme.php */ 259 self::$persistently_cache = apply_filters( 'wp_cache_themes_persistently', false, 'WP_Theme' ); 260 if ( self::$persistently_cache ) { 261 wp_cache_add_global_groups( 'themes' ); 262 if ( is_int( self::$persistently_cache ) ) { 263 self::$cache_expiration = self::$persistently_cache; 264 } 265 } else { 266 wp_cache_add_non_persistent_groups( 'themes' ); 267 } 268 } 269 270 // Handle a numeric theme directory as a string. 271 $theme_dir = (string) $theme_dir; 272 273 $this->theme_root = $theme_root; 274 $this->stylesheet = $theme_dir; 275 276 // Correct a situation where the theme is 'some-directory/some-theme' but 'some-directory' was passed in as part of the theme root instead. 277 if ( ! in_array( $theme_root, (array) $wp_theme_directories, true ) 278 && in_array( dirname( $theme_root ), (array) $wp_theme_directories, true ) 279 ) { 280 $this->stylesheet = basename( $this->theme_root ) . '/' . $this->stylesheet; 281 $this->theme_root = dirname( $theme_root ); 282 } 283 284 $this->cache_hash = md5( $this->theme_root . '/' . $this->stylesheet ); 285 $theme_file = $this->stylesheet . '/style.css'; 286 287 $cache = $this->cache_get( 'theme' ); 288 289 if ( is_array( $cache ) ) { 290 foreach ( array( 'block_template_folders', 'block_theme', 'errors', 'headers', 'template' ) as $key ) { 291 if ( isset( $cache[ $key ] ) ) { 292 $this->$key = $cache[ $key ]; 293 } 294 } 295 if ( $this->errors ) { 296 return; 297 } 298 if ( isset( $cache['theme_root_template'] ) ) { 299 $theme_root_template = $cache['theme_root_template']; 300 } 301 } elseif ( ! file_exists( $this->theme_root . '/' . $theme_file ) ) { 302 $this->headers['Name'] = $this->stylesheet; 303 if ( ! file_exists( $this->theme_root . '/' . $this->stylesheet ) ) { 304 $this->errors = new WP_Error( 305 'theme_not_found', 306 sprintf( 307 /* translators: %s: Theme directory name. */ 308 __( 'The theme directory "%s" does not exist.' ), 309 esc_html( $this->stylesheet ) 310 ) 311 ); 312 } else { 313 $this->errors = new WP_Error( 'theme_no_stylesheet', __( 'Stylesheet is missing.' ) ); 314 } 315 $this->template = $this->stylesheet; 316 $this->block_theme = false; 317 $this->block_template_folders = $this->default_template_folders; 318 $this->cache_add( 319 'theme', 320 array( 321 'block_template_folders' => $this->block_template_folders, 322 'block_theme' => $this->block_theme, 323 'headers' => $this->headers, 324 'errors' => $this->errors, 325 'stylesheet' => $this->stylesheet, 326 'template' => $this->template, 327 ) 328 ); 329 if ( ! file_exists( $this->theme_root ) ) { // Don't cache this one. 330 $this->errors->add( 'theme_root_missing', __( '<strong>Error:</strong> The themes directory is either empty or does not exist. Please check your installation.' ) ); 331 } 332 return; 333 } elseif ( ! is_readable( $this->theme_root . '/' . $theme_file ) ) { 334 $this->headers['Name'] = $this->stylesheet; 335 $this->errors = new WP_Error( 'theme_stylesheet_not_readable', __( 'Stylesheet is not readable.' ) ); 336 $this->template = $this->stylesheet; 337 $this->block_theme = false; 338 $this->block_template_folders = $this->default_template_folders; 339 $this->cache_add( 340 'theme', 341 array( 342 'block_template_folders' => $this->block_template_folders, 343 'block_theme' => $this->block_theme, 344 'headers' => $this->headers, 345 'errors' => $this->errors, 346 'stylesheet' => $this->stylesheet, 347 'template' => $this->template, 348 ) 349 ); 350 return; 351 } else { 352 $this->headers = get_file_data( $this->theme_root . '/' . $theme_file, self::$file_headers, 'theme' ); 353 /* 354 * Default themes always trump their pretenders. 355 * Properly identify default themes that are inside a directory within wp-content/themes. 356 */ 357 $default_theme_slug = array_search( $this->headers['Name'], self::$default_themes, true ); 358 if ( $default_theme_slug ) { 359 if ( basename( $this->stylesheet ) !== $default_theme_slug ) { 360 $this->headers['Name'] .= '/' . $this->stylesheet; 361 } 362 } 363 } 364 365 if ( ! $this->template && $this->stylesheet === $this->headers['Template'] ) { 366 $this->errors = new WP_Error( 367 'theme_child_invalid', 368 sprintf( 369 /* translators: %s: Template. */ 370 __( 'The theme defines itself as its parent theme. Please check the %s header.' ), 371 '<code>Template</code>' 372 ) 373 ); 374 $this->template = $this->stylesheet; 375 $this->cache_add( 376 'theme', 377 array( 378 'block_template_folders' => $this->get_block_template_folders(), 379 'block_theme' => $this->is_block_theme(), 380 'headers' => $this->headers, 381 'errors' => $this->errors, 382 'stylesheet' => $this->stylesheet, 383 'template' => $this->template, 384 ) 385 ); 386 387 return; 388 } 389 390 // (If template is set from cache [and there are no errors], we know it's good.) 391 if ( ! $this->template ) { 392 $this->template = $this->headers['Template']; 393 } 394 395 if ( ! $this->template ) { 396 $this->template = $this->stylesheet; 397 $theme_path = $this->theme_root . '/' . $this->stylesheet; 398 399 if ( ! $this->is_block_theme() && ! file_exists( $theme_path . '/index.php' ) ) { 400 $error_message = sprintf( 401 /* translators: 1: templates/index.html, 2: index.php, 3: Documentation URL, 4: Template, 5: style.css */ 402 __( '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.' ), 403 '<code>templates/index.html</code>', 404 '<code>index.php</code>', 405 __( 'https://developer.wordpress.org/themes/advanced-topics/child-themes/' ), 406 '<code>Template</code>', 407 '<code>style.css</code>' 408 ); 409 $this->errors = new WP_Error( 'theme_no_index', $error_message ); 410 $this->cache_add( 411 'theme', 412 array( 413 'block_template_folders' => $this->get_block_template_folders(), 414 'block_theme' => $this->block_theme, 415 'headers' => $this->headers, 416 'errors' => $this->errors, 417 'stylesheet' => $this->stylesheet, 418 'template' => $this->template, 419 ) 420 ); 421 return; 422 } 423 } 424 425 // If we got our data from cache, we can assume that 'template' is pointing to the right place. 426 if ( ! is_array( $cache ) 427 && $this->template !== $this->stylesheet 428 && ! file_exists( $this->theme_root . '/' . $this->template . '/index.php' ) 429 ) { 430 /* 431 * If we're in a directory of themes inside /themes, look for the parent nearby. 432 * wp-content/themes/directory-of-themes/* 433 */ 434 $parent_dir = dirname( $this->stylesheet ); 435 $directories = search_theme_directories(); 436 437 if ( '.' !== $parent_dir 438 && file_exists( $this->theme_root . '/' . $parent_dir . '/' . $this->template . '/index.php' ) 439 ) { 440 $this->template = $parent_dir . '/' . $this->template; 441 } elseif ( $directories && isset( $directories[ $this->template ] ) ) { 442 /* 443 * Look for the template in the search_theme_directories() results, in case it is in another theme root. 444 * We don't look into directories of themes, just the theme root. 445 */ 446 $theme_root_template = $directories[ $this->template ]['theme_root']; 447 } else { 448 // Parent theme is missing. 449 $this->errors = new WP_Error( 450 'theme_no_parent', 451 sprintf( 452 /* translators: %s: Theme directory name. */ 453 __( 'The parent theme is missing. Please install the "%s" parent theme.' ), 454 esc_html( $this->template ) 455 ) 456 ); 457 $this->cache_add( 458 'theme', 459 array( 460 'block_template_folders' => $this->get_block_template_folders(), 461 'block_theme' => $this->is_block_theme(), 462 'headers' => $this->headers, 463 'errors' => $this->errors, 464 'stylesheet' => $this->stylesheet, 465 'template' => $this->template, 466 ) 467 ); 468 $this->parent = new WP_Theme( $this->template, $this->theme_root, $this ); 469 return; 470 } 471 } 472 473 // Set the parent, if we're a child theme. 474 if ( $this->template !== $this->stylesheet ) { 475 // If we are a parent, then there is a problem. Only two generations allowed! Cancel things out. 476 if ( $_child instanceof WP_Theme && $_child->template === $this->stylesheet ) { 477 $_child->parent = null; 478 $_child->errors = new WP_Error( 479 'theme_parent_invalid', 480 sprintf( 481 /* translators: %s: Theme directory name. */ 482 __( 'The "%s" theme is not a valid parent theme.' ), 483 esc_html( $_child->template ) 484 ) 485 ); 486 $_child->cache_add( 487 'theme', 488 array( 489 'block_template_folders' => $_child->get_block_template_folders(), 490 'block_theme' => $_child->is_block_theme(), 491 'headers' => $_child->headers, 492 'errors' => $_child->errors, 493 'stylesheet' => $_child->stylesheet, 494 'template' => $_child->template, 495 ) 496 ); 497 // The two themes actually reference each other with the Template header. 498 if ( $_child->stylesheet === $this->template ) { 499 $this->errors = new WP_Error( 500 'theme_parent_invalid', 501 sprintf( 502 /* translators: %s: Theme directory name. */ 503 __( 'The "%s" theme is not a valid parent theme.' ), 504 esc_html( $this->template ) 505 ) 506 ); 507 $this->cache_add( 508 'theme', 509 array( 510 'block_template_folders' => $this->get_block_template_folders(), 511 'block_theme' => $this->is_block_theme(), 512 'headers' => $this->headers, 513 'errors' => $this->errors, 514 'stylesheet' => $this->stylesheet, 515 'template' => $this->template, 516 ) 517 ); 518 } 519 return; 520 } 521 // Set the parent. Pass the current instance so we can do the checks above and assess errors. 522 $this->parent = new WP_Theme( $this->template, $theme_root_template ?? $this->theme_root, $this ); 523 } 524 525 if ( wp_paused_themes()->get( $this->stylesheet ) && ( ! is_wp_error( $this->errors ) || ! isset( $this->errors->errors['theme_paused'] ) ) ) { 526 $this->errors = new WP_Error( 'theme_paused', __( 'This theme failed to load properly and was paused within the admin backend.' ) ); 527 } 528 529 // We're good. If we didn't retrieve from cache, set it. 530 if ( ! is_array( $cache ) ) { 531 $cache = array( 532 'block_theme' => $this->is_block_theme(), 533 'block_template_folders' => $this->get_block_template_folders(), 534 'headers' => $this->headers, 535 'errors' => $this->errors, 536 'stylesheet' => $this->stylesheet, 537 'template' => $this->template, 538 ); 539 // If the parent theme is in another root, we'll want to cache this. Avoids an entire branch of filesystem calls above. 540 if ( isset( $theme_root_template ) ) { 541 $cache['theme_root_template'] = $theme_root_template; 542 } 543 $this->cache_add( 'theme', $cache ); 544 } 545 } 546 547 /** 548 * When converting the object to a string, the theme name is returned. 549 * 550 * @since 3.4.0 551 * 552 * @return string Theme name, ready for display (translated) 553 */ 554 public function __toString() { 555 return (string) $this->display( 'Name' ); 556 } 557 558 /** 559 * __isset() magic method for properties formerly returned by current_theme_info() 560 * 561 * @since 3.4.0 562 * 563 * @param string $offset Property to check if set. 564 * @return bool Whether the given property is set. 565 */ 566 public function __isset( $offset ) { 567 static $properties = array( 568 'name', 569 'title', 570 'version', 571 'parent_theme', 572 'template_dir', 573 'stylesheet_dir', 574 'template', 575 'stylesheet', 576 'screenshot', 577 'description', 578 'author', 579 'tags', 580 'theme_root', 581 'theme_root_uri', 582 ); 583 584 return in_array( $offset, $properties, true ); 585 } 586 587 /** 588 * __get() magic method for properties formerly returned by current_theme_info() 589 * 590 * @since 3.4.0 591 * 592 * @param string $offset Property to get. 593 * @return mixed Property value. 594 */ 595 public function __get( $offset ) { 596 switch ( $offset ) { 597 case 'name': 598 case 'title': 599 return $this->get( 'Name' ); 600 case 'version': 601 return $this->get( 'Version' ); 602 case 'parent_theme': 603 return $this->parent() ? $this->parent()->get( 'Name' ) : ''; 604 case 'template_dir': 605 return $this->get_template_directory(); 606 case 'stylesheet_dir': 607 return $this->get_stylesheet_directory(); 608 case 'template': 609 return $this->get_template(); 610 case 'stylesheet': 611 return $this->get_stylesheet(); 612 case 'screenshot': 613 return $this->get_screenshot( 'relative' ); 614 // 'author' and 'description' did not previously return translated data. 615 case 'description': 616 return $this->display( 'Description' ); 617 case 'author': 618 return $this->display( 'Author' ); 619 case 'tags': 620 return $this->get( 'Tags' ); 621 case 'theme_root': 622 return $this->get_theme_root(); 623 case 'theme_root_uri': 624 return $this->get_theme_root_uri(); 625 // For cases where the array was converted to an object. 626 default: 627 return $this->offsetGet( $offset ); 628 } 629 } 630 631 /** 632 * Method to implement ArrayAccess for keys formerly returned by get_themes() 633 * 634 * @since 3.4.0 635 * 636 * @param mixed $offset 637 * @param mixed $value 638 */ 639 #[ReturnTypeWillChange] 640 public function offsetSet( $offset, $value ) {} 641 642 /** 643 * Method to implement ArrayAccess for keys formerly returned by get_themes() 644 * 645 * @since 3.4.0 646 * 647 * @param mixed $offset 648 */ 649 #[ReturnTypeWillChange] 650 public function offsetUnset( $offset ) {} 651 652 /** 653 * Method to implement ArrayAccess for keys formerly returned by get_themes() 654 * 655 * @since 3.4.0 656 * 657 * @param mixed $offset 658 * @return bool 659 * 660 * @phpstan-return ( $offset is Theme_Key ? true : false ) 661 */ 662 #[ReturnTypeWillChange] 663 public function offsetExists( $offset ) { 664 static $keys = array( 665 'Name', 666 'Version', 667 'Status', 668 'Title', 669 'Author', 670 'Author Name', 671 'Author URI', 672 'Description', 673 'Template', 674 'Stylesheet', 675 'Template Files', 676 'Stylesheet Files', 677 'Template Dir', 678 'Stylesheet Dir', 679 'Screenshot', 680 'Tags', 681 'Theme Root', 682 'Theme Root URI', 683 'Parent Theme', 684 ); 685 686 return in_array( $offset, $keys, true ); 687 } 688 689 /** 690 * Method to implement ArrayAccess for keys formerly returned by get_themes(). 691 * 692 * Author, Author Name, Author URI, and Description did not previously return 693 * translated data. We are doing so now as it is safe to do. However, as 694 * Name and Title could have been used as the key for get_themes(), both remain 695 * untranslated for back compatibility. This means that ['Name'] is not ideal, 696 * and care should be taken to use `$theme::display( 'Name' )` to get a properly 697 * translated header. 698 * 699 * @since 3.4.0 700 * 701 * @param mixed $offset 702 * @return mixed 703 * 704 * @phpstan-return ( $offset is Theme_Key ? mixed : null ) 705 */ 706 #[ReturnTypeWillChange] 707 public function offsetGet( $offset ) { 708 switch ( $offset ) { 709 case 'Name': 710 case 'Title': 711 /* 712 * See note above about using translated data. get() is not ideal. 713 * It is only for backward compatibility. Use display(). 714 */ 715 return $this->get( 'Name' ); 716 case 'Author': 717 return $this->display( 'Author' ); 718 case 'Author Name': 719 return $this->display( 'Author', false ); 720 case 'Author URI': 721 return $this->display( 'AuthorURI' ); 722 case 'Description': 723 return $this->display( 'Description' ); 724 case 'Version': 725 case 'Status': 726 return $this->get( $offset ); 727 case 'Template': 728 return $this->get_template(); 729 case 'Stylesheet': 730 return $this->get_stylesheet(); 731 case 'Template Files': 732 return $this->get_files( 'php', 1, true ); 733 case 'Stylesheet Files': 734 return $this->get_files( 'css', 0, false ); 735 case 'Template Dir': 736 return $this->get_template_directory(); 737 case 'Stylesheet Dir': 738 return $this->get_stylesheet_directory(); 739 case 'Screenshot': 740 return $this->get_screenshot( 'relative' ); 741 case 'Tags': 742 return $this->get( 'Tags' ); 743 case 'Theme Root': 744 return $this->get_theme_root(); 745 case 'Theme Root URI': 746 return $this->get_theme_root_uri(); 747 case 'Parent Theme': 748 return $this->parent() ? $this->parent()->get( 'Name' ) : ''; 749 default: 750 return null; 751 } 752 } 753 754 /** 755 * Returns errors property. 756 * 757 * @since 3.4.0 758 * 759 * @return WP_Error|false WP_Error if there are errors, or false. 760 */ 761 public function errors() { 762 return is_wp_error( $this->errors ) ? $this->errors : false; 763 } 764 765 /** 766 * Determines whether the theme exists. 767 * 768 * A theme with errors exists. A theme with the error of 'theme_not_found', 769 * meaning that the theme's directory was not found, does not exist. 770 * 771 * @since 3.4.0 772 * 773 * @return bool Whether the theme exists. 774 */ 775 public function exists() { 776 return ! ( $this->errors() && in_array( 'theme_not_found', $this->errors()->get_error_codes(), true ) ); 777 } 778 779 /** 780 * Returns reference to the parent theme. 781 * 782 * @since 3.4.0 783 * 784 * @return WP_Theme|false Parent theme, or false if the active theme is not a child theme. 785 */ 786 public function parent() { 787 return $this->parent ?? false; 788 } 789 790 /** 791 * Perform reinitialization tasks. 792 * 793 * Prevents a callback from being injected during unserialization of an object. 794 */ 795 public function __wakeup() { 796 if ( $this->parent && ! $this->parent instanceof self ) { 797 throw new UnexpectedValueException(); 798 } 799 if ( $this->headers && ! is_array( $this->headers ) ) { 800 throw new UnexpectedValueException(); 801 } 802 foreach ( $this->headers as $value ) { 803 if ( ! is_string( $value ) ) { 804 throw new UnexpectedValueException(); 805 } 806 } 807 $this->headers_sanitized = array(); 808 } 809 810 /** 811 * Adds theme data to cache. 812 * 813 * Cache entries keyed by the theme and the type of data. 814 * 815 * @since 3.4.0 816 * 817 * @param string $key Type of data to store (theme, screenshot, headers, post_templates) 818 * @param array|string $data Data to store 819 * @return bool Return value from wp_cache_add() 820 */ 821 private function cache_add( $key, $data ) { 822 return wp_cache_add( $key . '-' . $this->cache_hash, $data, 'themes', self::$cache_expiration ); 823 } 824 825 /** 826 * Gets theme data from cache. 827 * 828 * Cache entries are keyed by the theme and the type of data. 829 * 830 * @since 3.4.0 831 * 832 * @param string $key Type of data to retrieve (theme, screenshot, headers, post_templates) 833 * @return mixed Retrieved data 834 */ 835 private function cache_get( $key ) { 836 return wp_cache_get( $key . '-' . $this->cache_hash, 'themes' ); 837 } 838 839 /** 840 * Clears the cache for the theme. 841 * 842 * @since 3.4.0 843 */ 844 public function cache_delete() { 845 foreach ( array( 'theme', 'screenshot', 'headers', 'post_templates' ) as $key ) { 846 wp_cache_delete( $key . '-' . $this->cache_hash, 'themes' ); 847 } 848 $this->template = null; 849 $this->textdomain_loaded = null; 850 $this->theme_root_uri = null; 851 $this->parent = null; 852 $this->errors = null; 853 $this->headers_sanitized = null; 854 $this->name_translated = null; 855 $this->block_theme = null; 856 $this->block_template_folders = null; 857 $this->headers = array(); 858 $this->__construct( $this->stylesheet, $this->theme_root ); 859 $this->delete_pattern_cache(); 860 } 861 862 /** 863 * Gets a raw, unformatted theme header. 864 * 865 * The header is sanitized, but is not translated, and is not marked up for display. 866 * To get a theme header for display, use the display() method. 867 * 868 * Use the get_template() method, not the 'Template' header, for finding the template. 869 * The 'Template' header is only good for what was written in the style.css, while 870 * get_template() takes into account where WordPress actually located the theme and 871 * whether it is actually valid. 872 * 873 * @since 3.4.0 874 * 875 * @param string $header Theme header. Name, Description, Author, Version, ThemeURI, AuthorURI, Status, Tags. 876 * @return string|array|false String or array (for Tags header) on success, false on failure. 877 * 878 * @phpstan-return ( 879 * $header is 'Tags' 880 * ? string[]|false 881 * : ( $header is 'Name'|'ThemeURI'|'Description'|'Author'|'AuthorURI'|'Version'|'Template'|'Status'|'TextDomain'|'DomainPath'|'RequiresWP'|'RequiresPHP'|'UpdateURI' 882 * ? string|false 883 * : false ) 884 * ) 885 */ 886 public function get( $header ) { 887 if ( ! isset( $this->headers[ $header ] ) ) { 888 return false; 889 } 890 891 if ( ! isset( $this->headers_sanitized ) ) { 892 $this->headers_sanitized = $this->cache_get( 'headers' ); 893 if ( ! is_array( $this->headers_sanitized ) ) { 894 $this->headers_sanitized = array(); 895 } 896 } 897 898 if ( isset( $this->headers_sanitized[ $header ] ) ) { 899 return $this->headers_sanitized[ $header ]; 900 } 901 902 // If themes are a persistent group, sanitize everything and cache it. One cache add is better than many cache sets. 903 if ( self::$persistently_cache ) { 904 foreach ( array_keys( $this->headers ) as $_header ) { 905 $this->headers_sanitized[ $_header ] = $this->sanitize_header( $_header, $this->headers[ $_header ] ); 906 } 907 $this->cache_add( 'headers', $this->headers_sanitized ); 908 } else { 909 $this->headers_sanitized[ $header ] = $this->sanitize_header( $header, $this->headers[ $header ] ); 910 } 911 912 return $this->headers_sanitized[ $header ]; 913 } 914 915 /** 916 * Gets a theme header, formatted and translated for display. 917 * 918 * @since 3.4.0 919 * 920 * @param string $header Theme header. Name, Description, Author, Version, ThemeURI, AuthorURI, Status, Tags. 921 * @param bool $markup Optional. Whether to mark up the header. Defaults to true. 922 * @param bool $translate Optional. Whether to translate the header. Defaults to true. 923 * @return string|array|false Processed header. An array for Tags if `$markup` is false, string otherwise. 924 * False on failure. 925 * 926 * @phpstan-return ( 927 * $markup is false 928 * ? ( $header is 'Tags' 929 * ? string[]|false 930 * : string|false ) 931 * : string|false 932 * ) 933 */ 934 public function display( $header, $markup = true, $translate = true ) { 935 $value = $this->get( $header ); 936 if ( false === $value ) { 937 return false; 938 } 939 940 if ( $translate && ( empty( $value ) || ! $this->load_textdomain() ) ) { 941 $translate = false; 942 } 943 944 if ( $translate ) { 945 $value = $this->translate_header( $header, $value ); 946 } 947 948 if ( $markup ) { 949 $value = $this->markup_header( $header, $value, $translate ); 950 } 951 952 return $value; 953 } 954 955 /** 956 * Sanitizes a theme header. 957 * 958 * @since 3.4.0 959 * @since 5.4.0 Added support for `Requires at least` and `Requires PHP` headers. 960 * @since 6.1.0 Added support for `Update URI` header. 961 * 962 * @param string $header Theme header. Accepts 'Name', 'Description', 'Author', 'Version', 963 * 'ThemeURI', 'AuthorURI', 'Status', 'Tags', 'RequiresWP', 'RequiresPHP', 964 * 'UpdateURI'. 965 * @param string $value Value to sanitize. 966 * @return string|string[] An array for Tags header, string otherwise. 967 * 968 * @phpstan-return ( $header is 'Tags' ? string[] : string ) 969 */ 970 private function sanitize_header( $header, $value ) { 971 switch ( $header ) { 972 case 'Status': 973 if ( ! $value ) { 974 $value = 'publish'; 975 break; 976 } 977 // Fall through otherwise. 978 case 'Name': 979 static $header_tags = array( 980 'abbr' => array( 'title' => true ), 981 'acronym' => array( 'title' => true ), 982 'code' => true, 983 'em' => true, 984 'strong' => true, 985 ); 986 987 $value = wp_kses( $value, $header_tags ); 988 break; 989 case 'Author': 990 // There shouldn't be anchor tags in Author, but some themes like to be challenging. 991 case 'Description': 992 static $header_tags_with_a = array( 993 'a' => array( 994 'href' => true, 995 'title' => true, 996 ), 997 'abbr' => array( 'title' => true ), 998 'acronym' => array( 'title' => true ), 999 'code' => true, 1000 'em' => true, 1001 'strong' => true, 1002 ); 1003 1004 $value = wp_kses( $value, $header_tags_with_a ); 1005 break; 1006 case 'ThemeURI': 1007 case 'AuthorURI': 1008 $value = sanitize_url( $value ); 1009 break; 1010 case 'Tags': 1011 $value = array_filter( array_map( 'trim', explode( ',', strip_tags( $value ) ) ) ); 1012 break; 1013 case 'Version': 1014 case 'RequiresWP': 1015 case 'RequiresPHP': 1016 case 'UpdateURI': 1017 $value = strip_tags( $value ); 1018 break; 1019 } 1020 1021 return $value; 1022 } 1023 1024 /** 1025 * Marks up a theme header. 1026 * 1027 * @since 3.4.0 1028 * 1029 * @param string $header Theme header. Name, Description, Author, Version, ThemeURI, AuthorURI, Status, Tags. 1030 * @param string|array $value Value to mark up. An array for Tags header, string otherwise. 1031 * @param bool $translate Whether the header has been translated. 1032 * @return string Value, marked up. 1033 */ 1034 private function markup_header( $header, $value, $translate ) { 1035 switch ( $header ) { 1036 case 'Name': 1037 if ( empty( $value ) ) { 1038 $value = esc_html( $this->get_stylesheet() ); 1039 } 1040 break; 1041 case 'Description': 1042 $value = wptexturize( $value ); 1043 break; 1044 case 'Author': 1045 if ( $this->get( 'AuthorURI' ) ) { 1046 $value = sprintf( '<a href="%1$s">%2$s</a>', $this->display( 'AuthorURI', true, $translate ), $value ); 1047 } elseif ( ! $value ) { 1048 $value = __( 'Anonymous' ); 1049 } 1050 break; 1051 case 'Tags': 1052 static $comma = null; 1053 if ( ! isset( $comma ) ) { 1054 $comma = wp_get_list_item_separator(); 1055 } 1056 $value = implode( $comma, $value ); 1057 break; 1058 case 'ThemeURI': 1059 case 'AuthorURI': 1060 $value = esc_url( $value ); 1061 break; 1062 } 1063 1064 return $value; 1065 } 1066 1067 /** 1068 * Translates a theme header. 1069 * 1070 * @since 3.4.0 1071 * 1072 * @param string $header Theme header. Name, Description, Author, Version, ThemeURI, AuthorURI, Status, Tags. 1073 * @param string|string[] $value Value to translate. An array for Tags header, string otherwise. 1074 * @return string|string[] Translated value. An array for Tags header, string otherwise. 1075 * 1076 * @phpstan-return ( $value is string ? string : string[] ) 1077 */ 1078 private function translate_header( $header, $value ) { 1079 switch ( $header ) { 1080 case 'Name': 1081 // Cached for sorting reasons. 1082 if ( isset( $this->name_translated ) ) { 1083 return $this->name_translated; 1084 } 1085 1086 // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain 1087 $this->name_translated = translate( $value, $this->get( 'TextDomain' ) ); 1088 1089 return $this->name_translated; 1090 case 'Tags': 1091 if ( empty( $value ) || ! function_exists( 'get_theme_feature_list' ) ) { 1092 return $value; 1093 } 1094 1095 static $tags_list; 1096 if ( ! isset( $tags_list ) ) { 1097 $tags_list = array( 1098 // As of 4.6, deprecated tags which are only used to provide translation for older themes. 1099 'black' => __( 'Black' ), 1100 'blue' => __( 'Blue' ), 1101 'brown' => __( 'Brown' ), 1102 'gray' => __( 'Gray' ), 1103 'green' => __( 'Green' ), 1104 'orange' => __( 'Orange' ), 1105 'pink' => __( 'Pink' ), 1106 'purple' => __( 'Purple' ), 1107 'red' => __( 'Red' ), 1108 'silver' => __( 'Silver' ), 1109 'tan' => __( 'Tan' ), 1110 'white' => __( 'White' ), 1111 'yellow' => __( 'Yellow' ), 1112 'dark' => _x( 'Dark', 'color scheme' ), 1113 'light' => _x( 'Light', 'color scheme' ), 1114 'fixed-layout' => __( 'Fixed Layout' ), 1115 'fluid-layout' => __( 'Fluid Layout' ), 1116 'responsive-layout' => __( 'Responsive Layout' ), 1117 'blavatar' => __( 'Blavatar' ), 1118 'photoblogging' => __( 'Photoblogging' ), 1119 'seasonal' => __( 'Seasonal' ), 1120 ); 1121 1122 $feature_list = get_theme_feature_list( false ); // No API. 1123 1124 foreach ( $feature_list as $tags ) { 1125 $tags_list += $tags; 1126 } 1127 } 1128 1129 foreach ( $value as &$tag ) { 1130 if ( isset( $tags_list[ $tag ] ) ) { 1131 $tag = $tags_list[ $tag ]; 1132 } elseif ( isset( self::$tag_map[ $tag ] ) ) { 1133 $tag = $tags_list[ self::$tag_map[ $tag ] ]; 1134 } 1135 } 1136 1137 return $value; 1138 1139 default: 1140 // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain 1141 $value = translate( $value, $this->get( 'TextDomain' ) ); 1142 } 1143 return $value; 1144 } 1145 1146 /** 1147 * Returns the directory name of the theme's "stylesheet" files, inside the theme root. 1148 * 1149 * In the case of a child theme, this is directory name of the child theme. 1150 * Otherwise, get_stylesheet() is the same as get_template(). 1151 * 1152 * @since 3.4.0 1153 * 1154 * @return string Stylesheet 1155 */ 1156 public function get_stylesheet() { 1157 return $this->stylesheet; 1158 } 1159 1160 /** 1161 * Returns the directory name of the theme's "template" files, inside the theme root. 1162 * 1163 * In the case of a child theme, this is the directory name of the parent theme. 1164 * Otherwise, the get_template() is the same as get_stylesheet(). 1165 * 1166 * @since 3.4.0 1167 * 1168 * @return string Template 1169 */ 1170 public function get_template() { 1171 return $this->template; 1172 } 1173 1174 /** 1175 * Returns the absolute path to the directory of a theme's "stylesheet" files. 1176 * 1177 * In the case of a child theme, this is the absolute path to the directory 1178 * of the child theme's files. 1179 * 1180 * @since 3.4.0 1181 * 1182 * @return string Absolute path of the stylesheet directory. 1183 */ 1184 public function get_stylesheet_directory() { 1185 if ( $this->errors() && in_array( 'theme_root_missing', $this->errors()->get_error_codes(), true ) ) { 1186 return ''; 1187 } 1188 1189 return $this->theme_root . '/' . $this->stylesheet; 1190 } 1191 1192 /** 1193 * Returns the absolute path to the directory of a theme's "template" files. 1194 * 1195 * In the case of a child theme, this is the absolute path to the directory 1196 * of the parent theme's files. 1197 * 1198 * @since 3.4.0 1199 * 1200 * @return string Absolute path of the template directory. 1201 */ 1202 public function get_template_directory() { 1203 if ( $this->parent() ) { 1204 $theme_root = $this->parent()->theme_root; 1205 } else { 1206 $theme_root = $this->theme_root; 1207 } 1208 1209 return $theme_root . '/' . $this->template; 1210 } 1211 1212 /** 1213 * Returns the URL to the directory of a theme's "stylesheet" files. 1214 * 1215 * In the case of a child theme, this is the URL to the directory of the 1216 * child theme's files. 1217 * 1218 * @since 3.4.0 1219 * 1220 * @return string URL to the stylesheet directory. 1221 */ 1222 public function get_stylesheet_directory_uri() { 1223 return $this->get_theme_root_uri() . '/' . str_replace( '%2F', '/', rawurlencode( $this->stylesheet ) ); 1224 } 1225 1226 /** 1227 * Returns the URL to the directory of a theme's "template" files. 1228 * 1229 * In the case of a child theme, this is the URL to the directory of the 1230 * parent theme's files. 1231 * 1232 * @since 3.4.0 1233 * 1234 * @return string URL to the template directory. 1235 */ 1236 public function get_template_directory_uri() { 1237 if ( $this->parent() ) { 1238 $theme_root_uri = $this->parent()->get_theme_root_uri(); 1239 } else { 1240 $theme_root_uri = $this->get_theme_root_uri(); 1241 } 1242 1243 return $theme_root_uri . '/' . str_replace( '%2F', '/', rawurlencode( $this->template ) ); 1244 } 1245 1246 /** 1247 * Returns the absolute path to the directory of the theme root. 1248 * 1249 * This is typically the absolute path to wp-content/themes. 1250 * 1251 * @since 3.4.0 1252 * 1253 * @return string Theme root. 1254 */ 1255 public function get_theme_root() { 1256 return $this->theme_root; 1257 } 1258 1259 /** 1260 * Returns the URL to the directory of the theme root. 1261 * 1262 * This is typically the absolute URL to wp-content/themes. This forms the basis 1263 * for all other URLs returned by WP_Theme, so we pass it to the public function 1264 * get_theme_root_uri() and allow it to run the {@see 'theme_root_uri'} filter. 1265 * 1266 * @since 3.4.0 1267 * 1268 * @return string Theme root URI. 1269 */ 1270 public function get_theme_root_uri() { 1271 if ( ! isset( $this->theme_root_uri ) ) { 1272 $this->theme_root_uri = get_theme_root_uri( $this->stylesheet, $this->theme_root ); 1273 } 1274 return $this->theme_root_uri; 1275 } 1276 1277 /** 1278 * Returns the main screenshot file for the theme. 1279 * 1280 * The main screenshot is called screenshot.png. gif and jpg extensions are also allowed. 1281 * 1282 * Screenshots for a theme must be in the stylesheet directory. (In the case of child 1283 * themes, parent theme screenshots are not inherited.) 1284 * 1285 * @since 3.4.0 1286 * 1287 * @param string $uri Type of URL to return, either 'relative' or an absolute URI. Defaults to absolute URI. 1288 * @return string|false Screenshot file. False if the theme does not have a screenshot. 1289 */ 1290 public function get_screenshot( $uri = 'uri' ) { 1291 $screenshot = $this->cache_get( 'screenshot' ); 1292 if ( $screenshot ) { 1293 if ( 'relative' === $uri ) { 1294 return $screenshot; 1295 } 1296 return $this->get_stylesheet_directory_uri() . '/' . $screenshot; 1297 } elseif ( 0 === $screenshot ) { 1298 return false; 1299 } 1300 1301 foreach ( array( 'png', 'gif', 'jpg', 'jpeg', 'webp', 'avif' ) as $ext ) { 1302 if ( file_exists( $this->get_stylesheet_directory() . "/screenshot.$ext" ) ) { 1303 $this->cache_add( 'screenshot', 'screenshot.' . $ext ); 1304 if ( 'relative' === $uri ) { 1305 return 'screenshot.' . $ext; 1306 } 1307 return $this->get_stylesheet_directory_uri() . '/' . 'screenshot.' . $ext; 1308 } 1309 } 1310 1311 $this->cache_add( 'screenshot', 0 ); 1312 return false; 1313 } 1314 1315 /** 1316 * Returns files in the theme's directory. 1317 * 1318 * @since 3.4.0 1319 * 1320 * @param string[]|string $type Optional. Array of extensions to find, string of a single extension, 1321 * or null for all extensions. Default null. 1322 * @param int $depth Optional. How deep to search for files. Defaults to a flat scan (0 depth). 1323 * -1 depth is infinite. 1324 * @param bool $search_parent Optional. Whether to return parent files. Default false. 1325 * @return string[] Array of files, keyed by the path to the file relative to the theme's directory, with the values 1326 * being absolute paths. 1327 */ 1328 public function get_files( $type = null, $depth = 0, $search_parent = false ) { 1329 $files = (array) self::scandir( $this->get_stylesheet_directory(), $type, $depth ); 1330 1331 if ( $search_parent && $this->parent() ) { 1332 $files += (array) self::scandir( $this->get_template_directory(), $type, $depth ); 1333 } 1334 1335 return array_filter( $files ); 1336 } 1337 1338 /** 1339 * Returns the theme's post templates. 1340 * 1341 * @since 4.7.0 1342 * @since 5.8.0 Include block templates. 1343 * 1344 * @return array[] Array of page template arrays, keyed by post type and filename, 1345 * with the value of the translated header name. 1346 */ 1347 public function get_post_templates() { 1348 // If you screw up your active theme and we invalidate your parent, most things still work. Let it slide. 1349 if ( $this->errors() && $this->errors()->get_error_codes() !== array( 'theme_parent_invalid' ) ) { 1350 return array(); 1351 } 1352 1353 $post_templates = $this->cache_get( 'post_templates' ); 1354 1355 if ( ! is_array( $post_templates ) ) { 1356 $post_templates = array(); 1357 1358 $files = (array) $this->get_files( 'php', 1, true ); 1359 1360 foreach ( $files as $file => $full_path ) { 1361 $headers = get_file_data( 1362 $full_path, 1363 array( 1364 'TemplateName' => 'Template Name', 1365 'TemplatePostType' => 'Template Post Type', 1366 ), 1367 'theme' 1368 ); 1369 1370 if ( ! $headers['TemplateName'] ) { 1371 continue; 1372 } 1373 1374 $types = array( 'page' ); 1375 if ( $headers['TemplatePostType'] ) { 1376 $types = explode( ',', $headers['TemplatePostType'] ); 1377 } 1378 1379 foreach ( $types as $type ) { 1380 $type = sanitize_key( $type ); 1381 if ( ! isset( $post_templates[ $type ] ) ) { 1382 $post_templates[ $type ] = array(); 1383 } 1384 1385 $post_templates[ $type ][ $file ] = $headers['TemplateName']; 1386 } 1387 } 1388 1389 $this->cache_add( 'post_templates', $post_templates ); 1390 } 1391 1392 if ( current_theme_supports( 'block-templates' ) ) { 1393 $block_templates = get_block_templates( array(), 'wp_template' ); 1394 foreach ( get_post_types( array( 'public' => true ) ) as $type ) { 1395 foreach ( $block_templates as $block_template ) { 1396 if ( ! $block_template->is_custom ) { 1397 continue; 1398 } 1399 1400 if ( isset( $block_template->post_types ) && ! in_array( $type, $block_template->post_types, true ) ) { 1401 continue; 1402 } 1403 1404 $post_templates[ $type ][ $block_template->slug ] = $block_template->title; 1405 } 1406 } 1407 } 1408 1409 if ( $this->load_textdomain() ) { 1410 foreach ( $post_templates as &$post_type ) { 1411 foreach ( $post_type as &$post_template ) { 1412 $post_template = $this->translate_header( 'Template Name', $post_template ); 1413 } 1414 } 1415 } 1416 1417 return $post_templates; 1418 } 1419 1420 /** 1421 * Returns the theme's post templates for a given post type. 1422 * 1423 * @since 3.4.0 1424 * @since 4.7.0 Added the `$post_type` parameter. 1425 * 1426 * @param WP_Post|null $post Optional. The post being edited, provided for context. 1427 * @param string $post_type Optional. Post type to get the templates for. Default 'page'. 1428 * If a post is provided, its post type is used. 1429 * @return string[] Array of template header names keyed by the template file name. 1430 */ 1431 public function get_page_templates( $post = null, $post_type = 'page' ) { 1432 if ( $post ) { 1433 $post_type = get_post_type( $post ); 1434 } 1435 1436 $post_templates = $this->get_post_templates(); 1437 $post_templates = $post_templates[ $post_type ] ?? array(); 1438 1439 /** 1440 * Filters list of page templates for a theme. 1441 * 1442 * @since 4.9.6 1443 * 1444 * @param string[] $post_templates Array of template header names keyed by the template file name. 1445 * @param WP_Theme $theme The theme object. 1446 * @param WP_Post|null $post The post being edited, provided for context, or null. 1447 * @param string $post_type Post type to get the templates for. 1448 */ 1449 $post_templates = (array) apply_filters( 'theme_templates', $post_templates, $this, $post, $post_type ); 1450 1451 /** 1452 * Filters list of page templates for a theme. 1453 * 1454 * The dynamic portion of the hook name, `$post_type`, refers to the post type. 1455 * 1456 * Possible hook names include: 1457 * 1458 * - `theme_post_templates` 1459 * - `theme_page_templates` 1460 * - `theme_attachment_templates` 1461 * 1462 * @since 3.9.0 1463 * @since 4.4.0 Converted to allow complete control over the `$page_templates` array. 1464 * @since 4.7.0 Added the `$post_type` parameter. 1465 * 1466 * @param string[] $post_templates Array of template header names keyed by the template file name. 1467 * @param WP_Theme $theme The theme object. 1468 * @param WP_Post|null $post The post being edited, provided for context, or null. 1469 * @param string $post_type Post type to get the templates for. 1470 */ 1471 $post_templates = (array) apply_filters( "theme_{$post_type}_templates", $post_templates, $this, $post, $post_type ); 1472 1473 return $post_templates; 1474 } 1475 1476 /** 1477 * Scans a directory for files of a certain extension. 1478 * 1479 * @since 3.4.0 1480 * 1481 * @param string $path Absolute path to search. 1482 * @param array|string|null $extensions Optional. Array of extensions to find, string of a single extension, 1483 * or null for all extensions. Default null. 1484 * @param int $depth Optional. How many levels deep to search for files. Accepts 0, 1+, or 1485 * -1 (infinite depth). Default 0. 1486 * @param string $relative_path Optional. The basename of the absolute path. Used to control the 1487 * returned path for the found files, particularly when this function 1488 * recurses to lower depths. Default empty. 1489 * @return string[]|false Array of files, keyed by the path to the file relative to the `$path` directory prepended 1490 * with `$relative_path`, with the values being absolute paths. False otherwise. 1491 */ 1492 private static function scandir( $path, $extensions = null, $depth = 0, $relative_path = '' ) { 1493 if ( ! is_dir( $path ) ) { 1494 return false; 1495 } 1496 1497 if ( $extensions ) { 1498 $extensions = (array) $extensions; 1499 $_extensions = implode( '|', $extensions ); 1500 } 1501 1502 $relative_path = trailingslashit( $relative_path ); 1503 if ( '/' === $relative_path ) { 1504 $relative_path = ''; 1505 } 1506 1507 $results = scandir( $path ); 1508 $files = array(); 1509 1510 /** 1511 * Filters the array of excluded directories and files while scanning theme folder. 1512 * 1513 * @since 4.7.4 1514 * 1515 * @param string[] $exclusions Array of excluded directories and files. 1516 */ 1517 $exclusions = (array) apply_filters( 'theme_scandir_exclusions', array( 'CVS', 'node_modules', 'vendor', 'bower_components' ) ); 1518 1519 foreach ( $results as $result ) { 1520 if ( '.' === $result[0] || in_array( $result, $exclusions, true ) ) { 1521 continue; 1522 } 1523 if ( is_dir( $path . '/' . $result ) ) { 1524 if ( ! $depth ) { 1525 continue; 1526 } 1527 $found = self::scandir( $path . '/' . $result, $extensions, $depth - 1, $relative_path . $result ); 1528 $files = array_merge_recursive( $files, $found ); 1529 } elseif ( ! $extensions || preg_match( '~\.(' . $_extensions . ')$~', $result ) ) { 1530 $files[ $relative_path . $result ] = $path . '/' . $result; 1531 } 1532 } 1533 1534 return $files; 1535 } 1536 1537 /** 1538 * Loads the theme's textdomain. 1539 * 1540 * Translation files are not inherited from the parent theme. TODO: If this fails for the 1541 * child theme, it should probably try to load the parent theme's translations. 1542 * 1543 * @since 3.4.0 1544 * 1545 * @return bool True if the textdomain was successfully loaded or has already been loaded. 1546 * False if no textdomain was specified in the file headers, or if the domain could not be loaded. 1547 */ 1548 public function load_textdomain() { 1549 if ( isset( $this->textdomain_loaded ) ) { 1550 return $this->textdomain_loaded; 1551 } 1552 1553 $textdomain = $this->get( 'TextDomain' ); 1554 if ( ! $textdomain ) { 1555 $this->textdomain_loaded = false; 1556 return false; 1557 } 1558 1559 if ( is_textdomain_loaded( $textdomain ) ) { 1560 $this->textdomain_loaded = true; 1561 return true; 1562 } 1563 1564 $path = $this->get_stylesheet_directory(); 1565 $domainpath = $this->get( 'DomainPath' ); 1566 if ( $domainpath ) { 1567 $path .= $domainpath; 1568 } else { 1569 $path .= '/languages'; 1570 } 1571 1572 $this->textdomain_loaded = load_theme_textdomain( $textdomain, $path ); 1573 return $this->textdomain_loaded; 1574 } 1575 1576 /** 1577 * Determines whether the theme is allowed (multisite only). 1578 * 1579 * @since 3.4.0 1580 * 1581 * @param string $check Optional. Whether to check only the 'network'-wide settings, the 'site' 1582 * settings, or 'both'. Defaults to 'both'. 1583 * @param int $blog_id Optional. Ignored if only network-wide settings are checked. Defaults to current site. 1584 * @return bool Whether the theme is allowed for the network. Returns true in single-site. 1585 */ 1586 public function is_allowed( $check = 'both', $blog_id = null ) { 1587 if ( ! is_multisite() ) { 1588 return true; 1589 } 1590 1591 if ( 'both' === $check || 'network' === $check ) { 1592 $allowed = self::get_allowed_on_network(); 1593 if ( ! empty( $allowed[ $this->get_stylesheet() ] ) ) { 1594 return true; 1595 } 1596 } 1597 1598 if ( 'both' === $check || 'site' === $check ) { 1599 $allowed = self::get_allowed_on_site( $blog_id ); 1600 if ( ! empty( $allowed[ $this->get_stylesheet() ] ) ) { 1601 return true; 1602 } 1603 } 1604 1605 return false; 1606 } 1607 1608 /** 1609 * Returns whether this theme is a block-based theme or not. 1610 * 1611 * @since 5.9.0 1612 * 1613 * @return bool 1614 */ 1615 public function is_block_theme() { 1616 if ( isset( $this->block_theme ) ) { 1617 return $this->block_theme; 1618 } 1619 1620 $paths_to_index_block_template = array( 1621 $this->get_file_path( '/templates/index.html' ), 1622 $this->get_file_path( '/block-templates/index.html' ), 1623 ); 1624 1625 $this->block_theme = false; 1626 1627 foreach ( $paths_to_index_block_template as $path_to_index_block_template ) { 1628 if ( is_file( $path_to_index_block_template ) && is_readable( $path_to_index_block_template ) ) { 1629 $this->block_theme = true; 1630 break; 1631 } 1632 } 1633 1634 return $this->block_theme; 1635 } 1636 1637 /** 1638 * Retrieves the path of a file in the theme. 1639 * 1640 * Searches in the stylesheet directory before the template directory so themes 1641 * which inherit from a parent theme can just override one file. 1642 * 1643 * @since 5.9.0 1644 * 1645 * @param string $file Optional. File to search for in the stylesheet directory. 1646 * @return string The path of the file. 1647 */ 1648 public function get_file_path( $file = '' ) { 1649 $file = ltrim( $file, '/' ); 1650 1651 $stylesheet_directory = $this->get_stylesheet_directory(); 1652 $template_directory = $this->get_template_directory(); 1653 1654 if ( empty( $file ) ) { 1655 $path = $stylesheet_directory; 1656 } elseif ( $stylesheet_directory !== $template_directory && file_exists( $stylesheet_directory . '/' . $file ) ) { 1657 $path = $stylesheet_directory . '/' . $file; 1658 } else { 1659 $path = $template_directory . '/' . $file; 1660 } 1661 1662 /** This filter is documented in wp-includes/link-template.php */ 1663 return apply_filters( 'theme_file_path', $path, $file ); 1664 } 1665 1666 /** 1667 * Determines the latest WordPress default theme that is installed. 1668 * 1669 * This hits the filesystem. 1670 * 1671 * @since 4.4.0 1672 * 1673 * @return WP_Theme|false Object, or false if no theme is installed, which would be bad. 1674 */ 1675 public static function get_core_default_theme() { 1676 foreach ( array_reverse( self::$default_themes ) as $slug => $name ) { 1677 $theme = wp_get_theme( $slug ); 1678 if ( $theme->exists() ) { 1679 return $theme; 1680 } 1681 } 1682 return false; 1683 } 1684 1685 /** 1686 * Returns array of stylesheet names of themes allowed on the site or network. 1687 * 1688 * @since 3.4.0 1689 * 1690 * @param int $blog_id Optional. ID of the site. Defaults to the current site. 1691 * @return string[] Array of stylesheet names. 1692 */ 1693 public static function get_allowed( $blog_id = null ) { 1694 /** 1695 * Filters the array of themes allowed on the network. 1696 * 1697 * Site is provided as context so that a list of network allowed themes can 1698 * be filtered further. 1699 * 1700 * @since 4.5.0 1701 * 1702 * @param string[] $allowed_themes An array of theme stylesheet names. 1703 * @param int $blog_id ID of the site. 1704 */ 1705 $network = (array) apply_filters( 'network_allowed_themes', self::get_allowed_on_network(), $blog_id ); 1706 return $network + self::get_allowed_on_site( $blog_id ); 1707 } 1708 1709 /** 1710 * Returns array of stylesheet names of themes allowed on the network. 1711 * 1712 * @since 3.4.0 1713 * 1714 * @return string[] Array of stylesheet names. 1715 */ 1716 public static function get_allowed_on_network() { 1717 static $allowed_themes; 1718 if ( ! isset( $allowed_themes ) ) { 1719 $allowed_themes = (array) get_site_option( 'allowedthemes' ); 1720 } 1721 1722 /** 1723 * Filters the array of themes allowed on the network. 1724 * 1725 * @since MU (3.0.0) 1726 * 1727 * @param string[] $allowed_themes An array of theme stylesheet names. 1728 */ 1729 $allowed_themes = apply_filters( 'allowed_themes', $allowed_themes ); 1730 1731 return $allowed_themes; 1732 } 1733 1734 /** 1735 * Returns array of stylesheet names of themes allowed on the site. 1736 * 1737 * @since 3.4.0 1738 * 1739 * @param int $blog_id Optional. ID of the site. Defaults to the current site. 1740 * @return string[] Array of stylesheet names. 1741 */ 1742 public static function get_allowed_on_site( $blog_id = null ) { 1743 static $allowed_themes = array(); 1744 1745 if ( ! $blog_id || ! is_multisite() ) { 1746 $blog_id = get_current_blog_id(); 1747 } 1748 1749 if ( isset( $allowed_themes[ $blog_id ] ) ) { 1750 /** 1751 * Filters the array of themes allowed on the site. 1752 * 1753 * @since 4.5.0 1754 * 1755 * @param string[] $allowed_themes An array of theme stylesheet names. 1756 * @param int $blog_id ID of the site. Defaults to current site. 1757 */ 1758 return (array) apply_filters( 'site_allowed_themes', $allowed_themes[ $blog_id ], $blog_id ); 1759 } 1760 1761 $current = get_current_blog_id() === $blog_id; 1762 1763 if ( $current ) { 1764 $allowed_themes[ $blog_id ] = get_option( 'allowedthemes' ); 1765 } else { 1766 switch_to_blog( $blog_id ); 1767 $allowed_themes[ $blog_id ] = get_option( 'allowedthemes' ); 1768 restore_current_blog(); 1769 } 1770 1771 /* 1772 * This is all super old MU back compat joy. 1773 * 'allowedthemes' keys things by stylesheet. 'allowed_themes' keyed things by name. 1774 */ 1775 if ( false === $allowed_themes[ $blog_id ] ) { 1776 if ( $current ) { 1777 $allowed_themes[ $blog_id ] = get_option( 'allowed_themes' ); 1778 } else { 1779 switch_to_blog( $blog_id ); 1780 $allowed_themes[ $blog_id ] = get_option( 'allowed_themes' ); 1781 restore_current_blog(); 1782 } 1783 1784 if ( ! is_array( $allowed_themes[ $blog_id ] ) || empty( $allowed_themes[ $blog_id ] ) ) { 1785 $allowed_themes[ $blog_id ] = array(); 1786 } else { 1787 $converted = array(); 1788 $themes = wp_get_themes(); 1789 foreach ( $themes as $stylesheet => $theme_data ) { 1790 if ( isset( $allowed_themes[ $blog_id ][ $theme_data->get( 'Name' ) ] ) ) { 1791 $converted[ $stylesheet ] = true; 1792 } 1793 } 1794 $allowed_themes[ $blog_id ] = $converted; 1795 } 1796 // Set the option so we never have to go through this pain again. 1797 if ( is_admin() && $allowed_themes[ $blog_id ] ) { 1798 if ( $current ) { 1799 update_option( 'allowedthemes', $allowed_themes[ $blog_id ], false ); 1800 delete_option( 'allowed_themes' ); 1801 } else { 1802 switch_to_blog( $blog_id ); 1803 update_option( 'allowedthemes', $allowed_themes[ $blog_id ], false ); 1804 delete_option( 'allowed_themes' ); 1805 restore_current_blog(); 1806 } 1807 } 1808 } 1809 1810 /** This filter is documented in wp-includes/class-wp-theme.php */ 1811 return (array) apply_filters( 'site_allowed_themes', $allowed_themes[ $blog_id ], $blog_id ); 1812 } 1813 1814 /** 1815 * Returns the folder names of the block template directories. 1816 * 1817 * @since 6.4.0 1818 * 1819 * @return string[] { 1820 * Folder names used by block themes. 1821 * 1822 * @type string $wp_template Theme-relative directory name for block templates. 1823 * @type string $wp_template_part Theme-relative directory name for block template parts. 1824 * } 1825 */ 1826 public function get_block_template_folders() { 1827 // Return set/cached value if available. 1828 if ( isset( $this->block_template_folders ) ) { 1829 return $this->block_template_folders; 1830 } 1831 1832 $this->block_template_folders = $this->default_template_folders; 1833 1834 $stylesheet_directory = $this->get_stylesheet_directory(); 1835 // If the theme uses deprecated block template folders. 1836 if ( file_exists( $stylesheet_directory . '/block-templates' ) || file_exists( $stylesheet_directory . '/block-template-parts' ) ) { 1837 $this->block_template_folders = array( 1838 'wp_template' => 'block-templates', 1839 'wp_template_part' => 'block-template-parts', 1840 ); 1841 } 1842 return $this->block_template_folders; 1843 } 1844 1845 /** 1846 * Gets block pattern data for a specified theme. 1847 * Each pattern is defined as a PHP file and defines 1848 * its metadata using plugin-style headers. The minimum required definition is: 1849 * 1850 * /** 1851 * * Title: My Pattern 1852 * * Slug: my-theme/my-pattern 1853 * * 1854 * 1855 * The output of the PHP source corresponds to the content of the pattern, e.g.: 1856 * 1857 * <main><p><?php echo "Hello"; ?></p></main> 1858 * 1859 * If applicable, this will collect from both parent and child theme. 1860 * 1861 * Other settable fields include: 1862 * 1863 * - Description 1864 * - Viewport Width 1865 * - Inserter (yes/no) 1866 * - Categories (comma-separated values) 1867 * - Keywords (comma-separated values) 1868 * - Block Types (comma-separated values) 1869 * - Post Types (comma-separated values) 1870 * - Template Types (comma-separated values) 1871 * 1872 * @since 6.4.0 1873 * 1874 * @return array Block pattern data. 1875 */ 1876 public function get_block_patterns() { 1877 $can_use_cached = ! wp_is_development_mode( 'theme' ); 1878 1879 $pattern_data = $this->get_pattern_cache(); 1880 if ( is_array( $pattern_data ) ) { 1881 if ( $can_use_cached ) { 1882 return $pattern_data; 1883 } 1884 // If in development mode, clear pattern cache. 1885 $this->delete_pattern_cache(); 1886 } 1887 1888 $dirpath = $this->get_stylesheet_directory() . '/patterns'; 1889 $pattern_data = array(); 1890 1891 if ( ! file_exists( $dirpath ) ) { 1892 if ( $can_use_cached ) { 1893 $this->set_pattern_cache( $pattern_data ); 1894 } 1895 return $pattern_data; 1896 } 1897 1898 $files = (array) self::scandir( $dirpath, 'php', -1 ); 1899 1900 /** 1901 * Filters list of block pattern files for a theme. 1902 * 1903 * @since 6.8.0 1904 * 1905 * @param array $files Array of theme files found within `patterns` directory. 1906 * @param string $dirpath Path of theme `patterns` directory being scanned. 1907 */ 1908 $files = apply_filters( 'theme_block_pattern_files', $files, $dirpath ); 1909 1910 $dirpath = trailingslashit( $dirpath ); 1911 1912 if ( ! $files ) { 1913 if ( $can_use_cached ) { 1914 $this->set_pattern_cache( $pattern_data ); 1915 } 1916 return $pattern_data; 1917 } 1918 1919 $default_headers = array( 1920 'title' => 'Title', 1921 'slug' => 'Slug', 1922 'description' => 'Description', 1923 'viewportWidth' => 'Viewport Width', 1924 'inserter' => 'Inserter', 1925 'categories' => 'Categories', 1926 'keywords' => 'Keywords', 1927 'blockTypes' => 'Block Types', 1928 'postTypes' => 'Post Types', 1929 'templateTypes' => 'Template Types', 1930 ); 1931 1932 $properties_to_parse = array( 1933 'categories', 1934 'keywords', 1935 'blockTypes', 1936 'postTypes', 1937 'templateTypes', 1938 ); 1939 1940 foreach ( $files as $file ) { 1941 $pattern = get_file_data( $file, $default_headers ); 1942 1943 if ( empty( $pattern['slug'] ) ) { 1944 _doing_it_wrong( 1945 __FUNCTION__, 1946 sprintf( 1947 /* translators: 1: file name. */ 1948 __( 'Could not register file "%s" as a block pattern ("Slug" field missing)' ), 1949 $file 1950 ), 1951 '6.0.0' 1952 ); 1953 continue; 1954 } 1955 1956 if ( ! preg_match( '/^[A-z0-9\/_-]+$/', $pattern['slug'] ) ) { 1957 _doing_it_wrong( 1958 __FUNCTION__, 1959 sprintf( 1960 /* translators: 1: file name; 2: slug value found. */ 1961 __( 'Could not register file "%1$s" as a block pattern (invalid slug "%2$s")' ), 1962 $file, 1963 $pattern['slug'] 1964 ), 1965 '6.0.0' 1966 ); 1967 } 1968 1969 // Title is a required property. 1970 if ( ! $pattern['title'] ) { 1971 _doing_it_wrong( 1972 __FUNCTION__, 1973 sprintf( 1974 /* translators: 1: file name. */ 1975 __( 'Could not register file "%s" as a block pattern ("Title" field missing)' ), 1976 $file 1977 ), 1978 '6.0.0' 1979 ); 1980 continue; 1981 } 1982 1983 // For properties of type array, parse data as comma-separated. 1984 foreach ( $properties_to_parse as $property ) { 1985 if ( ! empty( $pattern[ $property ] ) ) { 1986 $pattern[ $property ] = array_filter( wp_parse_list( (string) $pattern[ $property ] ) ); 1987 } else { 1988 unset( $pattern[ $property ] ); 1989 } 1990 } 1991 1992 // Parse properties of type int. 1993 $property = 'viewportWidth'; 1994 if ( ! empty( $pattern[ $property ] ) ) { 1995 $pattern[ $property ] = (int) $pattern[ $property ]; 1996 } else { 1997 unset( $pattern[ $property ] ); 1998 } 1999 2000 // Parse properties of type bool. 2001 $property = 'inserter'; 2002 if ( ! empty( $pattern[ $property ] ) ) { 2003 $pattern[ $property ] = in_array( 2004 strtolower( $pattern[ $property ] ), 2005 array( 'yes', 'true' ), 2006 true 2007 ); 2008 } else { 2009 unset( $pattern[ $property ] ); 2010 } 2011 2012 $key = str_replace( $dirpath, '', $file ); 2013 2014 $pattern_data[ $key ] = $pattern; 2015 } 2016 2017 if ( $can_use_cached ) { 2018 $this->set_pattern_cache( $pattern_data ); 2019 } 2020 2021 return $pattern_data; 2022 } 2023 2024 /** 2025 * Gets block pattern cache. 2026 * 2027 * @since 6.4.0 2028 * @since 6.6.0 Uses transients to cache regardless of site environment. 2029 * 2030 * @return array|false Returns an array of patterns if cache is found, otherwise false. 2031 */ 2032 private function get_pattern_cache() { 2033 if ( ! $this->exists() ) { 2034 return false; 2035 } 2036 2037 $pattern_data = get_site_transient( 'wp_theme_files_patterns-' . $this->cache_hash ); 2038 2039 if ( is_array( $pattern_data ) && $pattern_data['version'] === $this->get( 'Version' ) ) { 2040 return $pattern_data['patterns']; 2041 } 2042 return false; 2043 } 2044 2045 /** 2046 * Sets block pattern cache. 2047 * 2048 * @since 6.4.0 2049 * @since 6.6.0 Uses transients to cache regardless of site environment. 2050 * 2051 * @param array $patterns Block patterns data to set in cache. 2052 */ 2053 private function set_pattern_cache( array $patterns ) { 2054 $pattern_data = array( 2055 'version' => $this->get( 'Version' ), 2056 'patterns' => $patterns, 2057 ); 2058 2059 /** 2060 * Filters the cache expiration time for theme files. 2061 * 2062 * @since 6.6.0 2063 * 2064 * @param int $cache_expiration Cache expiration time in seconds. 2065 * @param string $cache_type Type of cache being set. 2066 */ 2067 $cache_expiration = (int) apply_filters( 'wp_theme_files_cache_ttl', self::$cache_expiration, 'theme_block_patterns' ); 2068 2069 // We don't want to cache patterns infinitely. 2070 if ( $cache_expiration <= 0 ) { 2071 _doing_it_wrong( 2072 __METHOD__, 2073 sprintf( 2074 /* translators: %1$s: The filter name.*/ 2075 __( 'The %1$s filter must return an integer value greater than 0.' ), 2076 '<code>wp_theme_files_cache_ttl</code>' 2077 ), 2078 '6.6.0' 2079 ); 2080 2081 $cache_expiration = self::$cache_expiration; 2082 } 2083 2084 set_site_transient( 'wp_theme_files_patterns-' . $this->cache_hash, $pattern_data, $cache_expiration ); 2085 } 2086 2087 /** 2088 * Clears block pattern cache. 2089 * 2090 * @since 6.4.0 2091 * @since 6.6.0 Uses transients to cache regardless of site environment. 2092 */ 2093 public function delete_pattern_cache() { 2094 delete_site_transient( 'wp_theme_files_patterns-' . $this->cache_hash ); 2095 } 2096 2097 /** 2098 * Enables a theme for all sites on the current network. 2099 * 2100 * @since 4.6.0 2101 * 2102 * @param string|string[] $stylesheets Stylesheet name or array of stylesheet names. 2103 */ 2104 public static function network_enable_theme( $stylesheets ) { 2105 if ( ! is_multisite() ) { 2106 return; 2107 } 2108 2109 if ( ! is_array( $stylesheets ) ) { 2110 $stylesheets = array( $stylesheets ); 2111 } 2112 2113 $allowed_themes = get_site_option( 'allowedthemes' ); 2114 foreach ( $stylesheets as $stylesheet ) { 2115 $allowed_themes[ $stylesheet ] = true; 2116 } 2117 2118 update_site_option( 'allowedthemes', $allowed_themes ); 2119 } 2120 2121 /** 2122 * Disables a theme for all sites on the current network. 2123 * 2124 * @since 4.6.0 2125 * 2126 * @param string|string[] $stylesheets Stylesheet name or array of stylesheet names. 2127 */ 2128 public static function network_disable_theme( $stylesheets ) { 2129 if ( ! is_multisite() ) { 2130 return; 2131 } 2132 2133 if ( ! is_array( $stylesheets ) ) { 2134 $stylesheets = array( $stylesheets ); 2135 } 2136 2137 $allowed_themes = get_site_option( 'allowedthemes' ); 2138 foreach ( $stylesheets as $stylesheet ) { 2139 if ( isset( $allowed_themes[ $stylesheet ] ) ) { 2140 unset( $allowed_themes[ $stylesheet ] ); 2141 } 2142 } 2143 2144 update_site_option( 'allowedthemes', $allowed_themes ); 2145 } 2146 2147 /** 2148 * Sorts themes by name. 2149 * 2150 * @since 3.4.0 2151 * 2152 * @param WP_Theme[] $themes Array of theme objects to sort (passed by reference). 2153 */ 2154 public static function sort_by_name( &$themes ) { 2155 if ( str_starts_with( get_user_locale(), 'en_' ) ) { 2156 uasort( $themes, array( 'WP_Theme', '_name_sort' ) ); 2157 } else { 2158 foreach ( $themes as $key => $theme ) { 2159 $theme->translate_header( 'Name', $theme->headers['Name'] ); 2160 } 2161 uasort( $themes, array( 'WP_Theme', '_name_sort_i18n' ) ); 2162 } 2163 } 2164 2165 /** 2166 * Callback function for usort() to naturally sort themes by name. 2167 * 2168 * Accesses the Name header directly from the class for maximum speed. 2169 * Would choke on HTML but we don't care enough to slow it down with strip_tags(). 2170 * 2171 * @since 3.4.0 2172 * 2173 * @param WP_Theme $a First theme. 2174 * @param WP_Theme $b Second theme. 2175 * @return int Negative if `$a` falls lower in the natural order than `$b`. Zero if they fall equally. 2176 * Greater than 0 if `$a` falls higher in the natural order than `$b`. Used with usort(). 2177 */ 2178 private static function _name_sort( $a, $b ) { 2179 return strnatcasecmp( $a->headers['Name'], $b->headers['Name'] ); 2180 } 2181 2182 /** 2183 * Callback function for usort() to naturally sort themes by translated name. 2184 * 2185 * @since 3.4.0 2186 * 2187 * @param WP_Theme $a First theme. 2188 * @param WP_Theme $b Second theme. 2189 * @return int Negative if `$a` falls lower in the natural order than `$b`. Zero if they fall equally. 2190 * Greater than 0 if `$a` falls higher in the natural order than `$b`. Used with usort(). 2191 */ 2192 private static function _name_sort_i18n( $a, $b ) { 2193 return strnatcasecmp( $a->name_translated, $b->name_translated ); 2194 } 2195 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Tue Oct 13 08:20:33 2026 | Cross-referenced by PHPXref |