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