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