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