Issues (4967)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/wp-includes/class-wp-term-query.php (5 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/**
4
 * Taxonomy API: WP_Term_Query class.
5
 *
6
 * @package WordPress
7
 * @subpackage Taxonomy
8
 * @since 4.6.0
9
 */
10
11
/**
12
 * Class used for querying terms.
13
 *
14
 * @since 4.6.0
15
 *
16
 * @see WP_Term_Query::__construct() for accepted arguments.
17
 */
18
class WP_Term_Query {
19
20
	/**
21
	 * SQL string used to perform database query.
22
	 *
23
	 * @since 4.6.0
24
	 * @access public
25
	 * @var string
26
	 */
27
	public $request;
28
29
	/**
30
	 * Metadata query container.
31
	 *
32
	 * @since 4.6.0
33
	 * @access public
34
	 * @var object WP_Meta_Query
35
	 */
36
	public $meta_query = false;
37
38
	/**
39
	 * Metadata query clauses.
40
	 *
41
	 * @since 4.6.0
42
	 * @access protected
43
	 * @var array
44
	 */
45
	protected $meta_query_clauses;
46
47
	/**
48
	 * SQL query clauses.
49
	 *
50
	 * @since 4.6.0
51
	 * @access protected
52
	 * @var array
53
	 */
54
	protected $sql_clauses = array(
55
		'select'  => '',
56
		'from'    => '',
57
		'where'   => array(),
58
		'orderby' => '',
59
		'limits'  => '',
60
	);
61
62
	/**
63
	 * Query vars set by the user.
64
	 *
65
	 * @since 4.6.0
66
	 * @access public
67
	 * @var array
68
	 */
69
	public $query_vars;
70
71
	/**
72
	 * Default values for query vars.
73
	 *
74
	 * @since 4.6.0
75
	 * @access public
76
	 * @var array
77
	 */
78
	public $query_var_defaults;
79
80
	/**
81
	 * List of terms located by the query.
82
	 *
83
	 * @since 4.6.0
84
	 * @access public
85
	 * @var array
86
	 */
87
	public $terms;
88
89
	/**
90
	 * Constructor.
91
	 *
92
	 * Sets up the term query, based on the query vars passed.
93
	 *
94
	 * @since 4.6.0
95
	 * @since 4.6.0 Introduced 'term_taxonomy_id' parameter.
96
	 * @since 4.7.0 Introduced 'object_ids' parameter.
97
	 * @access public
98
	 *
99
	 * @param string|array $query {
100
	 *     Optional. Array or query string of term query parameters. Default empty.
101
	 *
102
	 *     @type string|array $taxonomy               Taxonomy name, or array of taxonomies, to which results should
103
	 *                                                be limited.
104
	 *     @type int|array    $object_ids             Optional. Object ID, or array of object IDs. Results will be
105
	 *                                                limited to terms associated with these objects.
106
	 *     @type string       $orderby                Field(s) to order terms by. Accepts term fields ('name',
107
	 *                                                'slug', 'term_group', 'term_id', 'id', 'description', 'parent'),
108
	 *                                                'count' for term taxonomy count, 'include' to match the
109
	 *                                                'order' of the $include param, 'meta_value', 'meta_value_num',
110
	 *                                                the value of `$meta_key`, the array keys of `$meta_query`, or
111
	 *                                                'none' to omit the ORDER BY clause. Defaults to 'name'.
112
	 *     @type string       $order                  Whether to order terms in ascending or descending order.
113
	 *                                                Accepts 'ASC' (ascending) or 'DESC' (descending).
114
	 *                                                Default 'ASC'.
115
	 *     @type bool|int     $hide_empty             Whether to hide terms not assigned to any posts. Accepts
116
	 *                                                1|true or 0|false. Default 1|true.
117
	 *     @type array|string $include                Array or comma/space-separated string of term ids to include.
118
	 *                                                Default empty array.
119
	 *     @type array|string $exclude                Array or comma/space-separated string of term ids to exclude.
120
	 *                                                If $include is non-empty, $exclude is ignored.
121
	 *                                                Default empty array.
122
	 *     @type array|string $exclude_tree           Array or comma/space-separated string of term ids to exclude
123
	 *                                                along with all of their descendant terms. If $include is
124
	 *                                                non-empty, $exclude_tree is ignored. Default empty array.
125
	 *     @type int|string   $number                 Maximum number of terms to return. Accepts ''|0 (all) or any
126
	 *                                                positive number. Default ''|0 (all).
127
	 *     @type int          $offset                 The number by which to offset the terms query. Default empty.
128
	 *     @type string       $fields                 Term fields to query for. Accepts 'all' (returns an array of
129
	 *                                                complete term objects), 'all_with_object_id' (returns an
130
	 *                                                array of term objects with the 'object_id' param; only works
131
	 *                                                when the `$fields` parameter is 'object_ids' ), 'ids'
132
	 *                                                (returns an array of ids), 'tt_ids' (returns an array of
133
	 *                                                term taxonomy ids), 'id=>parent' (returns an associative
134
	 *                                                array with ids as keys, parent term IDs as values), 'names'
135
	 *                                                (returns an array of term names), 'count' (returns the number
136
	 *                                                of matching terms), 'id=>name' (returns an associative array
137
	 *                                                with ids as keys, term names as values), or 'id=>slug'
138
	 *                                                (returns an associative array with ids as keys, term slugs
139
	 *                                                as values). Default 'all'.
140
	 *     @type bool         $count                  Whether to return a term count (true) or array of term objects
141
	 *                                                (false). Will take precedence over `$fields` if true.
142
	 *                                                Default false.
143
	 *     @type string|array $name                   Optional. Name or array of names to return term(s) for.
144
	 *                                                Default empty.
145
	 *     @type string|array $slug                   Optional. Slug or array of slugs to return term(s) for.
146
	 *                                                Default empty.
147
	 *     @type int|array    $term_taxonomy_id       Optional. Term taxonomy ID, or array of term taxonomy IDs,
148
	 *                                                to match when querying terms.
149
	 *     @type bool         $hierarchical           Whether to include terms that have non-empty descendants (even
150
	 *                                                if $hide_empty is set to true). Default true.
151
	 *     @type string       $search                 Search criteria to match terms. Will be SQL-formatted with
152
	 *                                                wildcards before and after. Default empty.
153
	 *     @type string       $name__like             Retrieve terms with criteria by which a term is LIKE
154
	 *                                                `$name__like`. Default empty.
155
	 *     @type string       $description__like      Retrieve terms where the description is LIKE
156
	 *                                                `$description__like`. Default empty.
157
	 *     @type bool         $pad_counts             Whether to pad the quantity of a term's children in the
158
	 *                                                quantity of each term's "count" object variable.
159
	 *                                                Default false.
160
	 *     @type string       $get                    Whether to return terms regardless of ancestry or whether the
161
	 *                                                terms are empty. Accepts 'all' or empty (disabled).
162
	 *                                                Default empty.
163
	 *     @type int          $child_of               Term ID to retrieve child terms of. If multiple taxonomies
164
	 *                                                are passed, $child_of is ignored. Default 0.
165
	 *     @type int|string   $parent                 Parent term ID to retrieve direct-child terms of.
166
	 *                                                Default empty.
167
	 *     @type bool         $childless              True to limit results to terms that have no children.
168
	 *                                                This parameter has no effect on non-hierarchical taxonomies.
169
	 *                                                Default false.
170
	 *     @type string       $cache_domain           Unique cache key to be produced when this query is stored in
171
	 *                                                an object cache. Default is 'core'.
172
	 *     @type bool         $update_term_meta_cache Whether to prime meta caches for matched terms. Default true.
173
	 *     @type array        $meta_query             Optional. Meta query clauses to limit retrieved terms by.
174
	 *                                                See `WP_Meta_Query`. Default empty.
175
	 *     @type string       $meta_key               Limit terms to those matching a specific metadata key.
176
	 *                                                Can be used in conjunction with `$meta_value`. Default empty.
177
	 *     @type string       $meta_value             Limit terms to those matching a specific metadata value.
178
	 *                                                Usually used in conjunction with `$meta_key`. Default empty.
179
	 *     @type string       $meta_type              Type of object metadata is for (e.g., comment, post, or user).
180
	 *                                                Default empty.
181
	 *     @type string       $meta_compare           Comparison operator to test the 'meta_value'. Default empty.
182
	 * }
183
	 */
184
	public function __construct( $query = '' ) {
185
		$this->query_var_defaults = array(
186
			'taxonomy'               => null,
187
			'object_ids'             => null,
188
			'orderby'                => 'name',
189
			'order'                  => 'ASC',
190
			'hide_empty'             => true,
191
			'include'                => array(),
192
			'exclude'                => array(),
193
			'exclude_tree'           => array(),
194
			'number'                 => '',
195
			'offset'                 => '',
196
			'fields'                 => 'all',
197
			'count'                  => false,
198
			'name'                   => '',
199
			'slug'                   => '',
200
			'term_taxonomy_id'       => '',
201
			'hierarchical'           => true,
202
			'search'                 => '',
203
			'name__like'             => '',
204
			'description__like'      => '',
205
			'pad_counts'             => false,
206
			'get'                    => '',
207
			'child_of'               => 0,
208
			'parent'                 => '',
209
			'childless'              => false,
210
			'cache_domain'           => 'core',
211
			'update_term_meta_cache' => true,
212
			'meta_query'             => '',
213
			'meta_key'               => '',
214
			'meta_value'             => '',
215
			'meta_type'              => '',
216
			'meta_compare'           => '',
217
		);
218
219
		if ( ! empty( $query ) ) {
220
			$this->query( $query );
221
		}
222
	}
223
224
	/**
225
	 * Parse arguments passed to the term query with default query parameters.
226
	 *
227
	 * @since 4.6.0
228
	 * @access public
229
	 *
230
	 * @param string|array $query WP_Term_Query arguments. See WP_Term_Query::__construct()
231
	 */
232
	public function parse_query( $query = '' ) {
233
		if ( empty( $query ) ) {
234
			$query = $this->query_vars;
235
		}
236
237
		$taxonomies = isset( $query['taxonomy'] ) ? (array) $query['taxonomy'] : null;
238
239
		/**
240
		 * Filters the terms query default arguments.
241
		 *
242
		 * Use {@see 'get_terms_args'} to filter the passed arguments.
243
		 *
244
		 * @since 4.4.0
245
		 *
246
		 * @param array $defaults   An array of default get_terms() arguments.
247
		 * @param array $taxonomies An array of taxonomies.
248
		 */
249
		$this->query_var_defaults = apply_filters( 'get_terms_defaults', $this->query_var_defaults, $taxonomies );
0 ignored issues
show
Documentation Bug introduced by
It seems like apply_filters('get_terms..._defaults, $taxonomies) of type * is incompatible with the declared type array of property $query_var_defaults.

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...
250
251
		$query = wp_parse_args( $query, $this->query_var_defaults );
252
253
		$query['number'] = absint( $query['number'] );
254
		$query['offset'] = absint( $query['offset'] );
255
256
		// 'parent' overrides 'child_of'.
257
		if ( 0 < intval( $query['parent'] ) ) {
258
			$query['child_of'] = false;
259
		}
260
261 View Code Duplication
		if ( 'all' == $query['get'] ) {
262
			$query['childless'] = false;
263
			$query['child_of'] = 0;
264
			$query['hide_empty'] = 0;
265
			$query['hierarchical'] = false;
266
			$query['pad_counts'] = false;
267
		}
268
269
		$query['taxonomy'] = $taxonomies;
270
271
		$this->query_vars = $query;
272
273
		/**
274
		 * Fires after term query vars have been parsed.
275
		 *
276
		 * @since 4.6.0
277
		 *
278
		 * @param WP_Term_Query $this Current instance of WP_Term_Query.
279
		 */
280
		do_action( 'parse_term_query', $this );
281
	}
282
283
	/**
284
	 * Sets up the query for retrieving terms.
285
	 *
286
	 * @since 4.6.0
287
	 * @access public
288
	 *
289
	 * @param string|array $query Array or URL query string of parameters.
290
	 * @return array|int List of terms, or number of terms when 'count' is passed as a query var.
291
	 */
292
	public function query( $query ) {
293
		$this->query_vars = wp_parse_args( $query );
294
		return $this->get_terms();
295
	}
296
297
	/**
298
	 * Get terms, based on query_vars.
299
	 *
300
	 * @since 4.6.0
301
	 * @access public
302
	 *
303
	 * @global wpdb $wpdb WordPress database abstraction object.
304
	 *
305
	 * @return array List of terms.
306
	 */
307
	public function get_terms() {
308
		global $wpdb;
309
310
		$this->parse_query( $this->query_vars );
311
		$args = &$this->query_vars;
312
313
		// Set up meta_query so it's available to 'pre_get_terms'.
314
		$this->meta_query = new WP_Meta_Query();
315
		$this->meta_query->parse_query_vars( $args );
316
317
		/**
318
		 * Fires before terms are retrieved.
319
		 *
320
		 * @since 4.6.0
321
		 *
322
		 * @param WP_Term_Query $this Current instance of WP_Term_Query.
323
		 */
324
		do_action( 'pre_get_terms', $this );
325
326
		$taxonomies = (array) $args['taxonomy'];
327
328
		// Save queries by not crawling the tree in the case of multiple taxes or a flat tax.
329
		$has_hierarchical_tax = false;
330
		if ( $taxonomies ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $taxonomies of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
331
			foreach ( $taxonomies as $_tax ) {
332
				if ( is_taxonomy_hierarchical( $_tax ) ) {
333
					$has_hierarchical_tax = true;
334
				}
335
			}
336
		}
337
338
		if ( ! $has_hierarchical_tax ) {
339
			$args['hierarchical'] = false;
340
			$args['pad_counts'] = false;
341
		}
342
343
		// 'parent' overrides 'child_of'.
344
		if ( 0 < intval( $args['parent'] ) ) {
345
			$args['child_of'] = false;
346
		}
347
348 View Code Duplication
		if ( 'all' == $args['get'] ) {
349
			$args['childless'] = false;
350
			$args['child_of'] = 0;
351
			$args['hide_empty'] = 0;
352
			$args['hierarchical'] = false;
353
			$args['pad_counts'] = false;
354
		}
355
356
		/**
357
		 * Filters the terms query arguments.
358
		 *
359
		 * @since 3.1.0
360
		 *
361
		 * @param array $args       An array of get_terms() arguments.
362
		 * @param array $taxonomies An array of taxonomies.
363
		 */
364
		$args = apply_filters( 'get_terms_args', $args, $taxonomies );
365
366
		// Avoid the query if the queried parent/child_of term has no descendants.
367
		$child_of = $args['child_of'];
368
		$parent   = $args['parent'];
369
370
		if ( $child_of ) {
371
			$_parent = $child_of;
372
		} elseif ( $parent ) {
373
			$_parent = $parent;
374
		} else {
375
			$_parent = false;
376
		}
377
378
		if ( $_parent ) {
379
			$in_hierarchy = false;
380
			foreach ( $taxonomies as $_tax ) {
381
				$hierarchy = _get_term_hierarchy( $_tax );
382
383
				if ( isset( $hierarchy[ $_parent ] ) ) {
384
					$in_hierarchy = true;
385
				}
386
			}
387
388
			if ( ! $in_hierarchy ) {
389
				return array();
390
			}
391
		}
392
393
		// 'term_order' is a legal sort order only when joining the relationship table.
394
		$_orderby = $this->query_vars['orderby'];
395
		if ( 'term_order' === $_orderby && empty( $this->query_vars['object_ids'] ) ) {
396
			$_orderby = 'term_id';
397
		}
398
		$orderby = $this->parse_orderby( $_orderby );
399
400
		if ( $orderby ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $orderby 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...
401
			$orderby = "ORDER BY $orderby";
402
		}
403
404
		$order = $this->parse_order( $this->query_vars['order'] );
405
406
		if ( $taxonomies ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $taxonomies of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
407
			$this->sql_clauses['where']['taxonomy'] = "tt.taxonomy IN ('" . implode( "', '", array_map( 'esc_sql', $taxonomies ) ) . "')";
408
		}
409
410
		$exclude      = $args['exclude'];
411
		$exclude_tree = $args['exclude_tree'];
412
		$include      = $args['include'];
413
414
		$inclusions = '';
415
		if ( ! empty( $include ) ) {
416
			$exclude = '';
417
			$exclude_tree = '';
418
			$inclusions = implode( ',', wp_parse_id_list( $include ) );
419
		}
420
421
		if ( ! empty( $inclusions ) ) {
422
			$this->sql_clauses['where']['inclusions'] = 't.term_id IN ( ' . $inclusions . ' )';
423
		}
424
425
		$exclusions = array();
426
		if ( ! empty( $exclude_tree ) ) {
427
			$exclude_tree = wp_parse_id_list( $exclude_tree );
428
			$excluded_children = $exclude_tree;
429
			foreach ( $exclude_tree as $extrunk ) {
430
				$excluded_children = array_merge(
431
					$excluded_children,
432
					(array) get_terms( $taxonomies[0], array(
433
						'child_of' => intval( $extrunk ),
434
						'fields' => 'ids',
435
						'hide_empty' => 0
436
					) )
437
				);
438
			}
439
			$exclusions = array_merge( $excluded_children, $exclusions );
440
		}
441
442
		if ( ! empty( $exclude ) ) {
443
			$exclusions = array_merge( wp_parse_id_list( $exclude ), $exclusions );
444
		}
445
446
		// 'childless' terms are those without an entry in the flattened term hierarchy.
447
		$childless = (bool) $args['childless'];
448
		if ( $childless ) {
449
			foreach ( $taxonomies as $_tax ) {
450
				$term_hierarchy = _get_term_hierarchy( $_tax );
451
				$exclusions = array_merge( array_keys( $term_hierarchy ), $exclusions );
452
			}
453
		}
454
455
		if ( ! empty( $exclusions ) ) {
456
			$exclusions = 't.term_id NOT IN (' . implode( ',', array_map( 'intval', $exclusions ) ) . ')';
457
		} else {
458
			$exclusions = '';
459
		}
460
461
		/**
462
		 * Filters the terms to exclude from the terms query.
463
		 *
464
		 * @since 2.3.0
465
		 *
466
		 * @param string $exclusions `NOT IN` clause of the terms query.
467
		 * @param array  $args       An array of terms query arguments.
468
		 * @param array  $taxonomies An array of taxonomies.
469
		 */
470
		$exclusions = apply_filters( 'list_terms_exclusions', $exclusions, $args, $taxonomies );
471
472
		if ( ! empty( $exclusions ) ) {
473
			// Must do string manipulation here for backward compatibility with filter.
474
			$this->sql_clauses['where']['exclusions'] = preg_replace( '/^\s*AND\s*/', '', $exclusions );
475
		}
476
477
		if (
478
			( ! empty( $args['name'] ) ) ||
479
			( is_string( $args['name'] ) && 0 !== strlen( $args['name'] ) )
480
		) {
481
			$names = (array) $args['name'];
482
			foreach ( $names as &$_name ) {
483
				// `sanitize_term_field()` returns slashed data.
484
				$_name = stripslashes( sanitize_term_field( 'name', $_name, 0, reset( $taxonomies ), 'db' ) );
485
			}
486
487
			$this->sql_clauses['where']['name'] = "t.name IN ('" . implode( "', '", array_map( 'esc_sql', $names ) ) . "')";
488
		}
489
490
		if (
491
			( ! empty( $args['slug'] ) ) ||
492
			( is_string( $args['slug'] ) && 0 !== strlen( $args['slug'] ) )
493
		) {
494
			if ( is_array( $args['slug'] ) ) {
495
				$slug = array_map( 'sanitize_title', $args['slug'] );
496
				$this->sql_clauses['where']['slug'] = "t.slug IN ('" . implode( "', '", $slug ) . "')";
497
			} else {
498
				$slug = sanitize_title( $args['slug'] );
499
				$this->sql_clauses['where']['slug'] = "t.slug = '$slug'";
500
			}
501
		}
502
503
		if ( ! empty( $args['term_taxonomy_id'] ) ) {
504
			if ( is_array( $args['term_taxonomy_id'] ) ) {
505
				$tt_ids = implode( ',', array_map( 'intval', $args['term_taxonomy_id'] ) );
506
				$this->sql_clauses['where']['term_taxonomy_id'] = "tt.term_taxonomy_id IN ({$tt_ids})";
507
			} else {
508
				$this->sql_clauses['where']['term_taxonomy_id'] = $wpdb->prepare( "tt.term_taxonomy_id = %d", $args['term_taxonomy_id'] );
509
			}
510
		}
511
512 View Code Duplication
		if ( ! empty( $args['name__like'] ) ) {
513
			$this->sql_clauses['where']['name__like'] = $wpdb->prepare( "t.name LIKE %s", '%' . $wpdb->esc_like( $args['name__like'] ) . '%' );
514
		}
515
516 View Code Duplication
		if ( ! empty( $args['description__like'] ) ) {
517
			$this->sql_clauses['where']['description__like'] = $wpdb->prepare( "tt.description LIKE %s", '%' . $wpdb->esc_like( $args['description__like'] ) . '%' );
518
		}
519
520
		if ( ! empty( $args['object_ids'] ) ) {
521
			$object_ids = $args['object_ids'];
522
			if ( ! is_array( $object_ids ) ) {
523
				$object_ids = array( $object_ids );
524
			}
525
526
			$object_ids = implode( ', ', array_map( 'intval', $object_ids ) );
527
			$this->sql_clauses['where']['object_ids'] = "tr.object_id IN ($object_ids)";
528
		}
529
530
		/*
531
		 * When querying for object relationships, the 'count > 0' check
532
		 * added by 'hide_empty' is superfluous.
533
		 */
534
		if ( ! empty( $args['object_ids'] ) ) {
535
			$args['hide_empty'] = false;
536
		}
537
538
		if ( '' !== $parent ) {
539
			$parent = (int) $parent;
540
			$this->sql_clauses['where']['parent'] = "tt.parent = '$parent'";
541
		}
542
543
		$hierarchical = $args['hierarchical'];
544
		if ( 'count' == $args['fields'] ) {
545
			$hierarchical = false;
546
		}
547
		if ( $args['hide_empty'] && !$hierarchical ) {
548
			$this->sql_clauses['where']['count'] = 'tt.count > 0';
549
		}
550
551
		$number = $args['number'];
552
		$offset = $args['offset'];
553
554
		// Don't limit the query results when we have to descend the family tree.
555
		if ( $number && ! $hierarchical && ! $child_of && '' === $parent ) {
556 View Code Duplication
			if ( $offset ) {
557
				$limits = 'LIMIT ' . $offset . ',' . $number;
558
			} else {
559
				$limits = 'LIMIT ' . $number;
560
			}
561
		} else {
562
			$limits = '';
563
		}
564
565
566
		if ( ! empty( $args['search'] ) ) {
567
			$this->sql_clauses['where']['search'] = $this->get_search_sql( $args['search'] );
568
		}
569
570
		// Meta query support.
571
		$join = '';
572
		$distinct = '';
573
574
		// Reparse meta_query query_vars, in case they were modified in a 'pre_get_terms' callback.
575
		$this->meta_query->parse_query_vars( $this->query_vars );
576
		$mq_sql = $this->meta_query->get_sql( 'term', 't', 'term_id' );
577
		$meta_clauses = $this->meta_query->get_clauses();
578
579
		if ( ! empty( $meta_clauses ) ) {
580
			$join .= $mq_sql['join'];
581
			$this->sql_clauses['where']['meta_query'] = preg_replace( '/^\s*AND\s*/', '', $mq_sql['where'] );
582
			$distinct .= "DISTINCT";
583
584
		}
585
586
		$selects = array();
587
		switch ( $args['fields'] ) {
588
			case 'all':
589
			case 'all_with_object_id' :
590
			case 'tt_ids' :
591
			case 'slugs' :
592
				$selects = array( 't.*', 'tt.*' );
593
				if ( 'all_with_object_id' === $args['fields'] && ! empty( $args['object_ids'] ) ) {
594
					$selects[] = 'tr.object_id';
595
				}
596
				break;
597
			case 'ids':
598
			case 'id=>parent':
599
				$selects = array( 't.term_id', 'tt.parent', 'tt.count', 'tt.taxonomy' );
600
				break;
601
			case 'names':
602
				$selects = array( 't.term_id', 'tt.parent', 'tt.count', 't.name', 'tt.taxonomy' );
603
				break;
604
			case 'count':
605
				$orderby = '';
606
				$order = '';
607
				$selects = array( 'COUNT(*)' );
608
				break;
609
			case 'id=>name':
610
				$selects = array( 't.term_id', 't.name', 'tt.count', 'tt.taxonomy' );
611
				break;
612
			case 'id=>slug':
613
				$selects = array( 't.term_id', 't.slug', 'tt.count', 'tt.taxonomy' );
614
				break;
615
		}
616
617
		$_fields = $args['fields'];
618
619
		/**
620
		 * Filters the fields to select in the terms query.
621
		 *
622
		 * Field lists modified using this filter will only modify the term fields returned
623
		 * by the function when the `$fields` parameter set to 'count' or 'all'. In all other
624
		 * cases, the term fields in the results array will be determined by the `$fields`
625
		 * parameter alone.
626
		 *
627
		 * Use of this filter can result in unpredictable behavior, and is not recommended.
628
		 *
629
		 * @since 2.8.0
630
		 *
631
		 * @param array $selects    An array of fields to select for the terms query.
632
		 * @param array $args       An array of term query arguments.
633
		 * @param array $taxonomies An array of taxonomies.
634
		 */
635
		$fields = implode( ', ', apply_filters( 'get_terms_fields', $selects, $args, $taxonomies ) );
636
637
		$join .= " INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id";
638
639
		if ( ! empty( $this->query_vars['object_ids'] ) ) {
640
			$join .= " INNER JOIN {$wpdb->term_relationships} AS tr ON tr.term_taxonomy_id = tt.term_taxonomy_id";
641
		}
642
643
		$where = implode( ' AND ', $this->sql_clauses['where'] );
644
645
		/**
646
		 * Filters the terms query SQL clauses.
647
		 *
648
		 * @since 3.1.0
649
		 *
650
		 * @param array $pieces     Terms query SQL clauses.
651
		 * @param array $taxonomies An array of taxonomies.
652
		 * @param array $args       An array of terms query arguments.
653
		 */
654
		$clauses = apply_filters( 'terms_clauses', compact( 'fields', 'join', 'where', 'distinct', 'orderby', 'order', 'limits' ), $taxonomies, $args );
655
656
		$fields = isset( $clauses[ 'fields' ] ) ? $clauses[ 'fields' ] : '';
657
		$join = isset( $clauses[ 'join' ] ) ? $clauses[ 'join' ] : '';
658
		$where = isset( $clauses[ 'where' ] ) ? $clauses[ 'where' ] : '';
659
		$distinct = isset( $clauses[ 'distinct' ] ) ? $clauses[ 'distinct' ] : '';
660
		$orderby = isset( $clauses[ 'orderby' ] ) ? $clauses[ 'orderby' ] : '';
661
		$order = isset( $clauses[ 'order' ] ) ? $clauses[ 'order' ] : '';
662
		$limits = isset( $clauses[ 'limits' ] ) ? $clauses[ 'limits' ] : '';
663
664
		if ( $where ) {
665
			$where = "WHERE $where";
666
		}
667
668
		$this->sql_clauses['select']  = "SELECT $distinct $fields";
669
		$this->sql_clauses['from']    = "FROM $wpdb->terms AS t $join";
670
		$this->sql_clauses['orderby'] = $orderby ? "$orderby $order" : '';
671
		$this->sql_clauses['limits']  = $limits;
672
673
		$this->request = "{$this->sql_clauses['select']} {$this->sql_clauses['from']} {$where} {$this->sql_clauses['orderby']} {$this->sql_clauses['limits']}";
674
675
		// $args can be anything. Only use the args defined in defaults to compute the key.
676
		$key = md5( serialize( wp_array_slice_assoc( $args, array_keys( $this->query_var_defaults ) ) ) . serialize( $taxonomies ) . $this->request );
677
		$last_changed = wp_cache_get_last_changed( 'terms' );
678
		$cache_key = "get_terms:$key:$last_changed";
679
		$cache = wp_cache_get( $cache_key, 'terms' );
680
		if ( false !== $cache ) {
681
			if ( 'all' === $_fields ) {
682
				$cache = array_map( 'get_term', $cache );
683
			}
684
685
			$this->terms = $cache;
0 ignored issues
show
Documentation Bug introduced by
It seems like $cache of type * is incompatible with the declared type array of property $terms.

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...
686
			return $this->terms;
687
		}
688
689
		if ( 'count' == $_fields ) {
690
			$count = $wpdb->get_var( $this->request );
691
			wp_cache_set( $cache_key, $count, 'terms' );
692
			return $count;
693
		}
694
695
		$terms = $wpdb->get_results( $this->request );
696
		if ( 'all' == $_fields || 'all_with_object_id' === $_fields ) {
697
			update_term_cache( $terms );
698
		}
699
700
		// Prime termmeta cache.
701
		if ( $args['update_term_meta_cache'] ) {
702
			$term_ids = wp_list_pluck( $terms, 'term_id' );
703
			update_termmeta_cache( $term_ids );
704
		}
705
706
		if ( empty( $terms ) ) {
707
			wp_cache_add( $cache_key, array(), 'terms', DAY_IN_SECONDS );
708
			return array();
709
		}
710
711
		if ( $child_of ) {
712
			foreach ( $taxonomies as $_tax ) {
713
				$children = _get_term_hierarchy( $_tax );
714
				if ( ! empty( $children ) ) {
715
					$terms = _get_term_children( $child_of, $terms, $_tax );
716
				}
717
			}
718
		}
719
720
		// Update term counts to include children.
721
		if ( $args['pad_counts'] && 'all' == $_fields ) {
722
			foreach ( $taxonomies as $_tax ) {
723
				_pad_term_counts( $terms, $_tax );
724
			}
725
		}
726
727
		// Make sure we show empty categories that have children.
728
		if ( $hierarchical && $args['hide_empty'] && is_array( $terms ) ) {
729
			foreach ( $terms as $k => $term ) {
730
				if ( ! $term->count ) {
731
					$children = get_term_children( $term->term_id, $term->taxonomy );
732
					if ( is_array( $children ) ) {
733
						foreach ( $children as $child_id ) {
734
							$child = get_term( $child_id, $term->taxonomy );
735
							if ( $child->count ) {
736
								continue 2;
737
							}
738
						}
739
					}
740
741
					// It really is empty.
742
					unset( $terms[ $k ] );
743
				}
744
			}
745
		}
746
747
		/*
748
		 * When querying for terms connected to objects, we may get
749
		 * duplicate results. The duplicates should be preserved if
750
		 * `$fields` is 'all_with_object_id', but should otherwise be
751
		 * removed.
752
		 */
753
		if ( ! empty( $args['object_ids'] ) && 'all_with_object_id' != $_fields ) {
754
			$_tt_ids = $_terms = array();
755
			foreach ( $terms as $term ) {
756
				if ( isset( $_tt_ids[ $term->term_id ] ) ) {
757
					continue;
758
				}
759
760
				$_tt_ids[ $term->term_id ] = 1;
761
				$_terms[] = $term;
762
			}
763
764
			$terms = $_terms;
765
		}
766
767
		$_terms = array();
768
		if ( 'id=>parent' == $_fields ) {
769
			foreach ( $terms as $term ) {
770
				$_terms[ $term->term_id ] = $term->parent;
771
			}
772
		} elseif ( 'ids' == $_fields ) {
773
			foreach ( $terms as $term ) {
774
				$_terms[] = (int) $term->term_id;
775
			}
776
		} elseif ( 'tt_ids' == $_fields ) {
777
			foreach ( $terms as $term ) {
778
				$_terms[] = (int) $term->term_taxonomy_id;
779
			}
780
		} elseif ( 'names' == $_fields ) {
781
			foreach ( $terms as $term ) {
782
				$_terms[] = $term->name;
783
			}
784
		} elseif ( 'slugs' == $_fields ) {
785
			foreach ( $terms as $term ) {
786
				$_terms[] = $term->slug;
787
			}
788
		} elseif ( 'id=>name' == $_fields ) {
789
			foreach ( $terms as $term ) {
790
				$_terms[ $term->term_id ] = $term->name;
791
			}
792
		} elseif ( 'id=>slug' == $_fields ) {
793
			foreach ( $terms as $term ) {
794
				$_terms[ $term->term_id ] = $term->slug;
795
			}
796
		}
797
798
		if ( ! empty( $_terms ) ) {
799
			$terms = $_terms;
800
		}
801
802
		// Hierarchical queries are not limited, so 'offset' and 'number' must be handled now.
803
		if ( $hierarchical && $number && is_array( $terms ) ) {
804
			if ( $offset >= count( $terms ) ) {
805
				$terms = array();
806
			} else {
807
				$terms = array_slice( $terms, $offset, $number, true );
808
			}
809
		}
810
811
		wp_cache_add( $cache_key, $terms, 'terms', DAY_IN_SECONDS );
812
813
		if ( 'all' === $_fields || 'all_with_object_id' === $_fields ) {
814
			$terms = array_map( 'get_term', $terms );
815
		}
816
817
		$this->terms = $terms;
818
		return $this->terms;
819
	}
820
821
	/**
822
	 * Parse and sanitize 'orderby' keys passed to the term query.
823
	 *
824
	 * @since 4.6.0
825
	 * @access protected
826
	 *
827
	 * @global wpdb $wpdb WordPress database abstraction object.
828
	 *
829
	 * @param string $orderby_raw Alias for the field to order by.
830
	 * @return string|false Value to used in the ORDER clause. False otherwise.
831
	 */
832
	protected function parse_orderby( $orderby_raw ) {
833
		$_orderby = strtolower( $orderby_raw );
834
		$maybe_orderby_meta = false;
835
836
		if ( in_array( $_orderby, array( 'term_id', 'name', 'slug', 'term_group' ), true ) ) {
837
			$orderby = "t.$_orderby";
838
		} elseif ( in_array( $_orderby, array( 'count', 'parent', 'taxonomy', 'term_taxonomy_id', 'description' ), true ) ) {
839
			$orderby = "tt.$_orderby";
840
		} elseif ( 'term_order' === $_orderby ) {
841
			$orderby = 'tr.term_order';
842 View Code Duplication
		} elseif ( 'include' == $_orderby && ! empty( $this->query_vars['include'] ) ) {
843
			$include = implode( ',', wp_parse_id_list( $this->query_vars['include'] ) );
844
			$orderby = "FIELD( t.term_id, $include )";
845
		} elseif ( 'none' == $_orderby ) {
846
			$orderby = '';
847
		} elseif ( empty( $_orderby ) || 'id' == $_orderby || 'term_id' === $_orderby ) {
848
			$orderby = 't.term_id';
849
		} else {
850
			$orderby = 't.name';
851
852
			// This may be a value of orderby related to meta.
853
			$maybe_orderby_meta = true;
854
		}
855
856
		/**
857
		 * Filters the ORDERBY clause of the terms query.
858
		 *
859
		 * @since 2.8.0
860
		 *
861
		 * @param string $orderby    `ORDERBY` clause of the terms query.
862
		 * @param array  $args       An array of terms query arguments.
863
		 * @param array  $taxonomies An array of taxonomies.
864
		 */
865
		$orderby = apply_filters( 'get_terms_orderby', $orderby, $this->query_vars, $this->query_vars['taxonomy'] );
866
867
		// Run after the 'get_terms_orderby' filter for backward compatibility.
868
		if ( $maybe_orderby_meta ) {
869
			$maybe_orderby_meta = $this->parse_orderby_meta( $_orderby );
870
			if ( $maybe_orderby_meta ) {
871
				$orderby = $maybe_orderby_meta;
872
			}
873
		}
874
875
		return $orderby;
876
	}
877
878
	/**
879
	 * Generate the ORDER BY clause for an 'orderby' param that is potentially related to a meta query.
880
	 *
881
	 * @since 4.6.0
882
	 * @access protected
883
	 *
884
	 * @param string $orderby_raw Raw 'orderby' value passed to WP_Term_Query.
885
	 * @return string ORDER BY clause.
886
	 */
887
	protected function parse_orderby_meta( $orderby_raw ) {
888
		$orderby = '';
889
890
		// Tell the meta query to generate its SQL, so we have access to table aliases.
891
		$this->meta_query->get_sql( 'term', 't', 'term_id' );
892
		$meta_clauses = $this->meta_query->get_clauses();
893
		if ( ! $meta_clauses || ! $orderby_raw ) {
894
			return $orderby;
895
		}
896
897
		$allowed_keys = array();
898
		$primary_meta_key = null;
899
		$primary_meta_query = reset( $meta_clauses );
900
		if ( ! empty( $primary_meta_query['key'] ) ) {
901
			$primary_meta_key = $primary_meta_query['key'];
902
			$allowed_keys[] = $primary_meta_key;
903
		}
904
		$allowed_keys[] = 'meta_value';
905
		$allowed_keys[] = 'meta_value_num';
906
		$allowed_keys   = array_merge( $allowed_keys, array_keys( $meta_clauses ) );
907
908
		if ( ! in_array( $orderby_raw, $allowed_keys, true ) ) {
909
			return $orderby;
910
		}
911
912
		switch( $orderby_raw ) {
913
			case $primary_meta_key:
914 View Code Duplication
			case 'meta_value':
915
				if ( ! empty( $primary_meta_query['type'] ) ) {
916
					$orderby = "CAST({$primary_meta_query['alias']}.meta_value AS {$primary_meta_query['cast']})";
917
				} else {
918
					$orderby = "{$primary_meta_query['alias']}.meta_value";
919
				}
920
				break;
921
922
			case 'meta_value_num':
923
				$orderby = "{$primary_meta_query['alias']}.meta_value+0";
924
				break;
925
926
			default:
927
				if ( array_key_exists( $orderby_raw, $meta_clauses ) ) {
928
					// $orderby corresponds to a meta_query clause.
929
					$meta_clause = $meta_clauses[ $orderby_raw ];
930
					$orderby = "CAST({$meta_clause['alias']}.meta_value AS {$meta_clause['cast']})";
931
				}
932
				break;
933
		}
934
935
		return $orderby;
936
	}
937
938
	/**
939
	 * Parse an 'order' query variable and cast it to ASC or DESC as necessary.
940
	 *
941
	 * @since 4.6.0
942
	 * @access protected
943
	 *
944
	 * @param string $order The 'order' query variable.
945
	 * @return string The sanitized 'order' query variable.
946
	 */
947 View Code Duplication
	protected function parse_order( $order ) {
948
		if ( ! is_string( $order ) || empty( $order ) ) {
949
			return 'DESC';
950
		}
951
952
		if ( 'ASC' === strtoupper( $order ) ) {
953
			return 'ASC';
954
		} else {
955
			return 'DESC';
956
		}
957
	}
958
959
	/**
960
	 * Used internally to generate a SQL string related to the 'search' parameter.
961
	 *
962
	 * @since 4.6.0
963
	 * @access protected
964
	 *
965
	 * @global wpdb $wpdb WordPress database abstraction object.
966
	 *
967
	 * @param string $string
968
	 * @return string
969
	 */
970
	protected function get_search_sql( $string ) {
971
		global $wpdb;
972
973
		$like = '%' . $wpdb->esc_like( $string ) . '%';
974
975
		return $wpdb->prepare( '((t.name LIKE %s) OR (t.slug LIKE %s))', $like, $like );
976
	}
977
}
978