Completed
Push — master ( a7cd2a...eabd6c )
by Stephen
38:42
created

WP_Theme::get_files()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 8
rs 9.4285
cc 3
eloc 5
nc 2
nop 3
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;
0 ignored issues
show
Documentation Bug introduced by
The property $cache_expiration was declared of type boolean, but self::$persistently_cache is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
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 ) );
0 ignored issues
show
Documentation introduced by
array('headers' => $this...te' => $this->template) is of type array<string,array<strin...","template":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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 ) );
0 ignored issues
show
Documentation introduced by
array('headers' => $this...te' => $this->template) is of type array<string,array<strin...","template":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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 ) );
0 ignored issues
show
Documentation introduced by
array('headers' => $this...te' => $this->template) is of type array<string,array|objec...","template":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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 ) );
0 ignored issues
show
Documentation introduced by
array('headers' => $this...te' => $this->template) is of type array<string,?,{"headers...tring","template":"?"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
299
				$this->parent = new WP_Theme( $this->template, $this->theme_root, $this );
0 ignored issues
show
Documentation introduced by
$this is of type this<WP_Theme>, but the function expects a object<WP_Error>|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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 ) );
0 ignored issues
show
Documentation introduced by
array('headers' => $this...te' => $this->template) is of type array<string,?,{"headers...tring","template":"?"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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 );
0 ignored issues
show
Documentation introduced by
$this is of type this<WP_Theme>, but the function expects a object<WP_Error>|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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 );
0 ignored issues
show
Documentation introduced by
$cache is of type array<string,?>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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 );
0 ignored issues
show
Documentation introduced by
self::$cache_expiration is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->name_translated = null of type null is incompatible with the declared type array of property $headers_sanitized.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
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' );
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->cache_get('headers') of type * is incompatible with the declared type array of property $headers_sanitized.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
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 );
0 ignored issues
show
Documentation introduced by
$this->headers_sanitized is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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 );
0 ignored issues
show
Documentation introduced by
$translate is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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') ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->get('AuthorURI') of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
729
					$value = sprintf( '<a href="%1$s">%2$s</a>', $this->display( 'AuthorURI', true, $translate ), $value );
0 ignored issues
show
Documentation introduced by
$translate is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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' ) );
0 ignored issues
show
Security Bug introduced by
It seems like $this->get('TextDomain') targeting WP_Theme::get() can also be of type false; however, translate() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
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 ) {
0 ignored issues
show
Bug introduced by
The expression $value of type string is not traversable.
Loading history...
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') );
0 ignored issues
show
Security Bug introduced by
It seems like $this->get('TextDomain') targeting WP_Theme::get() can also be of type false; however, translate() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
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 );
0 ignored issues
show
Documentation introduced by
$page_templates is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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 ) ) {
0 ignored issues
show
Bug introduced by
The variable $_extensions does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
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 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $textdomain of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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 View Code Duplication
		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 View Code Duplication
		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() )
0 ignored issues
show
Bug Best Practice introduced by
The expression $blog_id of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
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 View Code Duplication
		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 View Code Duplication
			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 View Code Duplication
				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 ) );
0 ignored issues
show
Bug introduced by
The method display cannot be called on $a (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
Bug introduced by
The method display cannot be called on $b (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
1372
	}
1373
}
1374