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