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