Make WordPress Core

source: trunk/src/wp-includes/class-wp-theme.php @ 34995

Last change on this file since 34995 was 34995, checked in by DrewAPicture, 7 years ago

Template: Make it possible to both add and remove items from the page templates list using the theme_page_templates filter.

The theme_page_templates hook was originally added in [27297] as page_templates, and later renamed in [27470]. Previously, it was only possible to remove or rename page templates via this hook.

Fixes #13265. Fixes #25879.

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