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