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