WordPress.org

Make WordPress Core

Ticket #26206: class-wp-theme.php

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