[ 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 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' ) 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 * 1186 * @return string[] Array of page templates, keyed by filename and post type, 1187 * with the value of the translated header name. 1188 */ 1189 public function get_post_templates() { 1190 // If you screw up your current theme and we invalidate your parent, most things still work. Let it slide. 1191 if ( $this->errors() && $this->errors()->get_error_codes() !== array( 'theme_parent_invalid' ) ) { 1192 return array(); 1193 } 1194 1195 $post_templates = $this->cache_get( 'post_templates' ); 1196 1197 if ( ! is_array( $post_templates ) ) { 1198 $post_templates = array(); 1199 1200 $files = (array) $this->get_files( 'php', 1, true ); 1201 1202 foreach ( $files as $file => $full_path ) { 1203 if ( ! preg_match( '|Template Name:(.*)$|mi', file_get_contents( $full_path ), $header ) ) { 1204 continue; 1205 } 1206 1207 $types = array( 'page' ); 1208 if ( preg_match( '|Template Post Type:(.*)$|mi', file_get_contents( $full_path ), $type ) ) { 1209 $types = explode( ',', _cleanup_header_comment( $type[1] ) ); 1210 } 1211 1212 foreach ( $types as $type ) { 1213 $type = sanitize_key( $type ); 1214 if ( ! isset( $post_templates[ $type ] ) ) { 1215 $post_templates[ $type ] = array(); 1216 } 1217 1218 $post_templates[ $type ][ $file ] = _cleanup_header_comment( $header[1] ); 1219 } 1220 } 1221 1222 $this->cache_add( 'post_templates', $post_templates ); 1223 } 1224 1225 if ( $this->load_textdomain() ) { 1226 foreach ( $post_templates as &$post_type ) { 1227 foreach ( $post_type as &$post_template ) { 1228 $post_template = $this->translate_header( 'Template Name', $post_template ); 1229 } 1230 } 1231 } 1232 1233 return $post_templates; 1234 } 1235 1236 /** 1237 * Returns the theme's post templates for a given post type. 1238 * 1239 * @since 3.4.0 1240 * @since 4.7.0 Added the `$post_type` parameter. 1241 * 1242 * @param WP_Post|null $post Optional. The post being edited, provided for context. 1243 * @param string $post_type Optional. Post type to get the templates for. Default 'page'. 1244 * If a post is provided, its post type is used. 1245 * @return string[] Array of template header names keyed by the template file name. 1246 */ 1247 public function get_page_templates( $post = null, $post_type = 'page' ) { 1248 if ( $post ) { 1249 $post_type = get_post_type( $post ); 1250 } 1251 1252 $post_templates = $this->get_post_templates(); 1253 $post_templates = isset( $post_templates[ $post_type ] ) ? $post_templates[ $post_type ] : array(); 1254 1255 /** 1256 * Filters list of page templates for a theme. 1257 * 1258 * @since 4.9.6 1259 * 1260 * @param string[] $post_templates Array of template header names keyed by the template file name. 1261 * @param WP_Theme $theme The theme object. 1262 * @param WP_Post|null $post The post being edited, provided for context, or null. 1263 * @param string $post_type Post type to get the templates for. 1264 */ 1265 $post_templates = (array) apply_filters( 'theme_templates', $post_templates, $this, $post, $post_type ); 1266 1267 /** 1268 * Filters list of page templates for a theme. 1269 * 1270 * The dynamic portion of the hook name, `$post_type`, refers to the post type. 1271 * 1272 * @since 3.9.0 1273 * @since 4.4.0 Converted to allow complete control over the `$page_templates` array. 1274 * @since 4.7.0 Added the `$post_type` parameter. 1275 * 1276 * @param string[] $post_templates Array of template header names keyed by the template file name. 1277 * @param WP_Theme $theme The theme object. 1278 * @param WP_Post|null $post The post being edited, provided for context, or null. 1279 * @param string $post_type Post type to get the templates for. 1280 */ 1281 $post_templates = (array) apply_filters( "theme_{$post_type}_templates", $post_templates, $this, $post, $post_type ); 1282 1283 return $post_templates; 1284 } 1285 1286 /** 1287 * Scans a directory for files of a certain extension. 1288 * 1289 * @since 3.4.0 1290 * 1291 * @param string $path Absolute path to search. 1292 * @param array|string|null $extensions Optional. Array of extensions to find, string of a single extension, 1293 * or null for all extensions. Default null. 1294 * @param int $depth Optional. How many levels deep to search for files. Accepts 0, 1+, or 1295 * -1 (infinite depth). Default 0. 1296 * @param string $relative_path Optional. The basename of the absolute path. Used to control the 1297 * returned path for the found files, particularly when this function 1298 * recurses to lower depths. Default empty. 1299 * @return string[]|false Array of files, keyed by the path to the file relative to the `$path` directory prepended 1300 * with `$relative_path`, with the values being absolute paths. False otherwise. 1301 */ 1302 private static function scandir( $path, $extensions = null, $depth = 0, $relative_path = '' ) { 1303 if ( ! is_dir( $path ) ) { 1304 return false; 1305 } 1306 1307 if ( $extensions ) { 1308 $extensions = (array) $extensions; 1309 $_extensions = implode( '|', $extensions ); 1310 } 1311 1312 $relative_path = trailingslashit( $relative_path ); 1313 if ( '/' === $relative_path ) { 1314 $relative_path = ''; 1315 } 1316 1317 $results = scandir( $path ); 1318 $files = array(); 1319 1320 /** 1321 * Filters the array of excluded directories and files while scanning theme folder. 1322 * 1323 * @since 4.7.4 1324 * 1325 * @param string[] $exclusions Array of excluded directories and files. 1326 */ 1327 $exclusions = (array) apply_filters( 'theme_scandir_exclusions', array( 'CVS', 'node_modules', 'vendor', 'bower_components' ) ); 1328 1329 foreach ( $results as $result ) { 1330 if ( '.' === $result[0] || in_array( $result, $exclusions, true ) ) { 1331 continue; 1332 } 1333 if ( is_dir( $path . '/' . $result ) ) { 1334 if ( ! $depth ) { 1335 continue; 1336 } 1337 $found = self::scandir( $path . '/' . $result, $extensions, $depth - 1, $relative_path . $result ); 1338 $files = array_merge_recursive( $files, $found ); 1339 } elseif ( ! $extensions || preg_match( '~\.(' . $_extensions . ')$~', $result ) ) { 1340 $files[ $relative_path . $result ] = $path . '/' . $result; 1341 } 1342 } 1343 1344 return $files; 1345 } 1346 1347 /** 1348 * Loads the theme's textdomain. 1349 * 1350 * Translation files are not inherited from the parent theme. TODO: If this fails for the 1351 * child theme, it should probably try to load the parent theme's translations. 1352 * 1353 * @since 3.4.0 1354 * 1355 * @return bool True if the textdomain was successfully loaded or has already been loaded. 1356 * False if no textdomain was specified in the file headers, or if the domain could not be loaded. 1357 */ 1358 public function load_textdomain() { 1359 if ( isset( $this->textdomain_loaded ) ) { 1360 return $this->textdomain_loaded; 1361 } 1362 1363 $textdomain = $this->get( 'TextDomain' ); 1364 if ( ! $textdomain ) { 1365 $this->textdomain_loaded = false; 1366 return false; 1367 } 1368 1369 if ( is_textdomain_loaded( $textdomain ) ) { 1370 $this->textdomain_loaded = true; 1371 return true; 1372 } 1373 1374 $path = $this->get_stylesheet_directory(); 1375 $domainpath = $this->get( 'DomainPath' ); 1376 if ( $domainpath ) { 1377 $path .= $domainpath; 1378 } else { 1379 $path .= '/languages'; 1380 } 1381 1382 $this->textdomain_loaded = load_theme_textdomain( $textdomain, $path ); 1383 return $this->textdomain_loaded; 1384 } 1385 1386 /** 1387 * Whether the theme is allowed (multisite only). 1388 * 1389 * @since 3.4.0 1390 * 1391 * @param string $check Optional. Whether to check only the 'network'-wide settings, the 'site' 1392 * settings, or 'both'. Defaults to 'both'. 1393 * @param int $blog_id Optional. Ignored if only network-wide settings are checked. Defaults to current site. 1394 * @return bool Whether the theme is allowed for the network. Returns true in single-site. 1395 */ 1396 public function is_allowed( $check = 'both', $blog_id = null ) { 1397 if ( ! is_multisite() ) { 1398 return true; 1399 } 1400 1401 if ( 'both' === $check || 'network' === $check ) { 1402 $allowed = self::get_allowed_on_network(); 1403 if ( ! empty( $allowed[ $this->get_stylesheet() ] ) ) { 1404 return true; 1405 } 1406 } 1407 1408 if ( 'both' === $check || 'site' === $check ) { 1409 $allowed = self::get_allowed_on_site( $blog_id ); 1410 if ( ! empty( $allowed[ $this->get_stylesheet() ] ) ) { 1411 return true; 1412 } 1413 } 1414 1415 return false; 1416 } 1417 1418 /** 1419 * Determines the latest WordPress default theme that is installed. 1420 * 1421 * This hits the filesystem. 1422 * 1423 * @since 4.4.0 1424 * 1425 * @return WP_Theme|false Object, or false if no theme is installed, which would be bad. 1426 */ 1427 public static function get_core_default_theme() { 1428 foreach ( array_reverse( self::$default_themes ) as $slug => $name ) { 1429 $theme = wp_get_theme( $slug ); 1430 if ( $theme->exists() ) { 1431 return $theme; 1432 } 1433 } 1434 return false; 1435 } 1436 1437 /** 1438 * Returns array of stylesheet names of themes allowed on the site or network. 1439 * 1440 * @since 3.4.0 1441 * 1442 * @param int $blog_id Optional. ID of the site. Defaults to the current site. 1443 * @return string[] Array of stylesheet names. 1444 */ 1445 public static function get_allowed( $blog_id = null ) { 1446 /** 1447 * Filters the array of themes allowed on the network. 1448 * 1449 * Site is provided as context so that a list of network allowed themes can 1450 * be filtered further. 1451 * 1452 * @since 4.5.0 1453 * 1454 * @param string[] $allowed_themes An array of theme stylesheet names. 1455 * @param int $blog_id ID of the site. 1456 */ 1457 $network = (array) apply_filters( 'network_allowed_themes', self::get_allowed_on_network(), $blog_id ); 1458 return $network + self::get_allowed_on_site( $blog_id ); 1459 } 1460 1461 /** 1462 * Returns array of stylesheet names of themes allowed on the network. 1463 * 1464 * @since 3.4.0 1465 * 1466 * @return string[] Array of stylesheet names. 1467 */ 1468 public static function get_allowed_on_network() { 1469 static $allowed_themes; 1470 if ( ! isset( $allowed_themes ) ) { 1471 $allowed_themes = (array) get_site_option( 'allowedthemes' ); 1472 } 1473 1474 /** 1475 * Filters the array of themes allowed on the network. 1476 * 1477 * @since MU (3.0.0) 1478 * 1479 * @param string[] $allowed_themes An array of theme stylesheet names. 1480 */ 1481 $allowed_themes = apply_filters( 'allowed_themes', $allowed_themes ); 1482 1483 return $allowed_themes; 1484 } 1485 1486 /** 1487 * Returns array of stylesheet names of themes allowed on the site. 1488 * 1489 * @since 3.4.0 1490 * 1491 * @param int $blog_id Optional. ID of the site. Defaults to the current site. 1492 * @return string[] Array of stylesheet names. 1493 */ 1494 public static function get_allowed_on_site( $blog_id = null ) { 1495 static $allowed_themes = array(); 1496 1497 if ( ! $blog_id || ! is_multisite() ) { 1498 $blog_id = get_current_blog_id(); 1499 } 1500 1501 if ( isset( $allowed_themes[ $blog_id ] ) ) { 1502 /** 1503 * Filters the array of themes allowed on the site. 1504 * 1505 * @since 4.5.0 1506 * 1507 * @param string[] $allowed_themes An array of theme stylesheet names. 1508 * @param int $blog_id ID of the site. Defaults to current site. 1509 */ 1510 return (array) apply_filters( 'site_allowed_themes', $allowed_themes[ $blog_id ], $blog_id ); 1511 } 1512 1513 $current = get_current_blog_id() == $blog_id; 1514 1515 if ( $current ) { 1516 $allowed_themes[ $blog_id ] = get_option( 'allowedthemes' ); 1517 } else { 1518 switch_to_blog( $blog_id ); 1519 $allowed_themes[ $blog_id ] = get_option( 'allowedthemes' ); 1520 restore_current_blog(); 1521 } 1522 1523 // This is all super old MU back compat joy. 1524 // 'allowedthemes' keys things by stylesheet. 'allowed_themes' keyed things by name. 1525 if ( false === $allowed_themes[ $blog_id ] ) { 1526 if ( $current ) { 1527 $allowed_themes[ $blog_id ] = get_option( 'allowed_themes' ); 1528 } else { 1529 switch_to_blog( $blog_id ); 1530 $allowed_themes[ $blog_id ] = get_option( 'allowed_themes' ); 1531 restore_current_blog(); 1532 } 1533 1534 if ( ! is_array( $allowed_themes[ $blog_id ] ) || empty( $allowed_themes[ $blog_id ] ) ) { 1535 $allowed_themes[ $blog_id ] = array(); 1536 } else { 1537 $converted = array(); 1538 $themes = wp_get_themes(); 1539 foreach ( $themes as $stylesheet => $theme_data ) { 1540 if ( isset( $allowed_themes[ $blog_id ][ $theme_data->get( 'Name' ) ] ) ) { 1541 $converted[ $stylesheet ] = true; 1542 } 1543 } 1544 $allowed_themes[ $blog_id ] = $converted; 1545 } 1546 // Set the option so we never have to go through this pain again. 1547 if ( is_admin() && $allowed_themes[ $blog_id ] ) { 1548 if ( $current ) { 1549 update_option( 'allowedthemes', $allowed_themes[ $blog_id ] ); 1550 delete_option( 'allowed_themes' ); 1551 } else { 1552 switch_to_blog( $blog_id ); 1553 update_option( 'allowedthemes', $allowed_themes[ $blog_id ] ); 1554 delete_option( 'allowed_themes' ); 1555 restore_current_blog(); 1556 } 1557 } 1558 } 1559 1560 /** This filter is documented in wp-includes/class-wp-theme.php */ 1561 return (array) apply_filters( 'site_allowed_themes', $allowed_themes[ $blog_id ], $blog_id ); 1562 } 1563 1564 /** 1565 * Enables a theme for all sites on the current network. 1566 * 1567 * @since 4.6.0 1568 * 1569 * @param string|string[] $stylesheets Stylesheet name or array of stylesheet names. 1570 */ 1571 public static function network_enable_theme( $stylesheets ) { 1572 if ( ! is_multisite() ) { 1573 return; 1574 } 1575 1576 if ( ! is_array( $stylesheets ) ) { 1577 $stylesheets = array( $stylesheets ); 1578 } 1579 1580 $allowed_themes = get_site_option( 'allowedthemes' ); 1581 foreach ( $stylesheets as $stylesheet ) { 1582 $allowed_themes[ $stylesheet ] = true; 1583 } 1584 1585 update_site_option( 'allowedthemes', $allowed_themes ); 1586 } 1587 1588 /** 1589 * Disables a theme for all sites on the current network. 1590 * 1591 * @since 4.6.0 1592 * 1593 * @param string|string[] $stylesheets Stylesheet name or array of stylesheet names. 1594 */ 1595 public static function network_disable_theme( $stylesheets ) { 1596 if ( ! is_multisite() ) { 1597 return; 1598 } 1599 1600 if ( ! is_array( $stylesheets ) ) { 1601 $stylesheets = array( $stylesheets ); 1602 } 1603 1604 $allowed_themes = get_site_option( 'allowedthemes' ); 1605 foreach ( $stylesheets as $stylesheet ) { 1606 if ( isset( $allowed_themes[ $stylesheet ] ) ) { 1607 unset( $allowed_themes[ $stylesheet ] ); 1608 } 1609 } 1610 1611 update_site_option( 'allowedthemes', $allowed_themes ); 1612 } 1613 1614 /** 1615 * Sorts themes by name. 1616 * 1617 * @since 3.4.0 1618 * 1619 * @param WP_Theme[] $themes Array of theme objects to sort (passed by reference). 1620 */ 1621 public static function sort_by_name( &$themes ) { 1622 if ( 0 === strpos( get_user_locale(), 'en_' ) ) { 1623 uasort( $themes, array( 'WP_Theme', '_name_sort' ) ); 1624 } else { 1625 foreach ( $themes as $key => $theme ) { 1626 $theme->translate_header( 'Name', $theme->headers['Name'] ); 1627 } 1628 uasort( $themes, array( 'WP_Theme', '_name_sort_i18n' ) ); 1629 } 1630 } 1631 1632 /** 1633 * Callback function for usort() to naturally sort themes by name. 1634 * 1635 * Accesses the Name header directly from the class for maximum speed. 1636 * Would choke on HTML but we don't care enough to slow it down with strip_tags(). 1637 * 1638 * @since 3.4.0 1639 * 1640 * @param WP_Theme $a First theme. 1641 * @param WP_Theme $b Second theme. 1642 * @return int Negative if `$a` falls lower in the natural order than `$b`. Zero if they fall equally. 1643 * Greater than 0 if `$a` falls higher in the natural order than `$b`. Used with usort(). 1644 */ 1645 private static function _name_sort( $a, $b ) { 1646 return strnatcasecmp( $a->headers['Name'], $b->headers['Name'] ); 1647 } 1648 1649 /** 1650 * Callback function for usort() to naturally sort themes by translated name. 1651 * 1652 * @since 3.4.0 1653 * 1654 * @param WP_Theme $a First theme. 1655 * @param WP_Theme $b Second theme. 1656 * @return int Negative if `$a` falls lower in the natural order than `$b`. Zero if they fall equally. 1657 * Greater than 0 if `$a` falls higher in the natural order than `$b`. Used with usort(). 1658 */ 1659 private static function _name_sort_i18n( $a, $b ) { 1660 return strnatcasecmp( $a->name_translated, $b->name_translated ); 1661 } 1662 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Mar 4 01:00:04 2021 | Cross-referenced by PHPXref 0.7.1 |