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