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