| [ 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 $headers = get_file_data( 1342 $full_path, 1343 array( 1344 'TemplateName' => 'Template Name', 1345 'TemplatePostType' => 'Template Post Type', 1346 ), 1347 'theme' 1348 ); 1349 1350 if ( ! $headers['TemplateName'] ) { 1351 continue; 1352 } 1353 1354 $types = array( 'page' ); 1355 if ( $headers['TemplatePostType'] ) { 1356 $types = explode( ',', $headers['TemplatePostType'] ); 1357 } 1358 1359 foreach ( $types as $type ) { 1360 $type = sanitize_key( $type ); 1361 if ( ! isset( $post_templates[ $type ] ) ) { 1362 $post_templates[ $type ] = array(); 1363 } 1364 1365 $post_templates[ $type ][ $file ] = $headers['TemplateName']; 1366 } 1367 } 1368 1369 $this->cache_add( 'post_templates', $post_templates ); 1370 } 1371 1372 if ( current_theme_supports( 'block-templates' ) ) { 1373 $block_templates = get_block_templates( array(), 'wp_template' ); 1374 foreach ( get_post_types( array( 'public' => true ) ) as $type ) { 1375 foreach ( $block_templates as $block_template ) { 1376 if ( ! $block_template->is_custom ) { 1377 continue; 1378 } 1379 1380 if ( isset( $block_template->post_types ) && ! in_array( $type, $block_template->post_types, true ) ) { 1381 continue; 1382 } 1383 1384 $post_templates[ $type ][ $block_template->slug ] = $block_template->title; 1385 } 1386 } 1387 } 1388 1389 if ( $this->load_textdomain() ) { 1390 foreach ( $post_templates as &$post_type ) { 1391 foreach ( $post_type as &$post_template ) { 1392 $post_template = $this->translate_header( 'Template Name', $post_template ); 1393 } 1394 } 1395 } 1396 1397 return $post_templates; 1398 } 1399 1400 /** 1401 * Returns the theme's post templates for a given post type. 1402 * 1403 * @since 3.4.0 1404 * @since 4.7.0 Added the `$post_type` parameter. 1405 * 1406 * @param WP_Post|null $post Optional. The post being edited, provided for context. 1407 * @param string $post_type Optional. Post type to get the templates for. Default 'page'. 1408 * If a post is provided, its post type is used. 1409 * @return string[] Array of template header names keyed by the template file name. 1410 */ 1411 public function get_page_templates( $post = null, $post_type = 'page' ) { 1412 if ( $post ) { 1413 $post_type = get_post_type( $post ); 1414 } 1415 1416 $post_templates = $this->get_post_templates(); 1417 $post_templates = $post_templates[ $post_type ] ?? array(); 1418 1419 /** 1420 * Filters list of page templates for a theme. 1421 * 1422 * @since 4.9.6 1423 * 1424 * @param string[] $post_templates Array of template header names keyed by the template file name. 1425 * @param WP_Theme $theme The theme object. 1426 * @param WP_Post|null $post The post being edited, provided for context, or null. 1427 * @param string $post_type Post type to get the templates for. 1428 */ 1429 $post_templates = (array) apply_filters( 'theme_templates', $post_templates, $this, $post, $post_type ); 1430 1431 /** 1432 * Filters list of page templates for a theme. 1433 * 1434 * The dynamic portion of the hook name, `$post_type`, refers to the post type. 1435 * 1436 * Possible hook names include: 1437 * 1438 * - `theme_post_templates` 1439 * - `theme_page_templates` 1440 * - `theme_attachment_templates` 1441 * 1442 * @since 3.9.0 1443 * @since 4.4.0 Converted to allow complete control over the `$page_templates` array. 1444 * @since 4.7.0 Added the `$post_type` parameter. 1445 * 1446 * @param string[] $post_templates Array of template header names keyed by the template file name. 1447 * @param WP_Theme $theme The theme object. 1448 * @param WP_Post|null $post The post being edited, provided for context, or null. 1449 * @param string $post_type Post type to get the templates for. 1450 */ 1451 $post_templates = (array) apply_filters( "theme_{$post_type}_templates", $post_templates, $this, $post, $post_type ); 1452 1453 return $post_templates; 1454 } 1455 1456 /** 1457 * Scans a directory for files of a certain extension. 1458 * 1459 * @since 3.4.0 1460 * 1461 * @param string $path Absolute path to search. 1462 * @param array|string|null $extensions Optional. Array of extensions to find, string of a single extension, 1463 * or null for all extensions. Default null. 1464 * @param int $depth Optional. How many levels deep to search for files. Accepts 0, 1+, or 1465 * -1 (infinite depth). Default 0. 1466 * @param string $relative_path Optional. The basename of the absolute path. Used to control the 1467 * returned path for the found files, particularly when this function 1468 * recurses to lower depths. Default empty. 1469 * @return string[]|false Array of files, keyed by the path to the file relative to the `$path` directory prepended 1470 * with `$relative_path`, with the values being absolute paths. False otherwise. 1471 */ 1472 private static function scandir( $path, $extensions = null, $depth = 0, $relative_path = '' ) { 1473 if ( ! is_dir( $path ) ) { 1474 return false; 1475 } 1476 1477 if ( $extensions ) { 1478 $extensions = (array) $extensions; 1479 $_extensions = implode( '|', $extensions ); 1480 } 1481 1482 $relative_path = trailingslashit( $relative_path ); 1483 if ( '/' === $relative_path ) { 1484 $relative_path = ''; 1485 } 1486 1487 $results = scandir( $path ); 1488 $files = array(); 1489 1490 /** 1491 * Filters the array of excluded directories and files while scanning theme folder. 1492 * 1493 * @since 4.7.4 1494 * 1495 * @param string[] $exclusions Array of excluded directories and files. 1496 */ 1497 $exclusions = (array) apply_filters( 'theme_scandir_exclusions', array( 'CVS', 'node_modules', 'vendor', 'bower_components' ) ); 1498 1499 foreach ( $results as $result ) { 1500 if ( '.' === $result[0] || in_array( $result, $exclusions, true ) ) { 1501 continue; 1502 } 1503 if ( is_dir( $path . '/' . $result ) ) { 1504 if ( ! $depth ) { 1505 continue; 1506 } 1507 $found = self::scandir( $path . '/' . $result, $extensions, $depth - 1, $relative_path . $result ); 1508 $files = array_merge_recursive( $files, $found ); 1509 } elseif ( ! $extensions || preg_match( '~\.(' . $_extensions . ')$~', $result ) ) { 1510 $files[ $relative_path . $result ] = $path . '/' . $result; 1511 } 1512 } 1513 1514 return $files; 1515 } 1516 1517 /** 1518 * Loads the theme's textdomain. 1519 * 1520 * Translation files are not inherited from the parent theme. TODO: If this fails for the 1521 * child theme, it should probably try to load the parent theme's translations. 1522 * 1523 * @since 3.4.0 1524 * 1525 * @return bool True if the textdomain was successfully loaded or has already been loaded. 1526 * False if no textdomain was specified in the file headers, or if the domain could not be loaded. 1527 */ 1528 public function load_textdomain() { 1529 if ( isset( $this->textdomain_loaded ) ) { 1530 return $this->textdomain_loaded; 1531 } 1532 1533 $textdomain = $this->get( 'TextDomain' ); 1534 if ( ! $textdomain ) { 1535 $this->textdomain_loaded = false; 1536 return false; 1537 } 1538 1539 if ( is_textdomain_loaded( $textdomain ) ) { 1540 $this->textdomain_loaded = true; 1541 return true; 1542 } 1543 1544 $path = $this->get_stylesheet_directory(); 1545 $domainpath = $this->get( 'DomainPath' ); 1546 if ( $domainpath ) { 1547 $path .= $domainpath; 1548 } else { 1549 $path .= '/languages'; 1550 } 1551 1552 $this->textdomain_loaded = load_theme_textdomain( $textdomain, $path ); 1553 return $this->textdomain_loaded; 1554 } 1555 1556 /** 1557 * Determines whether the theme is allowed (multisite only). 1558 * 1559 * @since 3.4.0 1560 * 1561 * @param string $check Optional. Whether to check only the 'network'-wide settings, the 'site' 1562 * settings, or 'both'. Defaults to 'both'. 1563 * @param int $blog_id Optional. Ignored if only network-wide settings are checked. Defaults to current site. 1564 * @return bool Whether the theme is allowed for the network. Returns true in single-site. 1565 */ 1566 public function is_allowed( $check = 'both', $blog_id = null ) { 1567 if ( ! is_multisite() ) { 1568 return true; 1569 } 1570 1571 if ( 'both' === $check || 'network' === $check ) { 1572 $allowed = self::get_allowed_on_network(); 1573 if ( ! empty( $allowed[ $this->get_stylesheet() ] ) ) { 1574 return true; 1575 } 1576 } 1577 1578 if ( 'both' === $check || 'site' === $check ) { 1579 $allowed = self::get_allowed_on_site( $blog_id ); 1580 if ( ! empty( $allowed[ $this->get_stylesheet() ] ) ) { 1581 return true; 1582 } 1583 } 1584 1585 return false; 1586 } 1587 1588 /** 1589 * Returns whether this theme is a block-based theme or not. 1590 * 1591 * @since 5.9.0 1592 * 1593 * @return bool 1594 */ 1595 public function is_block_theme() { 1596 if ( isset( $this->block_theme ) ) { 1597 return $this->block_theme; 1598 } 1599 1600 $paths_to_index_block_template = array( 1601 $this->get_file_path( '/templates/index.html' ), 1602 $this->get_file_path( '/block-templates/index.html' ), 1603 ); 1604 1605 $this->block_theme = false; 1606 1607 foreach ( $paths_to_index_block_template as $path_to_index_block_template ) { 1608 if ( is_file( $path_to_index_block_template ) && is_readable( $path_to_index_block_template ) ) { 1609 $this->block_theme = true; 1610 break; 1611 } 1612 } 1613 1614 return $this->block_theme; 1615 } 1616 1617 /** 1618 * Retrieves the path of a file in the theme. 1619 * 1620 * Searches in the stylesheet directory before the template directory so themes 1621 * which inherit from a parent theme can just override one file. 1622 * 1623 * @since 5.9.0 1624 * 1625 * @param string $file Optional. File to search for in the stylesheet directory. 1626 * @return string The path of the file. 1627 */ 1628 public function get_file_path( $file = '' ) { 1629 $file = ltrim( $file, '/' ); 1630 1631 $stylesheet_directory = $this->get_stylesheet_directory(); 1632 $template_directory = $this->get_template_directory(); 1633 1634 if ( empty( $file ) ) { 1635 $path = $stylesheet_directory; 1636 } elseif ( $stylesheet_directory !== $template_directory && file_exists( $stylesheet_directory . '/' . $file ) ) { 1637 $path = $stylesheet_directory . '/' . $file; 1638 } else { 1639 $path = $template_directory . '/' . $file; 1640 } 1641 1642 /** This filter is documented in wp-includes/link-template.php */ 1643 return apply_filters( 'theme_file_path', $path, $file ); 1644 } 1645 1646 /** 1647 * Determines the latest WordPress default theme that is installed. 1648 * 1649 * This hits the filesystem. 1650 * 1651 * @since 4.4.0 1652 * 1653 * @return WP_Theme|false Object, or false if no theme is installed, which would be bad. 1654 */ 1655 public static function get_core_default_theme() { 1656 foreach ( array_reverse( self::$default_themes ) as $slug => $name ) { 1657 $theme = wp_get_theme( $slug ); 1658 if ( $theme->exists() ) { 1659 return $theme; 1660 } 1661 } 1662 return false; 1663 } 1664 1665 /** 1666 * Returns array of stylesheet names of themes allowed on the site or network. 1667 * 1668 * @since 3.4.0 1669 * 1670 * @param int $blog_id Optional. ID of the site. Defaults to the current site. 1671 * @return string[] Array of stylesheet names. 1672 */ 1673 public static function get_allowed( $blog_id = null ) { 1674 /** 1675 * Filters the array of themes allowed on the network. 1676 * 1677 * Site is provided as context so that a list of network allowed themes can 1678 * be filtered further. 1679 * 1680 * @since 4.5.0 1681 * 1682 * @param string[] $allowed_themes An array of theme stylesheet names. 1683 * @param int $blog_id ID of the site. 1684 */ 1685 $network = (array) apply_filters( 'network_allowed_themes', self::get_allowed_on_network(), $blog_id ); 1686 return $network + self::get_allowed_on_site( $blog_id ); 1687 } 1688 1689 /** 1690 * Returns array of stylesheet names of themes allowed on the network. 1691 * 1692 * @since 3.4.0 1693 * 1694 * @return string[] Array of stylesheet names. 1695 */ 1696 public static function get_allowed_on_network() { 1697 static $allowed_themes; 1698 if ( ! isset( $allowed_themes ) ) { 1699 $allowed_themes = (array) get_site_option( 'allowedthemes' ); 1700 } 1701 1702 /** 1703 * Filters the array of themes allowed on the network. 1704 * 1705 * @since MU (3.0.0) 1706 * 1707 * @param string[] $allowed_themes An array of theme stylesheet names. 1708 */ 1709 $allowed_themes = apply_filters( 'allowed_themes', $allowed_themes ); 1710 1711 return $allowed_themes; 1712 } 1713 1714 /** 1715 * Returns array of stylesheet names of themes allowed on the site. 1716 * 1717 * @since 3.4.0 1718 * 1719 * @param int $blog_id Optional. ID of the site. Defaults to the current site. 1720 * @return string[] Array of stylesheet names. 1721 */ 1722 public static function get_allowed_on_site( $blog_id = null ) { 1723 static $allowed_themes = array(); 1724 1725 if ( ! $blog_id || ! is_multisite() ) { 1726 $blog_id = get_current_blog_id(); 1727 } 1728 1729 if ( isset( $allowed_themes[ $blog_id ] ) ) { 1730 /** 1731 * Filters the array of themes allowed on the site. 1732 * 1733 * @since 4.5.0 1734 * 1735 * @param string[] $allowed_themes An array of theme stylesheet names. 1736 * @param int $blog_id ID of the site. Defaults to current site. 1737 */ 1738 return (array) apply_filters( 'site_allowed_themes', $allowed_themes[ $blog_id ], $blog_id ); 1739 } 1740 1741 $current = get_current_blog_id() === $blog_id; 1742 1743 if ( $current ) { 1744 $allowed_themes[ $blog_id ] = get_option( 'allowedthemes' ); 1745 } else { 1746 switch_to_blog( $blog_id ); 1747 $allowed_themes[ $blog_id ] = get_option( 'allowedthemes' ); 1748 restore_current_blog(); 1749 } 1750 1751 /* 1752 * This is all super old MU back compat joy. 1753 * 'allowedthemes' keys things by stylesheet. 'allowed_themes' keyed things by name. 1754 */ 1755 if ( false === $allowed_themes[ $blog_id ] ) { 1756 if ( $current ) { 1757 $allowed_themes[ $blog_id ] = get_option( 'allowed_themes' ); 1758 } else { 1759 switch_to_blog( $blog_id ); 1760 $allowed_themes[ $blog_id ] = get_option( 'allowed_themes' ); 1761 restore_current_blog(); 1762 } 1763 1764 if ( ! is_array( $allowed_themes[ $blog_id ] ) || empty( $allowed_themes[ $blog_id ] ) ) { 1765 $allowed_themes[ $blog_id ] = array(); 1766 } else { 1767 $converted = array(); 1768 $themes = wp_get_themes(); 1769 foreach ( $themes as $stylesheet => $theme_data ) { 1770 if ( isset( $allowed_themes[ $blog_id ][ $theme_data->get( 'Name' ) ] ) ) { 1771 $converted[ $stylesheet ] = true; 1772 } 1773 } 1774 $allowed_themes[ $blog_id ] = $converted; 1775 } 1776 // Set the option so we never have to go through this pain again. 1777 if ( is_admin() && $allowed_themes[ $blog_id ] ) { 1778 if ( $current ) { 1779 update_option( 'allowedthemes', $allowed_themes[ $blog_id ], false ); 1780 delete_option( 'allowed_themes' ); 1781 } else { 1782 switch_to_blog( $blog_id ); 1783 update_option( 'allowedthemes', $allowed_themes[ $blog_id ], false ); 1784 delete_option( 'allowed_themes' ); 1785 restore_current_blog(); 1786 } 1787 } 1788 } 1789 1790 /** This filter is documented in wp-includes/class-wp-theme.php */ 1791 return (array) apply_filters( 'site_allowed_themes', $allowed_themes[ $blog_id ], $blog_id ); 1792 } 1793 1794 /** 1795 * Returns the folder names of the block template directories. 1796 * 1797 * @since 6.4.0 1798 * 1799 * @return string[] { 1800 * Folder names used by block themes. 1801 * 1802 * @type string $wp_template Theme-relative directory name for block templates. 1803 * @type string $wp_template_part Theme-relative directory name for block template parts. 1804 * } 1805 */ 1806 public function get_block_template_folders() { 1807 // Return set/cached value if available. 1808 if ( isset( $this->block_template_folders ) ) { 1809 return $this->block_template_folders; 1810 } 1811 1812 $this->block_template_folders = $this->default_template_folders; 1813 1814 $stylesheet_directory = $this->get_stylesheet_directory(); 1815 // If the theme uses deprecated block template folders. 1816 if ( file_exists( $stylesheet_directory . '/block-templates' ) || file_exists( $stylesheet_directory . '/block-template-parts' ) ) { 1817 $this->block_template_folders = array( 1818 'wp_template' => 'block-templates', 1819 'wp_template_part' => 'block-template-parts', 1820 ); 1821 } 1822 return $this->block_template_folders; 1823 } 1824 1825 /** 1826 * Gets block pattern data for a specified theme. 1827 * Each pattern is defined as a PHP file and defines 1828 * its metadata using plugin-style headers. The minimum required definition is: 1829 * 1830 * /** 1831 * * Title: My Pattern 1832 * * Slug: my-theme/my-pattern 1833 * * 1834 * 1835 * The output of the PHP source corresponds to the content of the pattern, e.g.: 1836 * 1837 * <main><p><?php echo "Hello"; ?></p></main> 1838 * 1839 * If applicable, this will collect from both parent and child theme. 1840 * 1841 * Other settable fields include: 1842 * 1843 * - Description 1844 * - Viewport Width 1845 * - Inserter (yes/no) 1846 * - Categories (comma-separated values) 1847 * - Keywords (comma-separated values) 1848 * - Block Types (comma-separated values) 1849 * - Post Types (comma-separated values) 1850 * - Template Types (comma-separated values) 1851 * 1852 * @since 6.4.0 1853 * 1854 * @return array Block pattern data. 1855 */ 1856 public function get_block_patterns() { 1857 $can_use_cached = ! wp_is_development_mode( 'theme' ); 1858 1859 $pattern_data = $this->get_pattern_cache(); 1860 if ( is_array( $pattern_data ) ) { 1861 if ( $can_use_cached ) { 1862 return $pattern_data; 1863 } 1864 // If in development mode, clear pattern cache. 1865 $this->delete_pattern_cache(); 1866 } 1867 1868 $dirpath = $this->get_stylesheet_directory() . '/patterns'; 1869 $pattern_data = array(); 1870 1871 if ( ! file_exists( $dirpath ) ) { 1872 if ( $can_use_cached ) { 1873 $this->set_pattern_cache( $pattern_data ); 1874 } 1875 return $pattern_data; 1876 } 1877 1878 $files = (array) self::scandir( $dirpath, 'php', -1 ); 1879 1880 /** 1881 * Filters list of block pattern files for a theme. 1882 * 1883 * @since 6.8.0 1884 * 1885 * @param array $files Array of theme files found within `patterns` directory. 1886 * @param string $dirpath Path of theme `patterns` directory being scanned. 1887 */ 1888 $files = apply_filters( 'theme_block_pattern_files', $files, $dirpath ); 1889 1890 $dirpath = trailingslashit( $dirpath ); 1891 1892 if ( ! $files ) { 1893 if ( $can_use_cached ) { 1894 $this->set_pattern_cache( $pattern_data ); 1895 } 1896 return $pattern_data; 1897 } 1898 1899 $default_headers = array( 1900 'title' => 'Title', 1901 'slug' => 'Slug', 1902 'description' => 'Description', 1903 'viewportWidth' => 'Viewport Width', 1904 'inserter' => 'Inserter', 1905 'categories' => 'Categories', 1906 'keywords' => 'Keywords', 1907 'blockTypes' => 'Block Types', 1908 'postTypes' => 'Post Types', 1909 'templateTypes' => 'Template Types', 1910 ); 1911 1912 $properties_to_parse = array( 1913 'categories', 1914 'keywords', 1915 'blockTypes', 1916 'postTypes', 1917 'templateTypes', 1918 ); 1919 1920 foreach ( $files as $file ) { 1921 $pattern = get_file_data( $file, $default_headers ); 1922 1923 if ( empty( $pattern['slug'] ) ) { 1924 _doing_it_wrong( 1925 __FUNCTION__, 1926 sprintf( 1927 /* translators: 1: file name. */ 1928 __( 'Could not register file "%s" as a block pattern ("Slug" field missing)' ), 1929 $file 1930 ), 1931 '6.0.0' 1932 ); 1933 continue; 1934 } 1935 1936 if ( ! preg_match( '/^[A-z0-9\/_-]+$/', $pattern['slug'] ) ) { 1937 _doing_it_wrong( 1938 __FUNCTION__, 1939 sprintf( 1940 /* translators: 1: file name; 2: slug value found. */ 1941 __( 'Could not register file "%1$s" as a block pattern (invalid slug "%2$s")' ), 1942 $file, 1943 $pattern['slug'] 1944 ), 1945 '6.0.0' 1946 ); 1947 } 1948 1949 // Title is a required property. 1950 if ( ! $pattern['title'] ) { 1951 _doing_it_wrong( 1952 __FUNCTION__, 1953 sprintf( 1954 /* translators: 1: file name. */ 1955 __( 'Could not register file "%s" as a block pattern ("Title" field missing)' ), 1956 $file 1957 ), 1958 '6.0.0' 1959 ); 1960 continue; 1961 } 1962 1963 // For properties of type array, parse data as comma-separated. 1964 foreach ( $properties_to_parse as $property ) { 1965 if ( ! empty( $pattern[ $property ] ) ) { 1966 $pattern[ $property ] = array_filter( wp_parse_list( (string) $pattern[ $property ] ) ); 1967 } else { 1968 unset( $pattern[ $property ] ); 1969 } 1970 } 1971 1972 // Parse properties of type int. 1973 $property = 'viewportWidth'; 1974 if ( ! empty( $pattern[ $property ] ) ) { 1975 $pattern[ $property ] = (int) $pattern[ $property ]; 1976 } else { 1977 unset( $pattern[ $property ] ); 1978 } 1979 1980 // Parse properties of type bool. 1981 $property = 'inserter'; 1982 if ( ! empty( $pattern[ $property ] ) ) { 1983 $pattern[ $property ] = in_array( 1984 strtolower( $pattern[ $property ] ), 1985 array( 'yes', 'true' ), 1986 true 1987 ); 1988 } else { 1989 unset( $pattern[ $property ] ); 1990 } 1991 1992 $key = str_replace( $dirpath, '', $file ); 1993 1994 $pattern_data[ $key ] = $pattern; 1995 } 1996 1997 if ( $can_use_cached ) { 1998 $this->set_pattern_cache( $pattern_data ); 1999 } 2000 2001 return $pattern_data; 2002 } 2003 2004 /** 2005 * Gets block pattern cache. 2006 * 2007 * @since 6.4.0 2008 * @since 6.6.0 Uses transients to cache regardless of site environment. 2009 * 2010 * @return array|false Returns an array of patterns if cache is found, otherwise false. 2011 */ 2012 private function get_pattern_cache() { 2013 if ( ! $this->exists() ) { 2014 return false; 2015 } 2016 2017 $pattern_data = get_site_transient( 'wp_theme_files_patterns-' . $this->cache_hash ); 2018 2019 if ( is_array( $pattern_data ) && $pattern_data['version'] === $this->get( 'Version' ) ) { 2020 return $pattern_data['patterns']; 2021 } 2022 return false; 2023 } 2024 2025 /** 2026 * Sets block pattern cache. 2027 * 2028 * @since 6.4.0 2029 * @since 6.6.0 Uses transients to cache regardless of site environment. 2030 * 2031 * @param array $patterns Block patterns data to set in cache. 2032 */ 2033 private function set_pattern_cache( array $patterns ) { 2034 $pattern_data = array( 2035 'version' => $this->get( 'Version' ), 2036 'patterns' => $patterns, 2037 ); 2038 2039 /** 2040 * Filters the cache expiration time for theme files. 2041 * 2042 * @since 6.6.0 2043 * 2044 * @param int $cache_expiration Cache expiration time in seconds. 2045 * @param string $cache_type Type of cache being set. 2046 */ 2047 $cache_expiration = (int) apply_filters( 'wp_theme_files_cache_ttl', self::$cache_expiration, 'theme_block_patterns' ); 2048 2049 // We don't want to cache patterns infinitely. 2050 if ( $cache_expiration <= 0 ) { 2051 _doing_it_wrong( 2052 __METHOD__, 2053 sprintf( 2054 /* translators: %1$s: The filter name.*/ 2055 __( 'The %1$s filter must return an integer value greater than 0.' ), 2056 '<code>wp_theme_files_cache_ttl</code>' 2057 ), 2058 '6.6.0' 2059 ); 2060 2061 $cache_expiration = self::$cache_expiration; 2062 } 2063 2064 set_site_transient( 'wp_theme_files_patterns-' . $this->cache_hash, $pattern_data, $cache_expiration ); 2065 } 2066 2067 /** 2068 * Clears block pattern cache. 2069 * 2070 * @since 6.4.0 2071 * @since 6.6.0 Uses transients to cache regardless of site environment. 2072 */ 2073 public function delete_pattern_cache() { 2074 delete_site_transient( 'wp_theme_files_patterns-' . $this->cache_hash ); 2075 } 2076 2077 /** 2078 * Enables a theme for all sites on the current network. 2079 * 2080 * @since 4.6.0 2081 * 2082 * @param string|string[] $stylesheets Stylesheet name or array of stylesheet names. 2083 */ 2084 public static function network_enable_theme( $stylesheets ) { 2085 if ( ! is_multisite() ) { 2086 return; 2087 } 2088 2089 if ( ! is_array( $stylesheets ) ) { 2090 $stylesheets = array( $stylesheets ); 2091 } 2092 2093 $allowed_themes = get_site_option( 'allowedthemes' ); 2094 foreach ( $stylesheets as $stylesheet ) { 2095 $allowed_themes[ $stylesheet ] = true; 2096 } 2097 2098 update_site_option( 'allowedthemes', $allowed_themes ); 2099 } 2100 2101 /** 2102 * Disables a theme for all sites on the current network. 2103 * 2104 * @since 4.6.0 2105 * 2106 * @param string|string[] $stylesheets Stylesheet name or array of stylesheet names. 2107 */ 2108 public static function network_disable_theme( $stylesheets ) { 2109 if ( ! is_multisite() ) { 2110 return; 2111 } 2112 2113 if ( ! is_array( $stylesheets ) ) { 2114 $stylesheets = array( $stylesheets ); 2115 } 2116 2117 $allowed_themes = get_site_option( 'allowedthemes' ); 2118 foreach ( $stylesheets as $stylesheet ) { 2119 if ( isset( $allowed_themes[ $stylesheet ] ) ) { 2120 unset( $allowed_themes[ $stylesheet ] ); 2121 } 2122 } 2123 2124 update_site_option( 'allowedthemes', $allowed_themes ); 2125 } 2126 2127 /** 2128 * Sorts themes by name. 2129 * 2130 * @since 3.4.0 2131 * 2132 * @param WP_Theme[] $themes Array of theme objects to sort (passed by reference). 2133 */ 2134 public static function sort_by_name( &$themes ) { 2135 if ( str_starts_with( get_user_locale(), 'en_' ) ) { 2136 uasort( $themes, array( 'WP_Theme', '_name_sort' ) ); 2137 } else { 2138 foreach ( $themes as $key => $theme ) { 2139 $theme->translate_header( 'Name', $theme->headers['Name'] ); 2140 } 2141 uasort( $themes, array( 'WP_Theme', '_name_sort_i18n' ) ); 2142 } 2143 } 2144 2145 /** 2146 * Callback function for usort() to naturally sort themes by name. 2147 * 2148 * Accesses the Name header directly from the class for maximum speed. 2149 * Would choke on HTML but we don't care enough to slow it down with strip_tags(). 2150 * 2151 * @since 3.4.0 2152 * 2153 * @param WP_Theme $a First theme. 2154 * @param WP_Theme $b Second theme. 2155 * @return int Negative if `$a` falls lower in the natural order than `$b`. Zero if they fall equally. 2156 * Greater than 0 if `$a` falls higher in the natural order than `$b`. Used with usort(). 2157 */ 2158 private static function _name_sort( $a, $b ) { 2159 return strnatcasecmp( $a->headers['Name'], $b->headers['Name'] ); 2160 } 2161 2162 /** 2163 * Callback function for usort() to naturally sort themes by translated name. 2164 * 2165 * @since 3.4.0 2166 * 2167 * @param WP_Theme $a First theme. 2168 * @param WP_Theme $b Second theme. 2169 * @return int Negative if `$a` falls lower in the natural order than `$b`. Zero if they fall equally. 2170 * Greater than 0 if `$a` falls higher in the natural order than `$b`. Used with usort(). 2171 */ 2172 private static function _name_sort_i18n( $a, $b ) { 2173 return strnatcasecmp( $a->name_translated, $b->name_translated ); 2174 } 2175 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Fri Jul 24 08:20:19 2026 | Cross-referenced by PHPXref |