[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

/wp-includes/ -> class-wp-theme.php (source)

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


Generated: Sat Nov 23 01:00:02 2024 Cross-referenced by PHPXref 0.7.1