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