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