Make WordPress Core

Ticket #26206: class-wp-theme.php

File class-wp-theme.php, 37.6 KB (added by richard2222, 13 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}