]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/class-wp-theme.php
WordPress 4.4.1
[autoinstalls/wordpress.git] / wp-includes / class-wp-theme.php
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          * @access public
16          * @var bool
17          *
18          * @see WP_MS_Themes_List_Table
19          */
20         public $update = false;
21
22         /**
23          * Headers for style.css files.
24          *
25          * @static
26          * @access private
27          * @var array
28          */
29         private static $file_headers = array(
30                 'Name'        => 'Theme Name',
31                 'ThemeURI'    => 'Theme URI',
32                 'Description' => 'Description',
33                 'Author'      => 'Author',
34                 'AuthorURI'   => 'Author URI',
35                 'Version'     => 'Version',
36                 'Template'    => 'Template',
37                 'Status'      => 'Status',
38                 'Tags'        => 'Tags',
39                 'TextDomain'  => 'Text Domain',
40                 'DomainPath'  => 'Domain Path',
41         );
42
43         /**
44          * Default themes.
45          *
46          * @static
47          * @access private
48          * @var array
49          */
50         private static $default_themes = array(
51                 'classic'        => 'WordPress Classic',
52                 'default'        => 'WordPress Default',
53                 'twentyten'      => 'Twenty Ten',
54                 'twentyeleven'   => 'Twenty Eleven',
55                 'twentytwelve'   => 'Twenty Twelve',
56                 'twentythirteen' => 'Twenty Thirteen',
57                 'twentyfourteen' => 'Twenty Fourteen',
58                 'twentyfifteen'  => 'Twenty Fifteen',
59                 'twentysixteen'  => 'Twenty Sixteen',
60         );
61
62         /**
63          * Renamed theme tags.
64          *
65          * @static
66          * @access private
67          * @var array
68          */
69         private static $tag_map = array(
70                 'fixed-width'    => 'fixed-layout',
71                 'flexible-width' => 'fluid-layout',
72         );
73
74         /**
75          * Absolute path to the theme root, usually wp-content/themes
76          *
77          * @access private
78          * @var string
79          */
80         private $theme_root;
81
82         /**
83          * Header data from the theme's style.css file.
84          *
85          * @access private
86          * @var array
87          */
88         private $headers = array();
89
90         /**
91          * Header data from the theme's style.css file after being sanitized.
92          *
93          * @access private
94          * @var array
95          */
96         private $headers_sanitized;
97
98         /**
99          * Header name from the theme's style.css after being translated.
100          *
101          * Cached due to sorting functions running over the translated name.
102          *
103          * @access private
104          * @var string
105          */
106         private $name_translated;
107
108         /**
109          * Errors encountered when initializing the theme.
110          *
111          * @access private
112          * @var WP_Error
113          */
114         private $errors;
115
116         /**
117          * The directory name of the theme's files, inside the theme root.
118          *
119          * In the case of a child theme, this is directory name of the child theme.
120          * Otherwise, 'stylesheet' is the same as 'template'.
121          *
122          * @access private
123          * @var string
124          */
125         private $stylesheet;
126
127         /**
128          * The directory name of the theme's files, inside the theme root.
129          *
130          * In the case of a child theme, this is the directory name of the parent theme.
131          * Otherwise, 'template' is the same as 'stylesheet'.
132          *
133          * @access private
134          * @var string
135          */
136         private $template;
137
138         /**
139          * A reference to the parent theme, in the case of a child theme.
140          *
141          * @access private
142          * @var WP_Theme
143          */
144         private $parent;
145
146         /**
147          * URL to the theme root, usually an absolute URL to wp-content/themes
148          *
149          * @access private
150          * var string
151          */
152         private $theme_root_uri;
153
154         /**
155          * Flag for whether the theme's textdomain is loaded.
156          *
157          * @access private
158          * @var bool
159          */
160         private $textdomain_loaded;
161
162         /**
163          * Stores an md5 hash of the theme root, to function as the cache key.
164          *
165          * @access private
166          * @var string
167          */
168         private $cache_hash;
169
170         /**
171          * Flag for whether the themes cache bucket should be persistently cached.
172          *
173          * Default is false. Can be set with the wp_cache_themes_persistently filter.
174          *
175          * @static
176          * @access private
177          * @var bool
178          */
179         private static $persistently_cache;
180
181         /**
182          * Expiration time for the themes cache bucket.
183          *
184          * By default the bucket is not cached, so this value is useless.
185          *
186          * @static
187          * @access private
188          * @var bool
189          */
190         private static $cache_expiration = 1800;
191
192         /**
193          * Constructor for WP_Theme.
194          *
195          * @global array $wp_theme_directories
196          *
197          * @param string $theme_dir Directory of the theme within the theme_root.
198          * @param string $theme_root Theme root.
199          * @param WP_Error|void $_child If this theme is a parent theme, the child may be passed for validation purposes.
200          */
201         public function __construct( $theme_dir, $theme_root, $_child = null ) {
202                 global $wp_theme_directories;
203
204                 // Initialize caching on first run.
205                 if ( ! isset( self::$persistently_cache ) ) {
206                         /** This action is documented in wp-includes/theme.php */
207                         self::$persistently_cache = apply_filters( 'wp_cache_themes_persistently', false, 'WP_Theme' );
208                         if ( self::$persistently_cache ) {
209                                 wp_cache_add_global_groups( 'themes' );
210                                 if ( is_int( self::$persistently_cache ) )
211                                         self::$cache_expiration = self::$persistently_cache;
212                         } else {
213                                 wp_cache_add_non_persistent_groups( 'themes' );
214                         }
215                 }
216
217                 $this->theme_root = $theme_root;
218                 $this->stylesheet = $theme_dir;
219
220                 // Correct a situation where the theme is 'some-directory/some-theme' but 'some-directory' was passed in as part of the theme root instead.
221                 if ( ! in_array( $theme_root, (array) $wp_theme_directories ) && in_array( dirname( $theme_root ), (array) $wp_theme_directories ) ) {
222                         $this->stylesheet = basename( $this->theme_root ) . '/' . $this->stylesheet;
223                         $this->theme_root = dirname( $theme_root );
224                 }
225
226                 $this->cache_hash = md5( $this->theme_root . '/' . $this->stylesheet );
227                 $theme_file = $this->stylesheet . '/style.css';
228
229                 $cache = $this->cache_get( 'theme' );
230
231                 if ( is_array( $cache ) ) {
232                         foreach ( array( 'errors', 'headers', 'template' ) as $key ) {
233                                 if ( isset( $cache[ $key ] ) )
234                                         $this->$key = $cache[ $key ];
235                         }
236                         if ( $this->errors )
237                                 return;
238                         if ( isset( $cache['theme_root_template'] ) )
239                                 $theme_root_template = $cache['theme_root_template'];
240                 } elseif ( ! file_exists( $this->theme_root . '/' . $theme_file ) ) {
241                         $this->headers['Name'] = $this->stylesheet;
242                         if ( ! file_exists( $this->theme_root . '/' . $this->stylesheet ) )
243                                 $this->errors = new WP_Error( 'theme_not_found', sprintf( __( 'The theme directory "%s" does not exist.' ), esc_html( $this->stylesheet ) ) );
244                         else
245                                 $this->errors = new WP_Error( 'theme_no_stylesheet', __( 'Stylesheet is missing.' ) );
246                         $this->template = $this->stylesheet;
247                         $this->cache_add( 'theme', array( 'headers' => $this->headers, 'errors' => $this->errors, 'stylesheet' => $this->stylesheet, 'template' => $this->template ) );
248                         if ( ! file_exists( $this->theme_root ) ) // Don't cache this one.
249                                 $this->errors->add( 'theme_root_missing', __( 'ERROR: The themes directory is either empty or doesn&#8217;t exist. Please check your installation.' ) );
250                         return;
251                 } elseif ( ! is_readable( $this->theme_root . '/' . $theme_file ) ) {
252                         $this->headers['Name'] = $this->stylesheet;
253                         $this->errors = new WP_Error( 'theme_stylesheet_not_readable', __( 'Stylesheet is not readable.' ) );
254                         $this->template = $this->stylesheet;
255                         $this->cache_add( 'theme', array( 'headers' => $this->headers, 'errors' => $this->errors, 'stylesheet' => $this->stylesheet, 'template' => $this->template ) );
256                         return;
257                 } else {
258                         $this->headers = get_file_data( $this->theme_root . '/' . $theme_file, self::$file_headers, 'theme' );
259                         // Default themes always trump their pretenders.
260                         // Properly identify default themes that are inside a directory within wp-content/themes.
261                         if ( $default_theme_slug = array_search( $this->headers['Name'], self::$default_themes ) ) {
262                                 if ( basename( $this->stylesheet ) != $default_theme_slug )
263                                         $this->headers['Name'] .= '/' . $this->stylesheet;
264                         }
265                 }
266
267                 // (If template is set from cache [and there are no errors], we know it's good.)
268                 if ( ! $this->template && ! ( $this->template = $this->headers['Template'] ) ) {
269                         $this->template = $this->stylesheet;
270                         if ( ! file_exists( $this->theme_root . '/' . $this->stylesheet . '/index.php' ) ) {
271                                 $this->errors = new WP_Error( 'theme_no_index', __( 'Template is missing.' ) );
272                                 $this->cache_add( 'theme', array( 'headers' => $this->headers, 'errors' => $this->errors, 'stylesheet' => $this->stylesheet, 'template' => $this->template ) );
273                                 return;
274                         }
275                 }
276
277                 // If we got our data from cache, we can assume that 'template' is pointing to the right place.
278                 if ( ! is_array( $cache ) && $this->template != $this->stylesheet && ! file_exists( $this->theme_root . '/' . $this->template . '/index.php' ) ) {
279                         // If we're in a directory of themes inside /themes, look for the parent nearby.
280                         // wp-content/themes/directory-of-themes/*
281                         $parent_dir = dirname( $this->stylesheet );
282                         if ( '.' != $parent_dir && file_exists( $this->theme_root . '/' . $parent_dir . '/' . $this->template . '/index.php' ) ) {
283                                 $this->template = $parent_dir . '/' . $this->template;
284                         } elseif ( ( $directories = search_theme_directories() ) && isset( $directories[ $this->template ] ) ) {
285                                 // Look for the template in the search_theme_directories() results, in case it is in another theme root.
286                                 // We don't look into directories of themes, just the theme root.
287                                 $theme_root_template = $directories[ $this->template ]['theme_root'];
288                         } else {
289                                 // Parent theme is missing.
290                                 $this->errors = new WP_Error( 'theme_no_parent', sprintf( __( 'The parent theme is missing. Please install the "%s" parent theme.' ), esc_html( $this->template ) ) );
291                                 $this->cache_add( 'theme', array( 'headers' => $this->headers, 'errors' => $this->errors, 'stylesheet' => $this->stylesheet, 'template' => $this->template ) );
292                                 $this->parent = new WP_Theme( $this->template, $this->theme_root, $this );
293                                 return;
294                         }
295                 }
296
297                 // Set the parent, if we're a child theme.
298                 if ( $this->template != $this->stylesheet ) {
299                         // If we are a parent, then there is a problem. Only two generations allowed! Cancel things out.
300                         if ( $_child instanceof WP_Theme && $_child->template == $this->stylesheet ) {
301                                 $_child->parent = null;
302                                 $_child->errors = new WP_Error( 'theme_parent_invalid', sprintf( __( 'The "%s" theme is not a valid parent theme.' ), esc_html( $_child->template ) ) );
303                                 $_child->cache_add( 'theme', array( 'headers' => $_child->headers, 'errors' => $_child->errors, 'stylesheet' => $_child->stylesheet, 'template' => $_child->template ) );
304                                 // The two themes actually reference each other with the Template header.
305                                 if ( $_child->stylesheet == $this->template ) {
306                                         $this->errors = new WP_Error( 'theme_parent_invalid', sprintf( __( 'The "%s" theme is not a valid parent theme.' ), esc_html( $this->template ) ) );
307                                         $this->cache_add( 'theme', array( 'headers' => $this->headers, 'errors' => $this->errors, 'stylesheet' => $this->stylesheet, 'template' => $this->template ) );
308                                 }
309                                 return;
310                         }
311                         // Set the parent. Pass the current instance so we can do the crazy checks above and assess errors.
312                         $this->parent = new WP_Theme( $this->template, isset( $theme_root_template ) ? $theme_root_template : $this->theme_root, $this );
313                 }
314
315                 // We're good. If we didn't retrieve from cache, set it.
316                 if ( ! is_array( $cache ) ) {
317                         $cache = array( 'headers' => $this->headers, 'errors' => $this->errors, 'stylesheet' => $this->stylesheet, 'template' => $this->template );
318                         // If the parent theme is in another root, we'll want to cache this. Avoids an entire branch of filesystem calls above.
319                         if ( isset( $theme_root_template ) )
320                                 $cache['theme_root_template'] = $theme_root_template;
321                         $this->cache_add( 'theme', $cache );
322                 }
323         }
324
325         /**
326          * When converting the object to a string, the theme name is returned.
327          *
328          * @return string Theme name, ready for display (translated)
329          */
330         public function __toString() {
331                 return (string) $this->display('Name');
332         }
333
334         /**
335          * __isset() magic method for properties formerly returned by current_theme_info()
336          *
337          * @staticvar array $properties
338          *
339          * @return bool
340          */
341         public function __isset( $offset ) {
342                 static $properties = array(
343                         'name', 'title', 'version', 'parent_theme', 'template_dir', 'stylesheet_dir', 'template', 'stylesheet',
344                         'screenshot', 'description', 'author', 'tags', 'theme_root', 'theme_root_uri',
345                 );
346
347                 return in_array( $offset, $properties );
348         }
349
350         /**
351          * __get() magic method for properties formerly returned by current_theme_info()
352          *
353          * @return mixed
354          */
355         public function __get( $offset ) {
356                 switch ( $offset ) {
357                         case 'name' :
358                         case 'title' :
359                                 return $this->get('Name');
360                         case 'version' :
361                                 return $this->get('Version');
362                         case 'parent_theme' :
363                                 return $this->parent() ? $this->parent()->get('Name') : '';
364                         case 'template_dir' :
365                                 return $this->get_template_directory();
366                         case 'stylesheet_dir' :
367                                 return $this->get_stylesheet_directory();
368                         case 'template' :
369                                 return $this->get_template();
370                         case 'stylesheet' :
371                                 return $this->get_stylesheet();
372                         case 'screenshot' :
373                                 return $this->get_screenshot( 'relative' );
374                         // 'author' and 'description' did not previously return translated data.
375                         case 'description' :
376                                 return $this->display('Description');
377                         case 'author' :
378                                 return $this->display('Author');
379                         case 'tags' :
380                                 return $this->get( 'Tags' );
381                         case 'theme_root' :
382                                 return $this->get_theme_root();
383                         case 'theme_root_uri' :
384                                 return $this->get_theme_root_uri();
385                         // For cases where the array was converted to an object.
386                         default :
387                                 return $this->offsetGet( $offset );
388                 }
389         }
390
391         /**
392          * Method to implement ArrayAccess for keys formerly returned by get_themes()
393          *
394          * @param mixed $offset
395          * @param mixed $value
396          */
397         public function offsetSet( $offset, $value ) {}
398
399         /**
400          * Method to implement ArrayAccess for keys formerly returned by get_themes()
401          *
402          * @param mixed $offset
403          */
404         public function offsetUnset( $offset ) {}
405
406         /**
407          * Method to implement ArrayAccess for keys formerly returned by get_themes()
408          *
409          * @staticvar array $keys
410          *
411          * @param mixed $offset
412          * @return bool
413          */
414         public function offsetExists( $offset ) {
415                 static $keys = array(
416                         'Name', 'Version', 'Status', 'Title', 'Author', 'Author Name', 'Author URI', 'Description',
417                         'Template', 'Stylesheet', 'Template Files', 'Stylesheet Files', 'Template Dir', 'Stylesheet Dir',
418                         'Screenshot', 'Tags', 'Theme Root', 'Theme Root URI', 'Parent Theme',
419                 );
420
421                 return in_array( $offset, $keys );
422         }
423
424         /**
425          * Method to implement ArrayAccess for keys formerly returned by get_themes().
426          *
427          * Author, Author Name, Author URI, and Description did not previously return
428          * translated data. We are doing so now as it is safe to do. However, as
429          * Name and Title could have been used as the key for get_themes(), both remain
430          * untranslated for back compatibility. This means that ['Name'] is not ideal,
431          * and care should be taken to use $theme->display('Name') to get a properly
432          * translated header.
433          *
434          * @param mixed $offset
435          * @return mixed
436          */
437         public function offsetGet( $offset ) {
438                 switch ( $offset ) {
439                         case 'Name' :
440                         case 'Title' :
441                                 // See note above about using translated data. get() is not ideal.
442                                 // It is only for backwards compatibility. Use display().
443                                 return $this->get('Name');
444                         case 'Author' :
445                                 return $this->display( 'Author');
446                         case 'Author Name' :
447                                 return $this->display( 'Author', false);
448                         case 'Author URI' :
449                                 return $this->display('AuthorURI');
450                         case 'Description' :
451                                 return $this->display( 'Description');
452                         case 'Version' :
453                         case 'Status' :
454                                 return $this->get( $offset );
455                         case 'Template' :
456                                 return $this->get_template();
457                         case 'Stylesheet' :
458                                 return $this->get_stylesheet();
459                         case 'Template Files' :
460                                 return $this->get_files( 'php', 1, true );
461                         case 'Stylesheet Files' :
462                                 return $this->get_files( 'css', 0, false );
463                         case 'Template Dir' :
464                                 return $this->get_template_directory();
465                         case 'Stylesheet Dir' :
466                                 return $this->get_stylesheet_directory();
467                         case 'Screenshot' :
468                                 return $this->get_screenshot( 'relative' );
469                         case 'Tags' :
470                                 return $this->get('Tags');
471                         case 'Theme Root' :
472                                 return $this->get_theme_root();
473                         case 'Theme Root URI' :
474                                 return $this->get_theme_root_uri();
475                         case 'Parent Theme' :
476                                 return $this->parent() ? $this->parent()->get('Name') : '';
477                         default :
478                                 return null;
479                 }
480         }
481
482         /**
483          * Returns errors property.
484          *
485          * @since 3.4.0
486          * @access public
487          *
488          * @return WP_Error|false WP_Error if there are errors, or false.
489          */
490         public function errors() {
491                 return is_wp_error( $this->errors ) ? $this->errors : false;
492         }
493
494         /**
495          * Whether the theme exists.
496          *
497          * A theme with errors exists. A theme with the error of 'theme_not_found',
498          * meaning that the theme's directory was not found, does not exist.
499          *
500          * @since 3.4.0
501          * @access public
502          *
503          * @return bool Whether the theme exists.
504          */
505         public function exists() {
506                 return ! ( $this->errors() && in_array( 'theme_not_found', $this->errors()->get_error_codes() ) );
507         }
508
509         /**
510          * Returns reference to the parent theme.
511          *
512          * @since 3.4.0
513          * @access public
514          *
515          * @return WP_Theme|false Parent theme, or false if the current theme is not a child theme.
516          */
517         public function parent() {
518                 return isset( $this->parent ) ? $this->parent : false;
519         }
520
521         /**
522          * Adds theme data to cache.
523          *
524          * Cache entries keyed by the theme and the type of data.
525          *
526          * @since 3.4.0
527          * @access private
528          *
529          * @param string $key Type of data to store (theme, screenshot, headers, page_templates)
530          * @param string $data Data to store
531          * @return bool Return value from wp_cache_add()
532          */
533         private function cache_add( $key, $data ) {
534                 return wp_cache_add( $key . '-' . $this->cache_hash, $data, 'themes', self::$cache_expiration );
535         }
536
537         /**
538          * Gets theme data from cache.
539          *
540          * Cache entries are keyed by the theme and the type of data.
541          *
542          * @since 3.4.0
543          * @access private
544          *
545          * @param string $key Type of data to retrieve (theme, screenshot, headers, page_templates)
546          * @return mixed Retrieved data
547          */
548         private function cache_get( $key ) {
549                 return wp_cache_get( $key . '-' . $this->cache_hash, 'themes' );
550         }
551
552         /**
553          * Clears the cache for the theme.
554          *
555          * @since 3.4.0
556          * @access public
557          */
558         public function cache_delete() {
559                 foreach ( array( 'theme', 'screenshot', 'headers', 'page_templates' ) as $key )
560                         wp_cache_delete( $key . '-' . $this->cache_hash, 'themes' );
561                 $this->template = $this->textdomain_loaded = $this->theme_root_uri = $this->parent = $this->errors = $this->headers_sanitized = $this->name_translated = null;
562                 $this->headers = array();
563                 $this->__construct( $this->stylesheet, $this->theme_root );
564         }
565
566         /**
567          * Get a raw, unformatted theme header.
568          *
569          * The header is sanitized, but is not translated, and is not marked up for display.
570          * To get a theme header for display, use the display() method.
571          *
572          * Use the get_template() method, not the 'Template' header, for finding the template.
573          * The 'Template' header is only good for what was written in the style.css, while
574          * get_template() takes into account where WordPress actually located the theme and
575          * whether it is actually valid.
576          *
577          * @since 3.4.0
578          * @access public
579          *
580          * @param string $header Theme header. Name, Description, Author, Version, ThemeURI, AuthorURI, Status, Tags.
581          * @return string|false String on success, false on failure.
582          */
583         public function get( $header ) {
584                 if ( ! isset( $this->headers[ $header ] ) )
585                         return false;
586
587                 if ( ! isset( $this->headers_sanitized ) ) {
588                         $this->headers_sanitized = $this->cache_get( 'headers' );
589                         if ( ! is_array( $this->headers_sanitized ) )
590                                 $this->headers_sanitized = array();
591                 }
592
593                 if ( isset( $this->headers_sanitized[ $header ] ) )
594                         return $this->headers_sanitized[ $header ];
595
596                 // If themes are a persistent group, sanitize everything and cache it. One cache add is better than many cache sets.
597                 if ( self::$persistently_cache ) {
598                         foreach ( array_keys( $this->headers ) as $_header )
599                                 $this->headers_sanitized[ $_header ] = $this->sanitize_header( $_header, $this->headers[ $_header ] );
600                         $this->cache_add( 'headers', $this->headers_sanitized );
601                 } else {
602                         $this->headers_sanitized[ $header ] = $this->sanitize_header( $header, $this->headers[ $header ] );
603                 }
604
605                 return $this->headers_sanitized[ $header ];
606         }
607
608         /**
609          * Gets a theme header, formatted and translated for display.
610          *
611          * @since 3.4.0
612          * @access public
613          *
614          * @param string $header Theme header. Name, Description, Author, Version, ThemeURI, AuthorURI, Status, Tags.
615          * @param bool $markup Optional. Whether to mark up the header. Defaults to true.
616          * @param bool $translate Optional. Whether to translate the header. Defaults to true.
617          * @return string|false Processed header, false on failure.
618          */
619         public function display( $header, $markup = true, $translate = true ) {
620                 $value = $this->get( $header );
621                 if ( false === $value ) {
622                         return false;
623                 }
624
625                 if ( $translate && ( empty( $value ) || ! $this->load_textdomain() ) )
626                         $translate = false;
627
628                 if ( $translate )
629                         $value = $this->translate_header( $header, $value );
630
631                 if ( $markup )
632                         $value = $this->markup_header( $header, $value, $translate );
633
634                 return $value;
635         }
636
637         /**
638          * Sanitize a theme header.
639          *
640          * @since 3.4.0
641          * @access private
642          *
643          * @staticvar array $header_tags
644          * @staticvar array $header_tags_with_a
645          *
646          * @param string $header Theme header. Name, Description, Author, Version, ThemeURI, AuthorURI, Status, Tags.
647          * @param string $value Value to sanitize.
648          * @return mixed
649          */
650         private function sanitize_header( $header, $value ) {
651                 switch ( $header ) {
652                         case 'Status' :
653                                 if ( ! $value ) {
654                                         $value = 'publish';
655                                         break;
656                                 }
657                                 // Fall through otherwise.
658                         case 'Name' :
659                                 static $header_tags = array(
660                                         'abbr'    => array( 'title' => true ),
661                                         'acronym' => array( 'title' => true ),
662                                         'code'    => true,
663                                         'em'      => true,
664                                         'strong'  => true,
665                                 );
666                                 $value = wp_kses( $value, $header_tags );
667                                 break;
668                         case 'Author' :
669                                 // There shouldn't be anchor tags in Author, but some themes like to be challenging.
670                         case 'Description' :
671                                 static $header_tags_with_a = array(
672                                         'a'       => array( 'href' => true, 'title' => true ),
673                                         'abbr'    => array( 'title' => true ),
674                                         'acronym' => array( 'title' => true ),
675                                         'code'    => true,
676                                         'em'      => true,
677                                         'strong'  => true,
678                                 );
679                                 $value = wp_kses( $value, $header_tags_with_a );
680                                 break;
681                         case 'ThemeURI' :
682                         case 'AuthorURI' :
683                                 $value = esc_url_raw( $value );
684                                 break;
685                         case 'Tags' :
686                                 $value = array_filter( array_map( 'trim', explode( ',', strip_tags( $value ) ) ) );
687                                 break;
688                         case 'Version' :
689                                 $value = strip_tags( $value );
690                                 break;
691                 }
692
693                 return $value;
694         }
695
696         /**
697          * Mark up a theme header.
698          *
699      * @since 3.4.0
700          * @access private
701          *
702          * @staticvar string $comma
703          *
704          * @param string $header Theme header. Name, Description, Author, Version, ThemeURI, AuthorURI, Status, Tags.
705          * @param string $value Value to mark up.
706          * @param string $translate Whether the header has been translated.
707          * @return string Value, marked up.
708          */
709         private function markup_header( $header, $value, $translate ) {
710                 switch ( $header ) {
711                         case 'Name' :
712                                 if ( empty( $value ) )
713                                         $value = $this->get_stylesheet();
714                                 break;
715                         case 'Description' :
716                                 $value = wptexturize( $value );
717                                 break;
718                         case 'Author' :
719                                 if ( $this->get('AuthorURI') ) {
720                                         $value = sprintf( '<a href="%1$s">%2$s</a>', $this->display( 'AuthorURI', true, $translate ), $value );
721                                 } elseif ( ! $value ) {
722                                         $value = __( 'Anonymous' );
723                                 }
724                                 break;
725                         case 'Tags' :
726                                 static $comma = null;
727                                 if ( ! isset( $comma ) ) {
728                                         /* translators: used between list items, there is a space after the comma */
729                                         $comma = __( ', ' );
730                                 }
731                                 $value = implode( $comma, $value );
732                                 break;
733                         case 'ThemeURI' :
734                         case 'AuthorURI' :
735                                 $value = esc_url( $value );
736                                 break;
737                 }
738
739                 return $value;
740         }
741
742         /**
743          * Translate a theme header.
744          *
745          * @since 3.4.0
746          * @access private
747          *
748          * @staticvar array $tags_list
749          *
750          * @param string $header Theme header. Name, Description, Author, Version, ThemeURI, AuthorURI, Status, Tags.
751          * @param string $value Value to translate.
752          * @return string Translated value.
753          */
754         private function translate_header( $header, $value ) {
755                 switch ( $header ) {
756                         case 'Name' :
757                                 // Cached for sorting reasons.
758                                 if ( isset( $this->name_translated ) )
759                                         return $this->name_translated;
760                                 $this->name_translated = translate( $value, $this->get('TextDomain' ) );
761                                 return $this->name_translated;
762                         case 'Tags' :
763                                 if ( empty( $value ) || ! function_exists( 'get_theme_feature_list' ) )
764                                         return $value;
765
766                                 static $tags_list;
767                                 if ( ! isset( $tags_list ) ) {
768                                         $tags_list = array();
769                                         $feature_list = get_theme_feature_list( false ); // No API
770                                         foreach ( $feature_list as $tags )
771                                                 $tags_list += $tags;
772                                 }
773
774                                 foreach ( $value as &$tag ) {
775                                         if ( isset( $tags_list[ $tag ] ) ) {
776                                                 $tag = $tags_list[ $tag ];
777                                         } elseif ( isset( self::$tag_map[ $tag ] ) ) {
778                                                 $tag = $tags_list[ self::$tag_map[ $tag ] ];
779                                         }
780                                 }
781
782                                 return $value;
783
784                         default :
785                                 $value = translate( $value, $this->get('TextDomain') );
786                 }
787                 return $value;
788         }
789
790         /**
791          * The directory name of the theme's "stylesheet" files, inside the theme root.
792          *
793          * In the case of a child theme, this is directory name of the child theme.
794          * Otherwise, get_stylesheet() is the same as get_template().
795          *
796          * @since 3.4.0
797          * @access public
798          *
799          * @return string Stylesheet
800          */
801         public function get_stylesheet() {
802                 return $this->stylesheet;
803         }
804
805         /**
806          * The directory name of the theme's "template" files, inside the theme root.
807          *
808          * In the case of a child theme, this is the directory name of the parent theme.
809          * Otherwise, the get_template() is the same as get_stylesheet().
810          *
811          * @since 3.4.0
812          * @access public
813          *
814          * @return string Template
815          */
816         public function get_template() {
817                 return $this->template;
818         }
819
820         /**
821          * Returns the absolute path to the directory of a theme's "stylesheet" files.
822          *
823          * In the case of a child theme, this is the absolute path to the directory
824          * of the child theme's files.
825          *
826          * @since 3.4.0
827          * @access public
828          *
829          * @return string Absolute path of the stylesheet directory.
830          */
831         public function get_stylesheet_directory() {
832                 if ( $this->errors() && in_array( 'theme_root_missing', $this->errors()->get_error_codes() ) )
833                         return '';
834
835                 return $this->theme_root . '/' . $this->stylesheet;
836         }
837
838         /**
839          * Returns the absolute path to the directory of a theme's "template" files.
840          *
841          * In the case of a child theme, this is the absolute path to the directory
842          * of the parent theme's files.
843          *
844          * @since 3.4.0
845          * @access public
846          *
847          * @return string Absolute path of the template directory.
848          */
849         public function get_template_directory() {
850                 if ( $this->parent() )
851                         $theme_root = $this->parent()->theme_root;
852                 else
853                         $theme_root = $this->theme_root;
854
855                 return $theme_root . '/' . $this->template;
856         }
857
858         /**
859          * Returns the URL to the directory of a theme's "stylesheet" files.
860          *
861          * In the case of a child theme, this is the URL to the directory of the
862          * child theme's files.
863          *
864          * @since 3.4.0
865          * @access public
866          *
867          * @return string URL to the stylesheet directory.
868          */
869         public function get_stylesheet_directory_uri() {
870                 return $this->get_theme_root_uri() . '/' . str_replace( '%2F', '/', rawurlencode( $this->stylesheet ) );
871         }
872
873         /**
874          * Returns the URL to the directory of a theme's "template" files.
875          *
876          * In the case of a child theme, this is the URL to the directory of the
877          * parent theme's files.
878          *
879          * @since 3.4.0
880          * @access public
881          *
882          * @return string URL to the template directory.
883          */
884         public function get_template_directory_uri() {
885                 if ( $this->parent() )
886                         $theme_root_uri = $this->parent()->get_theme_root_uri();
887                 else
888                         $theme_root_uri = $this->get_theme_root_uri();
889
890                 return $theme_root_uri . '/' . str_replace( '%2F', '/', rawurlencode( $this->template ) );
891         }
892
893         /**
894          * The absolute path to the directory of the theme root.
895          *
896          * This is typically the absolute path to wp-content/themes.
897          *
898          * @since 3.4.0
899          * @access public
900          *
901          * @return string Theme root.
902          */
903         public function get_theme_root() {
904                 return $this->theme_root;
905         }
906
907         /**
908          * Returns the URL to the directory of the theme root.
909          *
910          * This is typically the absolute URL to wp-content/themes. This forms the basis
911          * for all other URLs returned by WP_Theme, so we pass it to the public function
912          * get_theme_root_uri() and allow it to run the theme_root_uri filter.
913          *
914          * @since 3.4.0
915          * @access public
916          *
917          * @return string Theme root URI.
918          */
919         public function get_theme_root_uri() {
920                 if ( ! isset( $this->theme_root_uri ) )
921                         $this->theme_root_uri = get_theme_root_uri( $this->stylesheet, $this->theme_root );
922                 return $this->theme_root_uri;
923         }
924
925         /**
926          * Returns the main screenshot file for the theme.
927          *
928          * The main screenshot is called screenshot.png. gif and jpg extensions are also allowed.
929          *
930          * Screenshots for a theme must be in the stylesheet directory. (In the case of child
931          * themes, parent theme screenshots are not inherited.)
932          *
933          * @since 3.4.0
934          * @access public
935          *
936          * @param string $uri Type of URL to return, either 'relative' or an absolute URI. Defaults to absolute URI.
937          * @return string|false Screenshot file. False if the theme does not have a screenshot.
938          */
939         public function get_screenshot( $uri = 'uri' ) {
940                 $screenshot = $this->cache_get( 'screenshot' );
941                 if ( $screenshot ) {
942                         if ( 'relative' == $uri )
943                                 return $screenshot;
944                         return $this->get_stylesheet_directory_uri() . '/' . $screenshot;
945                 } elseif ( 0 === $screenshot ) {
946                         return false;
947                 }
948
949                 foreach ( array( 'png', 'gif', 'jpg', 'jpeg' ) as $ext ) {
950                         if ( file_exists( $this->get_stylesheet_directory() . "/screenshot.$ext" ) ) {
951                                 $this->cache_add( 'screenshot', 'screenshot.' . $ext );
952                                 if ( 'relative' == $uri )
953                                         return 'screenshot.' . $ext;
954                                 return $this->get_stylesheet_directory_uri() . '/' . 'screenshot.' . $ext;
955                         }
956                 }
957
958                 $this->cache_add( 'screenshot', 0 );
959                 return false;
960         }
961
962         /**
963          * Return files in the theme's directory.
964          *
965          * @since 3.4.0
966          * @access public
967          *
968          * @param mixed $type Optional. Array of extensions to return. Defaults to all files (null).
969          * @param int $depth Optional. How deep to search for files. Defaults to a flat scan (0 depth). -1 depth is infinite.
970          * @param bool $search_parent Optional. Whether to return parent files. Defaults to false.
971          * @return array Array of files, keyed by the path to the file relative to the theme's directory, with the values
972          *                   being absolute paths.
973          */
974         public function get_files( $type = null, $depth = 0, $search_parent = false ) {
975                 $files = (array) self::scandir( $this->get_stylesheet_directory(), $type, $depth );
976
977                 if ( $search_parent && $this->parent() )
978                         $files += (array) self::scandir( $this->get_template_directory(), $type, $depth );
979
980                 return $files;
981         }
982
983         /**
984          * Returns the theme's page templates.
985          *
986          * @since 3.4.0
987          * @access public
988          *
989          * @param WP_Post|null $post Optional. The post being edited, provided for context.
990          * @return array Array of page templates, keyed by filename, with the value of the translated header name.
991          */
992         public function get_page_templates( $post = null ) {
993                 // If you screw up your current theme and we invalidate your parent, most things still work. Let it slide.
994                 if ( $this->errors() && $this->errors()->get_error_codes() !== array( 'theme_parent_invalid' ) )
995                         return array();
996
997                 $page_templates = $this->cache_get( 'page_templates' );
998
999                 if ( ! is_array( $page_templates ) ) {
1000                         $page_templates = array();
1001
1002                         $files = (array) $this->get_files( 'php', 1 );
1003
1004                         foreach ( $files as $file => $full_path ) {
1005                                 if ( ! preg_match( '|Template Name:(.*)$|mi', file_get_contents( $full_path ), $header ) )
1006                                         continue;
1007                                 $page_templates[ $file ] = _cleanup_header_comment( $header[1] );
1008                         }
1009
1010                         $this->cache_add( 'page_templates', $page_templates );
1011                 }
1012
1013                 if ( $this->load_textdomain() ) {
1014                         foreach ( $page_templates as &$page_template ) {
1015                                 $page_template = $this->translate_header( 'Template Name', $page_template );
1016                         }
1017                 }
1018
1019                 if ( $this->parent() )
1020                         $page_templates += $this->parent()->get_page_templates( $post );
1021
1022                 /**
1023                  * Filter list of page templates for a theme.
1024                  *
1025                  * @since 3.9.0
1026                  * @since 4.4.0 Converted to allow complete control over the `$page_templates` array.
1027                  *
1028                  * @param array        $page_templates Array of page templates. Keys are filenames,
1029                  *                                     values are translated names.
1030                  * @param WP_Theme     $this           The theme object.
1031                  * @param WP_Post|null $post           The post being edited, provided for context, or null.
1032                  */
1033                 return (array) apply_filters( 'theme_page_templates', $page_templates, $this, $post );
1034         }
1035
1036         /**
1037          * Scans a directory for files of a certain extension.
1038          *
1039          * @since 3.4.0
1040          *
1041          * @static
1042          * @access private
1043          *
1044          * @param string            $path          Absolute path to search.
1045          * @param array|string|null $extensions    Optional. Array of extensions to find, string of a single extension,
1046          *                                         or null for all extensions. Default null.
1047          * @param int               $depth         Optional. How many levels deep to search for files. Accepts 0, 1+, or
1048          *                                         -1 (infinite depth). Default 0.
1049          * @param string            $relative_path Optional. The basename of the absolute path. Used to control the
1050          *                                         returned path for the found files, particularly when this function
1051          *                                         recurses to lower depths. Default empty.
1052          * @return array|false Array of files, keyed by the path to the file relative to the `$path` directory prepended
1053          *                     with `$relative_path`, with the values being absolute paths. False otherwise.
1054          */
1055         private static function scandir( $path, $extensions = null, $depth = 0, $relative_path = '' ) {
1056                 if ( ! is_dir( $path ) )
1057                         return false;
1058
1059                 if ( $extensions ) {
1060                         $extensions = (array) $extensions;
1061                         $_extensions = implode( '|', $extensions );
1062                 }
1063
1064                 $relative_path = trailingslashit( $relative_path );
1065                 if ( '/' == $relative_path )
1066                         $relative_path = '';
1067
1068                 $results = scandir( $path );
1069                 $files = array();
1070
1071                 foreach ( $results as $result ) {
1072                         if ( '.' == $result[0] )
1073                                 continue;
1074                         if ( is_dir( $path . '/' . $result ) ) {
1075                                 if ( ! $depth || 'CVS' == $result )
1076                                         continue;
1077                                 $found = self::scandir( $path . '/' . $result, $extensions, $depth - 1 , $relative_path . $result );
1078                                 $files = array_merge_recursive( $files, $found );
1079                         } elseif ( ! $extensions || preg_match( '~\.(' . $_extensions . ')$~', $result ) ) {
1080                                 $files[ $relative_path . $result ] = $path . '/' . $result;
1081                         }
1082                 }
1083
1084                 return $files;
1085         }
1086
1087         /**
1088          * Loads the theme's textdomain.
1089          *
1090          * Translation files are not inherited from the parent theme. Todo: if this fails for the
1091          * child theme, it should probably try to load the parent theme's translations.
1092          *
1093          * @since 3.4.0
1094          * @access public
1095          *
1096          * @return bool True if the textdomain was successfully loaded or has already been loaded.
1097          *      False if no textdomain was specified in the file headers, or if the domain could not be loaded.
1098          */
1099         public function load_textdomain() {
1100                 if ( isset( $this->textdomain_loaded ) )
1101                         return $this->textdomain_loaded;
1102
1103                 $textdomain = $this->get('TextDomain');
1104                 if ( ! $textdomain ) {
1105                         $this->textdomain_loaded = false;
1106                         return false;
1107                 }
1108
1109                 if ( is_textdomain_loaded( $textdomain ) ) {
1110                         $this->textdomain_loaded = true;
1111                         return true;
1112                 }
1113
1114                 $path = $this->get_stylesheet_directory();
1115                 if ( $domainpath = $this->get('DomainPath') )
1116                         $path .= $domainpath;
1117                 else
1118                         $path .= '/languages';
1119
1120                 $this->textdomain_loaded = load_theme_textdomain( $textdomain, $path );
1121                 return $this->textdomain_loaded;
1122         }
1123
1124         /**
1125          * Whether the theme is allowed (multisite only).
1126          *
1127          * @since 3.4.0
1128          * @access public
1129          *
1130          * @param string $check Optional. Whether to check only the 'network'-wide settings, the 'site'
1131          *      settings, or 'both'. Defaults to 'both'.
1132          * @param int $blog_id Optional. Ignored if only network-wide settings are checked. Defaults to current blog.
1133          * @return bool Whether the theme is allowed for the network. Returns true in single-site.
1134          */
1135         public function is_allowed( $check = 'both', $blog_id = null ) {
1136                 if ( ! is_multisite() )
1137                         return true;
1138
1139                 if ( 'both' == $check || 'network' == $check ) {
1140                         $allowed = self::get_allowed_on_network();
1141                         if ( ! empty( $allowed[ $this->get_stylesheet() ] ) )
1142                                 return true;
1143                 }
1144
1145                 if ( 'both' == $check || 'site' == $check ) {
1146                         $allowed = self::get_allowed_on_site( $blog_id );
1147                         if ( ! empty( $allowed[ $this->get_stylesheet() ] ) )
1148                                 return true;
1149                 }
1150
1151                 return false;
1152         }
1153
1154         /**
1155          * Determines the latest WordPress default theme that is installed.
1156          *
1157          * This hits the filesystem.
1158          *
1159          * @return WP_Theme|false Object, or false if no theme is installed, which would be bad.
1160          */
1161         public static function get_core_default_theme() {
1162                 foreach ( array_reverse( self::$default_themes ) as $slug => $name ) {
1163                         $theme = wp_get_theme( $slug );
1164                         if ( $theme->exists() ) {
1165                                 return $theme;
1166                         }
1167                 }
1168                 return false;
1169         }
1170
1171         /**
1172          * Returns array of stylesheet names of themes allowed on the site or network.
1173          *
1174          * @since 3.4.0
1175          *
1176          * @static
1177          * @access public
1178          *
1179          * @param int $blog_id Optional. Defaults to current blog.
1180          * @return array Array of stylesheet names.
1181          */
1182         public static function get_allowed( $blog_id = null ) {
1183                 /**
1184                  * Filter the array of themes allowed on the site or network.
1185                  *
1186                  * @since MU
1187                  *
1188                  * @param array $allowed_themes An array of theme stylesheet names.
1189                  */
1190                 $network = (array) apply_filters( 'allowed_themes', self::get_allowed_on_network() );
1191                 return $network + self::get_allowed_on_site( $blog_id );
1192         }
1193
1194         /**
1195          * Returns array of stylesheet names of themes allowed on the network.
1196          *
1197          * @since 3.4.0
1198          *
1199          * @static
1200          * @access public
1201          *
1202          * @staticvar array $allowed_themes
1203          *
1204          * @return array Array of stylesheet names.
1205          */
1206         public static function get_allowed_on_network() {
1207                 static $allowed_themes;
1208                 if ( ! isset( $allowed_themes ) )
1209                         $allowed_themes = (array) get_site_option( 'allowedthemes' );
1210                 return $allowed_themes;
1211         }
1212
1213         /**
1214          * Returns array of stylesheet names of themes allowed on the site.
1215          *
1216          * @since 3.4.0
1217          *
1218          * @static
1219          * @access public
1220          *
1221          * @staticvar array $allowed_themes
1222          *
1223          * @param int $blog_id Optional. Defaults to current blog.
1224          * @return array Array of stylesheet names.
1225          */
1226         public static function get_allowed_on_site( $blog_id = null ) {
1227                 static $allowed_themes = array();
1228
1229                 if ( ! $blog_id || ! is_multisite() )
1230                         $blog_id = get_current_blog_id();
1231
1232                 if ( isset( $allowed_themes[ $blog_id ] ) )
1233                         return $allowed_themes[ $blog_id ];
1234
1235                 $current = $blog_id == get_current_blog_id();
1236
1237                 if ( $current ) {
1238                         $allowed_themes[ $blog_id ] = get_option( 'allowedthemes' );
1239                 } else {
1240                         switch_to_blog( $blog_id );
1241                         $allowed_themes[ $blog_id ] = get_option( 'allowedthemes' );
1242                         restore_current_blog();
1243                 }
1244
1245                 // This is all super old MU back compat joy.
1246                 // 'allowedthemes' keys things by stylesheet. 'allowed_themes' keyed things by name.
1247                 if ( false === $allowed_themes[ $blog_id ] ) {
1248                         if ( $current ) {
1249                                 $allowed_themes[ $blog_id ] = get_option( 'allowed_themes' );
1250                         } else {
1251                                 switch_to_blog( $blog_id );
1252                                 $allowed_themes[ $blog_id ] = get_option( 'allowed_themes' );
1253                                 restore_current_blog();
1254                         }
1255
1256                         if ( ! is_array( $allowed_themes[ $blog_id ] ) || empty( $allowed_themes[ $blog_id ] ) ) {
1257                                 $allowed_themes[ $blog_id ] = array();
1258                         } else {
1259                                 $converted = array();
1260                                 $themes = wp_get_themes();
1261                                 foreach ( $themes as $stylesheet => $theme_data ) {
1262                                         if ( isset( $allowed_themes[ $blog_id ][ $theme_data->get('Name') ] ) )
1263                                                 $converted[ $stylesheet ] = true;
1264                                 }
1265                                 $allowed_themes[ $blog_id ] = $converted;
1266                         }
1267                         // Set the option so we never have to go through this pain again.
1268                         if ( is_admin() && $allowed_themes[ $blog_id ] ) {
1269                                 if ( $current ) {
1270                                         update_option( 'allowedthemes', $allowed_themes[ $blog_id ] );
1271                                         delete_option( 'allowed_themes' );
1272                                 } else {
1273                                         switch_to_blog( $blog_id );
1274                                         update_option( 'allowedthemes', $allowed_themes[ $blog_id ] );
1275                                         delete_option( 'allowed_themes' );
1276                                         restore_current_blog();
1277                                 }
1278                         }
1279                 }
1280
1281                 return (array) $allowed_themes[ $blog_id ];
1282         }
1283
1284         /**
1285          * Sort themes by name.
1286          *
1287          * @since 3.4.0
1288          *
1289          * @static
1290          * @access public
1291          */
1292         public static function sort_by_name( &$themes ) {
1293                 if ( 0 === strpos( get_locale(), 'en_' ) ) {
1294                         uasort( $themes, array( 'WP_Theme', '_name_sort' ) );
1295                 } else {
1296                         uasort( $themes, array( 'WP_Theme', '_name_sort_i18n' ) );
1297                 }
1298         }
1299
1300         /**
1301          * Callback function for usort() to naturally sort themes by name.
1302          *
1303          * Accesses the Name header directly from the class for maximum speed.
1304          * Would choke on HTML but we don't care enough to slow it down with strip_tags().
1305          *
1306          * @since 3.4.0
1307          *
1308          * @static
1309          * @access private
1310          *
1311          * @return int
1312          */
1313         private static function _name_sort( $a, $b ) {
1314                 return strnatcasecmp( $a->headers['Name'], $b->headers['Name'] );
1315         }
1316
1317         /**
1318          * Name sort (with translation).
1319          *
1320          * @since 3.4.0
1321          *
1322          * @static
1323          * @access private
1324          *
1325          * @return int
1326          */
1327         private static function _name_sort_i18n( $a, $b ) {
1328                 // Don't mark up; Do translate.
1329                 return strnatcasecmp( $a->display( 'Name', false, true ), $b->display( 'Name', false, true ) );
1330         }
1331 }