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