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