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-tax-query.php (4 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
 * Taxonomy API: WP_Tax_Query class
4
 *
5
 * @package WordPress
6
 * @subpackage Taxonomy
7
 * @since 4.4.0
8
 */
9
10
/**
11
 * Core class used to implement taxonomy queries for the Taxonomy API.
12
 *
13
 * Used for generating SQL clauses that filter a primary query according to object
14
 * taxonomy terms.
15
 *
16
 * WP_Tax_Query is a helper that allows primary query classes, such as WP_Query, to filter
17
 * their results by object metadata, by generating `JOIN` and `WHERE` subclauses to be
18
 * attached to the primary SQL query string.
19
 *
20
 * @since 3.1.0
21
 */
22
class WP_Tax_Query {
23
24
	/**
25
	 * Array of taxonomy queries.
26
	 *
27
	 * See WP_Tax_Query::__construct() for information on tax query arguments.
28
	 *
29
	 * @since 3.1.0
30
	 * @access public
31
	 * @var array
32
	 */
33
	public $queries = array();
34
35
	/**
36
	 * The relation between the queries. Can be one of 'AND' or 'OR'.
37
	 *
38
	 * @since 3.1.0
39
	 * @access public
40
	 * @var string
41
	 */
42
	public $relation;
43
44
	/**
45
	 * Standard response when the query should not return any rows.
46
	 *
47
	 * @since 3.2.0
48
	 *
49
	 * @static
50
	 * @access private
51
	 * @var string
52
	 */
53
	private static $no_results = array( 'join' => array( '' ), 'where' => array( '0 = 1' ) );
54
55
	/**
56
	 * A flat list of table aliases used in the JOIN clauses.
57
	 *
58
	 * @since 4.1.0
59
	 * @access protected
60
	 * @var array
61
	 */
62
	protected $table_aliases = array();
63
64
	/**
65
	 * Terms and taxonomies fetched by this query.
66
	 *
67
	 * We store this data in a flat array because they are referenced in a
68
	 * number of places by WP_Query.
69
	 *
70
	 * @since 4.1.0
71
	 * @access public
72
	 * @var array
73
	 */
74
	public $queried_terms = array();
75
76
	/**
77
	 * Database table that where the metadata's objects are stored (eg $wpdb->users).
78
	 *
79
	 * @since 4.1.0
80
	 * @access public
81
	 * @var string
82
	 */
83
	public $primary_table;
84
85
	/**
86
	 * Column in 'primary_table' that represents the ID of the object.
87
	 *
88
	 * @since 4.1.0
89
	 * @access public
90
	 * @var string
91
	 */
92
	public $primary_id_column;
93
94
	/**
95
	 * Constructor.
96
	 *
97
	 * @since 3.1.0
98
	 * @since 4.1.0 Added support for `$operator` 'NOT EXISTS' and 'EXISTS' values.
99
	 * @access public
100
	 *
101
	 * @param array $tax_query {
102
	 *     Array of taxonomy query clauses.
103
	 *
104
	 *     @type string $relation Optional. The MySQL keyword used to join
105
	 *                            the clauses of the query. Accepts 'AND', or 'OR'. Default 'AND'.
106
	 *     @type array {
107
	 *         Optional. An array of first-order clause parameters, or another fully-formed tax query.
108
	 *
109
	 *         @type string           $taxonomy         Taxonomy being queried. Optional when field=term_taxonomy_id.
110
	 *         @type string|int|array $terms            Term or terms to filter by.
111
	 *         @type string           $field            Field to match $terms against. Accepts 'term_id', 'slug',
112
	 *                                                 'name', or 'term_taxonomy_id'. Default: 'term_id'.
113
	 *         @type string           $operator         MySQL operator to be used with $terms in the WHERE clause.
114
	 *                                                  Accepts 'AND', 'IN', 'NOT IN', 'EXISTS', 'NOT EXISTS'.
115
	 *                                                  Default: 'IN'.
116
	 *         @type bool             $include_children Optional. Whether to include child terms.
117
	 *                                                  Requires a $taxonomy. Default: true.
118
	 *     }
119
	 * }
120
	 */
121
	public function __construct( $tax_query ) {
122
		if ( isset( $tax_query['relation'] ) ) {
123
			$this->relation = $this->sanitize_relation( $tax_query['relation'] );
124
		} else {
125
			$this->relation = 'AND';
126
		}
127
128
		$this->queries = $this->sanitize_query( $tax_query );
129
	}
130
131
	/**
132
	 * Ensure the 'tax_query' argument passed to the class constructor is well-formed.
133
	 *
134
	 * Ensures that each query-level clause has a 'relation' key, and that
135
	 * each first-order clause contains all the necessary keys from `$defaults`.
136
	 *
137
	 * @since 4.1.0
138
	 * @access public
139
	 *
140
	 * @param array $queries Array of queries clauses.
141
	 * @return array Sanitized array of query clauses.
142
	 */
143
	public function sanitize_query( $queries ) {
144
		$cleaned_query = array();
145
146
		$defaults = array(
147
			'taxonomy' => '',
148
			'terms' => array(),
149
			'field' => 'term_id',
150
			'operator' => 'IN',
151
			'include_children' => true,
152
		);
153
154
		foreach ( $queries as $key => $query ) {
155
			if ( 'relation' === $key ) {
156
				$cleaned_query['relation'] = $this->sanitize_relation( $query );
157
158
			// First-order clause.
159
			} elseif ( self::is_first_order_clause( $query ) ) {
160
161
				$cleaned_clause = array_merge( $defaults, $query );
162
				$cleaned_clause['terms'] = (array) $cleaned_clause['terms'];
163
				$cleaned_query[] = $cleaned_clause;
164
165
				/*
166
				 * Keep a copy of the clause in the flate
167
				 * $queried_terms array, for use in WP_Query.
168
				 */
169
				if ( ! empty( $cleaned_clause['taxonomy'] ) && 'NOT IN' !== $cleaned_clause['operator'] ) {
170
					$taxonomy = $cleaned_clause['taxonomy'];
171
					if ( ! isset( $this->queried_terms[ $taxonomy ] ) ) {
172
						$this->queried_terms[ $taxonomy ] = array();
173
					}
174
175
					/*
176
					 * Backward compatibility: Only store the first
177
					 * 'terms' and 'field' found for a given taxonomy.
178
					 */
179 View Code Duplication
					if ( ! empty( $cleaned_clause['terms'] ) && ! isset( $this->queried_terms[ $taxonomy ]['terms'] ) ) {
180
						$this->queried_terms[ $taxonomy ]['terms'] = $cleaned_clause['terms'];
181
					}
182
183 View Code Duplication
					if ( ! empty( $cleaned_clause['field'] ) && ! isset( $this->queried_terms[ $taxonomy ]['field'] ) ) {
184
						$this->queried_terms[ $taxonomy ]['field'] = $cleaned_clause['field'];
185
					}
186
				}
187
188
			// Otherwise, it's a nested query, so we recurse.
189
			} elseif ( is_array( $query ) ) {
190
				$cleaned_subquery = $this->sanitize_query( $query );
191
192
				if ( ! empty( $cleaned_subquery ) ) {
193
					// All queries with children must have a relation.
194
					if ( ! isset( $cleaned_subquery['relation'] ) ) {
195
						$cleaned_subquery['relation'] = 'AND';
196
					}
197
198
					$cleaned_query[] = $cleaned_subquery;
199
				}
200
			}
201
		}
202
203
		return $cleaned_query;
204
	}
205
206
	/**
207
	 * Sanitize a 'relation' operator.
208
	 *
209
	 * @since 4.1.0
210
	 * @access public
211
	 *
212
	 * @param string $relation Raw relation key from the query argument.
213
	 * @return string Sanitized relation ('AND' or 'OR').
214
	 */
215
	public function sanitize_relation( $relation ) {
216
		if ( 'OR' === strtoupper( $relation ) ) {
217
			return 'OR';
218
		} else {
219
			return 'AND';
220
		}
221
	}
222
223
	/**
224
	 * Determine whether a clause is first-order.
225
	 *
226
	 * A "first-order" clause is one that contains any of the first-order
227
	 * clause keys ('terms', 'taxonomy', 'include_children', 'field',
228
	 * 'operator'). An empty clause also counts as a first-order clause,
229
	 * for backward compatibility. Any clause that doesn't meet this is
230
	 * determined, by process of elimination, to be a higher-order query.
231
	 *
232
	 * @since 4.1.0
233
	 *
234
	 * @static
235
	 * @access protected
236
	 *
237
	 * @param array $query Tax query arguments.
238
	 * @return bool Whether the query clause is a first-order clause.
239
	 */
240
	protected static function is_first_order_clause( $query ) {
241
		return is_array( $query ) && ( empty( $query ) || array_key_exists( 'terms', $query ) || array_key_exists( 'taxonomy', $query ) || array_key_exists( 'include_children', $query ) || array_key_exists( 'field', $query ) || array_key_exists( 'operator', $query ) );
242
	}
243
244
	/**
245
	 * Generates SQL clauses to be appended to a main query.
246
	 *
247
	 * @since 3.1.0
248
	 *
249
	 * @static
250
	 * @access public
251
	 *
252
	 * @param string $primary_table     Database table where the object being filtered is stored (eg wp_users).
253
	 * @param string $primary_id_column ID column for the filtered object in $primary_table.
254
	 * @return array {
0 ignored issues
show
Consider making the return type a bit more specific; maybe use array<string,string>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
255
	 *     Array containing JOIN and WHERE SQL clauses to append to the main query.
256
	 *
257
	 *     @type string $join  SQL fragment to append to the main JOIN clause.
258
	 *     @type string $where SQL fragment to append to the main WHERE clause.
259
	 * }
260
	 */
261
	public function get_sql( $primary_table, $primary_id_column ) {
262
		$this->primary_table = $primary_table;
263
		$this->primary_id_column = $primary_id_column;
264
265
		return $this->get_sql_clauses();
266
	}
267
268
	/**
269
	 * Generate SQL clauses to be appended to a main query.
270
	 *
271
	 * Called by the public WP_Tax_Query::get_sql(), this method
272
	 * is abstracted out to maintain parity with the other Query classes.
273
	 *
274
	 * @since 4.1.0
275
	 * @access protected
276
	 *
277
	 * @return array {
0 ignored issues
show
Consider making the return type a bit more specific; maybe use array<string,string>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
278
	 *     Array containing JOIN and WHERE SQL clauses to append to the main query.
279
	 *
280
	 *     @type string $join  SQL fragment to append to the main JOIN clause.
281
	 *     @type string $where SQL fragment to append to the main WHERE clause.
282
	 * }
283
	 */
284 View Code Duplication
	protected function get_sql_clauses() {
285
		/*
286
		 * $queries are passed by reference to get_sql_for_query() for recursion.
287
		 * To keep $this->queries unaltered, pass a copy.
288
		 */
289
		$queries = $this->queries;
290
		$sql = $this->get_sql_for_query( $queries );
291
292
		if ( ! empty( $sql['where'] ) ) {
293
			$sql['where'] = ' AND ' . $sql['where'];
294
		}
295
296
		return $sql;
297
	}
298
299
	/**
300
	 * Generate SQL clauses for a single query array.
301
	 *
302
	 * If nested subqueries are found, this method recurses the tree to
303
	 * produce the properly nested SQL.
304
	 *
305
	 * @since 4.1.0
306
	 * @access protected
307
	 *
308
	 * @param array $query Query to parse, passed by reference.
309
	 * @param int   $depth Optional. Number of tree levels deep we currently are.
310
	 *                     Used to calculate indentation. Default 0.
311
	 * @return array {
0 ignored issues
show
Consider making the return type a bit more specific; maybe use array<string,string>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
312
	 *     Array containing JOIN and WHERE SQL clauses to append to a single query array.
313
	 *
314
	 *     @type string $join  SQL fragment to append to the main JOIN clause.
315
	 *     @type string $where SQL fragment to append to the main WHERE clause.
316
	 * }
317
	 */
318 View Code Duplication
	protected function get_sql_for_query( &$query, $depth = 0 ) {
319
		$sql_chunks = array(
320
			'join'  => array(),
321
			'where' => array(),
322
		);
323
324
		$sql = array(
325
			'join'  => '',
326
			'where' => '',
327
		);
328
329
		$indent = '';
330
		for ( $i = 0; $i < $depth; $i++ ) {
331
			$indent .= "  ";
332
		}
333
334
		foreach ( $query as $key => &$clause ) {
335
			if ( 'relation' === $key ) {
336
				$relation = $query['relation'];
337
			} elseif ( is_array( $clause ) ) {
338
339
				// This is a first-order clause.
340
				if ( $this->is_first_order_clause( $clause ) ) {
341
					$clause_sql = $this->get_sql_for_clause( $clause, $query );
342
343
					$where_count = count( $clause_sql['where'] );
344
					if ( ! $where_count ) {
345
						$sql_chunks['where'][] = '';
346
					} elseif ( 1 === $where_count ) {
347
						$sql_chunks['where'][] = $clause_sql['where'][0];
348
					} else {
349
						$sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )';
350
					}
351
352
					$sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] );
353
				// This is a subquery, so we recurse.
354
				} else {
355
					$clause_sql = $this->get_sql_for_query( $clause, $depth + 1 );
356
357
					$sql_chunks['where'][] = $clause_sql['where'];
358
					$sql_chunks['join'][]  = $clause_sql['join'];
359
				}
360
			}
361
		}
362
363
		// Filter to remove empties.
364
		$sql_chunks['join']  = array_filter( $sql_chunks['join'] );
365
		$sql_chunks['where'] = array_filter( $sql_chunks['where'] );
366
367
		if ( empty( $relation ) ) {
368
			$relation = 'AND';
369
		}
370
371
		// Filter duplicate JOIN clauses and combine into a single string.
372
		if ( ! empty( $sql_chunks['join'] ) ) {
373
			$sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) );
374
		}
375
376
		// Generate a single WHERE clause with proper brackets and indentation.
377
		if ( ! empty( $sql_chunks['where'] ) ) {
378
			$sql['where'] = '( ' . "\n  " . $indent . implode( ' ' . "\n  " . $indent . $relation . ' ' . "\n  " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')';
379
		}
380
381
		return $sql;
382
	}
383
384
	/**
385
	 * Generate SQL JOIN and WHERE clauses for a "first-order" query clause.
386
	 *
387
	 * @since 4.1.0
388
	 * @access public
389
	 *
390
	 * @global wpdb $wpdb The WordPress database abstraction object.
391
	 *
392
	 * @param array $clause       Query clause, passed by reference.
393
	 * @param array $parent_query Parent query array.
394
	 * @return array {
0 ignored issues
show
Should the return type not be string|array<string,array>?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
395
	 *     Array containing JOIN and WHERE SQL clauses to append to a first-order query.
396
	 *
397
	 *     @type string $join  SQL fragment to append to the main JOIN clause.
398
	 *     @type string $where SQL fragment to append to the main WHERE clause.
399
	 * }
400
	 */
401
	public function get_sql_for_clause( &$clause, $parent_query ) {
402
		global $wpdb;
403
404
		$sql = array(
405
			'where' => array(),
406
			'join'  => array(),
407
		);
408
409
		$join = $where = '';
410
411
		$this->clean_query( $clause );
412
413
		if ( is_wp_error( $clause ) ) {
414
			return self::$no_results;
415
		}
416
417
		$terms = $clause['terms'];
418
		$operator = strtoupper( $clause['operator'] );
419
420
		if ( 'IN' == $operator ) {
421
422
			if ( empty( $terms ) ) {
423
				return self::$no_results;
424
			}
425
426
			$terms = implode( ',', $terms );
427
428
			/*
429
			 * Before creating another table join, see if this clause has a
430
			 * sibling with an existing join that can be shared.
431
			 */
432
			$alias = $this->find_compatible_table_alias( $clause, $parent_query );
433
			if ( false === $alias ) {
434
				$i = count( $this->table_aliases );
435
				$alias = $i ? 'tt' . $i : $wpdb->term_relationships;
436
437
				// Store the alias as part of a flat array to build future iterators.
438
				$this->table_aliases[] = $alias;
439
440
				// Store the alias with this clause, so later siblings can use it.
441
				$clause['alias'] = $alias;
442
443
				$join .= " LEFT JOIN $wpdb->term_relationships";
444
				$join .= $i ? " AS $alias" : '';
445
				$join .= " ON ($this->primary_table.$this->primary_id_column = $alias.object_id)";
446
			}
447
448
449
			$where = "$alias.term_taxonomy_id $operator ($terms)";
450
451
		} elseif ( 'NOT IN' == $operator ) {
452
453
			if ( empty( $terms ) ) {
454
				return $sql;
455
			}
456
457
			$terms = implode( ',', $terms );
458
459
			$where = "$this->primary_table.$this->primary_id_column NOT IN (
460
				SELECT object_id
461
				FROM $wpdb->term_relationships
462
				WHERE term_taxonomy_id IN ($terms)
463
			)";
464
465
		} elseif ( 'AND' == $operator ) {
466
467
			if ( empty( $terms ) ) {
468
				return $sql;
469
			}
470
471
			$num_terms = count( $terms );
472
473
			$terms = implode( ',', $terms );
474
475
			$where = "(
476
				SELECT COUNT(1)
477
				FROM $wpdb->term_relationships
478
				WHERE term_taxonomy_id IN ($terms)
479
				AND object_id = $this->primary_table.$this->primary_id_column
480
			) = $num_terms";
481
482
		} elseif ( 'NOT EXISTS' === $operator || 'EXISTS' === $operator ) {
483
484
			$where = $wpdb->prepare( "$operator (
485
				SELECT 1
486
				FROM $wpdb->term_relationships
487
				INNER JOIN $wpdb->term_taxonomy
488
				ON $wpdb->term_taxonomy.term_taxonomy_id = $wpdb->term_relationships.term_taxonomy_id
489
				WHERE $wpdb->term_taxonomy.taxonomy = %s
490
				AND $wpdb->term_relationships.object_id = $this->primary_table.$this->primary_id_column
491
			)", $clause['taxonomy'] );
492
493
		}
494
495
		$sql['join'][]  = $join;
496
		$sql['where'][] = $where;
497
		return $sql;
498
	}
499
500
	/**
501
	 * Identify an existing table alias that is compatible with the current query clause.
502
	 *
503
	 * We avoid unnecessary table joins by allowing each clause to look for
504
	 * an existing table alias that is compatible with the query that it
505
	 * needs to perform.
506
	 *
507
	 * An existing alias is compatible if (a) it is a sibling of `$clause`
508
	 * (ie, it's under the scope of the same relation), and (b) the combination
509
	 * of operator and relation between the clauses allows for a shared table
510
	 * join. In the case of WP_Tax_Query, this only applies to 'IN'
511
	 * clauses that are connected by the relation 'OR'.
512
	 *
513
	 * @since 4.1.0
514
	 * @access protected
515
	 *
516
	 * @param array       $clause       Query clause.
517
	 * @param array       $parent_query Parent query of $clause.
518
	 * @return string|false Table alias if found, otherwise false.
519
	 */
520
	protected function find_compatible_table_alias( $clause, $parent_query ) {
521
		$alias = false;
522
523
		// Sanity check. Only IN queries use the JOIN syntax .
524
		if ( ! isset( $clause['operator'] ) || 'IN' !== $clause['operator'] ) {
525
			return $alias;
526
		}
527
528
		// Since we're only checking IN queries, we're only concerned with OR relations.
529
		if ( ! isset( $parent_query['relation'] ) || 'OR' !== $parent_query['relation'] ) {
530
			return $alias;
531
		}
532
533
		$compatible_operators = array( 'IN' );
534
535
		foreach ( $parent_query as $sibling ) {
536
			if ( ! is_array( $sibling ) || ! $this->is_first_order_clause( $sibling ) ) {
537
				continue;
538
			}
539
540
			if ( empty( $sibling['alias'] ) || empty( $sibling['operator'] ) ) {
541
				continue;
542
			}
543
544
			// The sibling must both have compatible operator to share its alias.
545
			if ( in_array( strtoupper( $sibling['operator'] ), $compatible_operators ) ) {
546
				$alias = $sibling['alias'];
547
				break;
548
			}
549
		}
550
551
		return $alias;
552
	}
553
554
	/**
555
	 * Validates a single query.
556
	 *
557
	 * @since 3.2.0
558
	 * @access private
559
	 *
560
	 * @param array $query The single query. Passed by reference.
561
	 */
562
	private function clean_query( &$query ) {
563
		if ( empty( $query['taxonomy'] ) ) {
564
			if ( 'term_taxonomy_id' !== $query['field'] ) {
565
				$query = new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
566
				return;
567
			}
568
569
			// so long as there are shared terms, include_children requires that a taxonomy is set
570
			$query['include_children'] = false;
571
		} elseif ( ! taxonomy_exists( $query['taxonomy'] ) ) {
572
			$query = new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) );
573
			return;
574
		}
575
576
		$query['terms'] = array_unique( (array) $query['terms'] );
577
578
		if ( is_taxonomy_hierarchical( $query['taxonomy'] ) && $query['include_children'] ) {
579
			$this->transform_query( $query, 'term_id' );
580
581
			if ( is_wp_error( $query ) )
582
				return;
583
584
			$children = array();
585
			foreach ( $query['terms'] as $term ) {
586
				$children = array_merge( $children, get_term_children( $term, $query['taxonomy'] ) );
587
				$children[] = $term;
588
			}
589
			$query['terms'] = $children;
590
		}
591
592
		$this->transform_query( $query, 'term_taxonomy_id' );
593
	}
594
595
	/**
596
	 * Transforms a single query, from one field to another.
597
	 *
598
	 * @since 3.2.0
599
	 *
600
	 * @global wpdb $wpdb The WordPress database abstraction object.
601
	 *
602
	 * @param array  $query           The single query. Passed by reference.
603
	 * @param string $resulting_field The resulting field. Accepts 'slug', 'name', 'term_taxonomy_id',
604
	 *                                or 'term_id'. Default 'term_id'.
605
	 */
606
	public function transform_query( &$query, $resulting_field ) {
607
		global $wpdb;
608
609
		if ( empty( $query['terms'] ) )
610
			return;
611
612
		if ( $query['field'] == $resulting_field )
613
			return;
614
615
		$resulting_field = sanitize_key( $resulting_field );
616
617
		switch ( $query['field'] ) {
618
			case 'slug':
619
			case 'name':
620
				foreach ( $query['terms'] as &$term ) {
621
					/*
622
					 * 0 is the $term_id parameter. We don't have a term ID yet, but it doesn't
623
					 * matter because `sanitize_term_field()` ignores the $term_id param when the
624
					 * context is 'db'.
625
					 */
626
					$clean_term = sanitize_term_field( $query['field'], $term, 0, $query['taxonomy'], 'db' );
627
628
					// Match sanitization in wp_insert_term().
629
					$clean_term = wp_unslash( $clean_term );
630
631
					$term = "'" . esc_sql( $clean_term ) . "'";
632
				}
633
634
				$terms = implode( ",", $query['terms'] );
635
636
				$terms = $wpdb->get_col( "
637
					SELECT $wpdb->term_taxonomy.$resulting_field
638
					FROM $wpdb->term_taxonomy
639
					INNER JOIN $wpdb->terms USING (term_id)
640
					WHERE taxonomy = '{$query['taxonomy']}'
641
					AND $wpdb->terms.{$query['field']} IN ($terms)
642
				" );
643
				break;
644 View Code Duplication
			case 'term_taxonomy_id':
645
				$terms = implode( ',', array_map( 'intval', $query['terms'] ) );
646
				$terms = $wpdb->get_col( "
647
					SELECT $resulting_field
648
					FROM $wpdb->term_taxonomy
649
					WHERE term_taxonomy_id IN ($terms)
650
				" );
651
				break;
652 View Code Duplication
			default:
653
				$terms = implode( ',', array_map( 'intval', $query['terms'] ) );
654
				$terms = $wpdb->get_col( "
655
					SELECT $resulting_field
656
					FROM $wpdb->term_taxonomy
657
					WHERE taxonomy = '{$query['taxonomy']}'
658
					AND term_id IN ($terms)
659
				" );
660
		}
661
662
		if ( 'AND' == $query['operator'] && count( $terms ) < count( $query['terms'] ) ) {
663
			$query = new WP_Error( 'inexistent_terms', __( 'Inexistent terms.' ) );
664
			return;
665
		}
666
667
		$query['terms'] = $terms;
668
		$query['field'] = $resulting_field;
669
	}
670
}
671