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