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-meta-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
 * Meta API: WP_Meta_Query class
4
 *
5
 * @package WordPress
6
 * @subpackage Meta
7
 * @since 4.4.0
8
 */
9
10
/**
11
 * Core class used to implement meta queries for the Meta API.
12
 *
13
 * Used for generating SQL clauses that filter a primary query according to metadata keys and values.
14
 *
15
 * WP_Meta_Query is a helper that allows primary query classes, such as WP_Query and WP_User_Query,
16
 *
17
 * to filter their results by object metadata, by generating `JOIN` and `WHERE` subclauses to be attached
18
 * to the primary SQL query string.
19
 *
20
 * @since 3.2.0
21
 * @package WordPress
22
 * @subpackage Meta
23
 */
24
class WP_Meta_Query {
25
	/**
26
	 * Array of metadata queries.
27
	 *
28
	 * See WP_Meta_Query::__construct() for information on meta query arguments.
29
	 *
30
	 * @since 3.2.0
31
	 * @access public
32
	 * @var array
33
	 */
34
	public $queries = array();
35
36
	/**
37
	 * The relation between the queries. Can be one of 'AND' or 'OR'.
38
	 *
39
	 * @since 3.2.0
40
	 * @access public
41
	 * @var string
42
	 */
43
	public $relation;
44
45
	/**
46
	 * Database table to query for the metadata.
47
	 *
48
	 * @since 4.1.0
49
	 * @access public
50
	 * @var string
51
	 */
52
	public $meta_table;
53
54
	/**
55
	 * Column in meta_table that represents the ID of the object the metadata belongs to.
56
	 *
57
	 * @since 4.1.0
58
	 * @access public
59
	 * @var string
60
	 */
61
	public $meta_id_column;
62
63
	/**
64
	 * Database table that where the metadata's objects are stored (eg $wpdb->users).
65
	 *
66
	 * @since 4.1.0
67
	 * @access public
68
	 * @var string
69
	 */
70
	public $primary_table;
71
72
	/**
73
	 * Column in primary_table that represents the ID of the object.
74
	 *
75
	 * @since 4.1.0
76
	 * @access public
77
	 * @var string
78
	 */
79
	public $primary_id_column;
80
81
	/**
82
	 * A flat list of table aliases used in JOIN clauses.
83
	 *
84
	 * @since 4.1.0
85
	 * @access protected
86
	 * @var array
87
	 */
88
	protected $table_aliases = array();
89
90
	/**
91
	 * A flat list of clauses, keyed by clause 'name'.
92
	 *
93
	 * @since 4.2.0
94
	 * @access protected
95
	 * @var array
96
	 */
97
	protected $clauses = array();
98
99
	/**
100
	 * Whether the query contains any OR relations.
101
	 *
102
	 * @since 4.3.0
103
	 * @access protected
104
	 * @var bool
105
	 */
106
	protected $has_or_relation = false;
107
108
	/**
109
	 * Constructor.
110
	 *
111
	 * @since 3.2.0
112
	 * @since 4.2.0 Introduced support for naming query clauses by associative array keys.
113
	 *
114
	 * @access public
115
	 *
116
	 * @param array $meta_query {
0 ignored issues
show
Should the type for parameter $meta_query not be false|array? Also, consider making the array more specific, something like array<String>, or String[].

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive. In addition it looks for parameters that have the generic type array and suggests a stricter type like array<String>.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
117
	 *     Array of meta query clauses. When first-order clauses or sub-clauses use strings as
118
	 *     their array keys, they may be referenced in the 'orderby' parameter of the parent query.
119
	 *
120
	 *     @type string $relation Optional. The MySQL keyword used to join
121
	 *                            the clauses of the query. Accepts 'AND', or 'OR'. Default 'AND'.
122
	 *     @type array {
123
	 *         Optional. An array of first-order clause parameters, or another fully-formed meta query.
124
	 *
125
	 *         @type string $key     Meta key to filter by.
126
	 *         @type string $value   Meta value to filter by.
127
	 *         @type string $compare MySQL operator used for comparing the $value. Accepts '=',
128
	 *                               '!=', '>', '>=', '<', '<=', 'LIKE', 'NOT LIKE',
129
	 *                               'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN', 'REGEXP',
130
	 *                               'NOT REGEXP', 'RLIKE', 'EXISTS' or 'NOT EXISTS'.
131
	 *                               Default is 'IN' when `$value` is an array, '=' otherwise.
132
	 *         @type string $type    MySQL data type that the meta_value column will be CAST to for
133
	 *                               comparisons. Accepts 'NUMERIC', 'BINARY', 'CHAR', 'DATE',
134
	 *                               'DATETIME', 'DECIMAL', 'SIGNED', 'TIME', or 'UNSIGNED'.
135
	 *                               Default is 'CHAR'.
136
	 *     }
137
	 * }
138
	 */
139
	public function __construct( $meta_query = false ) {
140
		if ( !$meta_query )
141
			return;
142
143 View Code Duplication
		if ( isset( $meta_query['relation'] ) && strtoupper( $meta_query['relation'] ) == 'OR' ) {
144
			$this->relation = 'OR';
145
		} else {
146
			$this->relation = 'AND';
147
		}
148
149
		$this->queries = $this->sanitize_query( $meta_query );
150
	}
151
152
	/**
153
	 * Ensure the 'meta_query' argument passed to the class constructor is well-formed.
154
	 *
155
	 * Eliminates empty items and ensures that a 'relation' is set.
156
	 *
157
	 * @since 4.1.0
158
	 * @access public
159
	 *
160
	 * @param array $queries Array of query clauses.
161
	 * @return array Sanitized array of query clauses.
162
	 */
163
	public function sanitize_query( $queries ) {
164
		$clean_queries = array();
165
166
		if ( ! is_array( $queries ) ) {
167
			return $clean_queries;
168
		}
169
170
		foreach ( $queries as $key => $query ) {
171
			if ( 'relation' === $key ) {
172
				$relation = $query;
173
174
			} elseif ( ! is_array( $query ) ) {
175
				continue;
176
177
			// First-order clause.
178
			} elseif ( $this->is_first_order_clause( $query ) ) {
179
				if ( isset( $query['value'] ) && array() === $query['value'] ) {
180
					unset( $query['value'] );
181
				}
182
183
				$clean_queries[ $key ] = $query;
184
185
			// Otherwise, it's a nested query, so we recurse.
186
			} else {
187
				$cleaned_query = $this->sanitize_query( $query );
188
189
				if ( ! empty( $cleaned_query ) ) {
190
					$clean_queries[ $key ] = $cleaned_query;
191
				}
192
			}
193
		}
194
195
		if ( empty( $clean_queries ) ) {
196
			return $clean_queries;
197
		}
198
199
		// Sanitize the 'relation' key provided in the query.
200
		if ( isset( $relation ) && 'OR' === strtoupper( $relation ) ) {
201
			$clean_queries['relation'] = 'OR';
202
			$this->has_or_relation = true;
203
204
		/*
205
		 * If there is only a single clause, call the relation 'OR'.
206
		 * This value will not actually be used to join clauses, but it
207
		 * simplifies the logic around combining key-only queries.
208
		 */
209
		} elseif ( 1 === count( $clean_queries ) ) {
210
			$clean_queries['relation'] = 'OR';
211
212
		// Default to AND.
213
		} else {
214
			$clean_queries['relation'] = 'AND';
215
		}
216
217
		return $clean_queries;
218
	}
219
220
	/**
221
	 * Determine whether a query clause is first-order.
222
	 *
223
	 * A first-order meta query clause is one that has either a 'key' or
224
	 * a 'value' array key.
225
	 *
226
	 * @since 4.1.0
227
	 * @access protected
228
	 *
229
	 * @param array $query Meta query arguments.
230
	 * @return bool Whether the query clause is a first-order clause.
231
	 */
232
	protected function is_first_order_clause( $query ) {
233
		return isset( $query['key'] ) || isset( $query['value'] );
234
	}
235
236
	/**
237
	 * Constructs a meta query based on 'meta_*' query vars
238
	 *
239
	 * @since 3.2.0
240
	 * @access public
241
	 *
242
	 * @param array $qv The query variables
243
	 */
244
	public function parse_query_vars( $qv ) {
245
		$meta_query = array();
246
247
		/*
248
		 * For orderby=meta_value to work correctly, simple query needs to be
249
		 * first (so that its table join is against an unaliased meta table) and
250
		 * needs to be its own clause (so it doesn't interfere with the logic of
251
		 * the rest of the meta_query).
252
		 */
253
		$primary_meta_query = array();
254
		foreach ( array( 'key', 'compare', 'type' ) as $key ) {
255
			if ( ! empty( $qv[ "meta_$key" ] ) ) {
256
				$primary_meta_query[ $key ] = $qv[ "meta_$key" ];
257
			}
258
		}
259
260
		// WP_Query sets 'meta_value' = '' by default.
261
		if ( isset( $qv['meta_value'] ) && '' !== $qv['meta_value'] && ( ! is_array( $qv['meta_value'] ) || $qv['meta_value'] ) ) {
262
			$primary_meta_query['value'] = $qv['meta_value'];
263
		}
264
265
		$existing_meta_query = isset( $qv['meta_query'] ) && is_array( $qv['meta_query'] ) ? $qv['meta_query'] : array();
266
267
		if ( ! empty( $primary_meta_query ) && ! empty( $existing_meta_query ) ) {
268
			$meta_query = array(
269
				'relation' => 'AND',
270
				$primary_meta_query,
271
				$existing_meta_query,
272
			);
273
		} elseif ( ! empty( $primary_meta_query ) ) {
274
			$meta_query = array(
275
				$primary_meta_query,
276
			);
277
		} elseif ( ! empty( $existing_meta_query ) ) {
278
			$meta_query = $existing_meta_query;
279
		}
280
281
		$this->__construct( $meta_query );
282
	}
283
284
	/**
285
	 * Return the appropriate alias for the given meta type if applicable.
286
	 *
287
	 * @since 3.7.0
288
	 * @access public
289
	 *
290
	 * @param string $type MySQL type to cast meta_value.
291
	 * @return string MySQL type.
292
	 */
293
	public function get_cast_for_type( $type = '' ) {
294
		if ( empty( $type ) )
295
			return 'CHAR';
296
297
		$meta_type = strtoupper( $type );
298
299
		if ( ! preg_match( '/^(?:BINARY|CHAR|DATE|DATETIME|SIGNED|UNSIGNED|TIME|NUMERIC(?:\(\d+(?:,\s?\d+)?\))?|DECIMAL(?:\(\d+(?:,\s?\d+)?\))?)$/', $meta_type ) )
300
			return 'CHAR';
301
302
		if ( 'NUMERIC' == $meta_type )
303
			$meta_type = 'SIGNED';
304
305
		return $meta_type;
306
	}
307
308
	/**
309
	 * Generates SQL clauses to be appended to a main query.
310
	 *
311
	 * @since 3.2.0
312
	 * @access public
313
	 *
314
	 * @param string $type              Type of meta, eg 'user', 'post'.
315
	 * @param string $primary_table     Database table where the object being filtered is stored (eg wp_users).
316
	 * @param string $primary_id_column ID column for the filtered object in $primary_table.
317
	 * @param object $context           Optional. The main query object.
318
	 * @return false|array {
319
	 *     Array containing JOIN and WHERE SQL clauses to append to the main query.
320
	 *
321
	 *     @type string $join  SQL fragment to append to the main JOIN clause.
322
	 *     @type string $where SQL fragment to append to the main WHERE clause.
323
	 * }
324
	 */
325
	public function get_sql( $type, $primary_table, $primary_id_column, $context = null ) {
326
		if ( ! $meta_table = _get_meta_table( $type ) ) {
327
			return false;
328
		}
329
330
		$this->table_aliases = array();
331
332
		$this->meta_table     = $meta_table;
333
		$this->meta_id_column = sanitize_key( $type . '_id' );
334
335
		$this->primary_table     = $primary_table;
336
		$this->primary_id_column = $primary_id_column;
337
338
		$sql = $this->get_sql_clauses();
339
340
		/*
341
		 * If any JOINs are LEFT JOINs (as in the case of NOT EXISTS), then all JOINs should
342
		 * be LEFT. Otherwise posts with no metadata will be excluded from results.
343
		 */
344
		if ( false !== strpos( $sql['join'], 'LEFT JOIN' ) ) {
345
			$sql['join'] = str_replace( 'INNER JOIN', 'LEFT JOIN', $sql['join'] );
346
		}
347
348
		/**
349
		 * Filters the meta query's generated SQL.
350
		 *
351
		 * @since 3.1.0
352
		 *
353
		 * @param array  $clauses           Array containing the query's JOIN and WHERE clauses.
354
		 * @param array  $queries           Array of meta queries.
355
		 * @param string $type              Type of meta.
356
		 * @param string $primary_table     Primary table.
357
		 * @param string $primary_id_column Primary column ID.
358
		 * @param object $context           The main query object.
359
		 */
360
		return apply_filters_ref_array( 'get_meta_sql', array( $sql, $this->queries, $type, $primary_table, $primary_id_column, $context ) );
361
	}
362
363
	/**
364
	 * Generate SQL clauses to be appended to a main query.
365
	 *
366
	 * Called by the public WP_Meta_Query::get_sql(), this method is abstracted
367
	 * out to maintain parity with the other Query classes.
368
	 *
369
	 * @since 4.1.0
370
	 * @access protected
371
	 *
372
	 * @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...
373
	 *     Array containing JOIN and WHERE SQL clauses to append to the main query.
374
	 *
375
	 *     @type string $join  SQL fragment to append to the main JOIN clause.
376
	 *     @type string $where SQL fragment to append to the main WHERE clause.
377
	 * }
378
	 */
379 View Code Duplication
	protected function get_sql_clauses() {
380
		/*
381
		 * $queries are passed by reference to get_sql_for_query() for recursion.
382
		 * To keep $this->queries unaltered, pass a copy.
383
		 */
384
		$queries = $this->queries;
385
		$sql = $this->get_sql_for_query( $queries );
386
387
		if ( ! empty( $sql['where'] ) ) {
388
			$sql['where'] = ' AND ' . $sql['where'];
389
		}
390
391
		return $sql;
392
	}
393
394
	/**
395
	 * Generate SQL clauses for a single query array.
396
	 *
397
	 * If nested subqueries are found, this method recurses the tree to
398
	 * produce the properly nested SQL.
399
	 *
400
	 * @since 4.1.0
401
	 * @access protected
402
	 *
403
	 * @param array $query Query to parse, passed by reference.
404
	 * @param int   $depth Optional. Number of tree levels deep we currently are.
405
	 *                     Used to calculate indentation. Default 0.
406
	 * @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...
407
	 *     Array containing JOIN and WHERE SQL clauses to append to a single query array.
408
	 *
409
	 *     @type string $join  SQL fragment to append to the main JOIN clause.
410
	 *     @type string $where SQL fragment to append to the main WHERE clause.
411
	 * }
412
	 */
413 View Code Duplication
	protected function get_sql_for_query( &$query, $depth = 0 ) {
414
		$sql_chunks = array(
415
			'join'  => array(),
416
			'where' => array(),
417
		);
418
419
		$sql = array(
420
			'join'  => '',
421
			'where' => '',
422
		);
423
424
		$indent = '';
425
		for ( $i = 0; $i < $depth; $i++ ) {
426
			$indent .= "  ";
427
		}
428
429
		foreach ( $query as $key => &$clause ) {
430
			if ( 'relation' === $key ) {
431
				$relation = $query['relation'];
432
			} elseif ( is_array( $clause ) ) {
433
434
				// This is a first-order clause.
435
				if ( $this->is_first_order_clause( $clause ) ) {
436
					$clause_sql = $this->get_sql_for_clause( $clause, $query, $key );
437
438
					$where_count = count( $clause_sql['where'] );
439
					if ( ! $where_count ) {
440
						$sql_chunks['where'][] = '';
441
					} elseif ( 1 === $where_count ) {
442
						$sql_chunks['where'][] = $clause_sql['where'][0];
443
					} else {
444
						$sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )';
445
					}
446
447
					$sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] );
448
				// This is a subquery, so we recurse.
449
				} else {
450
					$clause_sql = $this->get_sql_for_query( $clause, $depth + 1 );
451
452
					$sql_chunks['where'][] = $clause_sql['where'];
453
					$sql_chunks['join'][]  = $clause_sql['join'];
454
				}
455
			}
456
		}
457
458
		// Filter to remove empties.
459
		$sql_chunks['join']  = array_filter( $sql_chunks['join'] );
460
		$sql_chunks['where'] = array_filter( $sql_chunks['where'] );
461
462
		if ( empty( $relation ) ) {
463
			$relation = 'AND';
464
		}
465
466
		// Filter duplicate JOIN clauses and combine into a single string.
467
		if ( ! empty( $sql_chunks['join'] ) ) {
468
			$sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) );
469
		}
470
471
		// Generate a single WHERE clause with proper brackets and indentation.
472
		if ( ! empty( $sql_chunks['where'] ) ) {
473
			$sql['where'] = '( ' . "\n  " . $indent . implode( ' ' . "\n  " . $indent . $relation . ' ' . "\n  " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')';
474
		}
475
476
		return $sql;
477
	}
478
479
	/**
480
	 * Generate SQL JOIN and WHERE clauses for a first-order query clause.
481
	 *
482
	 * "First-order" means that it's an array with a 'key' or 'value'.
483
	 *
484
	 * @since 4.1.0
485
	 * @access public
486
	 *
487
	 * @global wpdb $wpdb WordPress database abstraction object.
488
	 *
489
	 * @param array  $clause       Query clause, passed by reference.
490
	 * @param array  $parent_query Parent query array.
491
	 * @param string $clause_key   Optional. The array key used to name the clause in the original `$meta_query`
492
	 *                             parameters. If not provided, a key will be generated automatically.
493
	 * @return array {
0 ignored issues
show
Consider making the return type a bit more specific; maybe use array<string,array>.

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...
494
	 *     Array containing JOIN and WHERE SQL clauses to append to a first-order query.
495
	 *
496
	 *     @type string $join  SQL fragment to append to the main JOIN clause.
497
	 *     @type string $where SQL fragment to append to the main WHERE clause.
498
	 * }
499
	 */
500
	public function get_sql_for_clause( &$clause, $parent_query, $clause_key = '' ) {
501
		global $wpdb;
502
503
		$sql_chunks = array(
504
			'where' => array(),
505
			'join' => array(),
506
		);
507
508
		if ( isset( $clause['compare'] ) ) {
509
			$clause['compare'] = strtoupper( $clause['compare'] );
510
		} else {
511
			$clause['compare'] = isset( $clause['value'] ) && is_array( $clause['value'] ) ? 'IN' : '=';
512
		}
513
514
		if ( ! in_array( $clause['compare'], array(
515
			'=', '!=', '>', '>=', '<', '<=',
516
			'LIKE', 'NOT LIKE',
517
			'IN', 'NOT IN',
518
			'BETWEEN', 'NOT BETWEEN',
519
			'EXISTS', 'NOT EXISTS',
520
			'REGEXP', 'NOT REGEXP', 'RLIKE'
521
		) ) ) {
522
			$clause['compare'] = '=';
523
		}
524
525
		$meta_compare = $clause['compare'];
526
527
		// First build the JOIN clause, if one is required.
528
		$join = '';
529
530
		// We prefer to avoid joins if possible. Look for an existing join compatible with this clause.
531
		$alias = $this->find_compatible_table_alias( $clause, $parent_query );
532
		if ( false === $alias ) {
533
			$i = count( $this->table_aliases );
534
			$alias = $i ? 'mt' . $i : $this->meta_table;
535
536
			// JOIN clauses for NOT EXISTS have their own syntax.
537
			if ( 'NOT EXISTS' === $meta_compare ) {
538
				$join .= " LEFT JOIN $this->meta_table";
539
				$join .= $i ? " AS $alias" : '';
540
				$join .= $wpdb->prepare( " ON ($this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column AND $alias.meta_key = %s )", $clause['key'] );
541
542
			// All other JOIN clauses.
543
			} else {
544
				$join .= " INNER JOIN $this->meta_table";
545
				$join .= $i ? " AS $alias" : '';
546
				$join .= " ON ( $this->primary_table.$this->primary_id_column = $alias.$this->meta_id_column )";
547
			}
548
549
			$this->table_aliases[] = $alias;
550
			$sql_chunks['join'][] = $join;
551
		}
552
553
		// Save the alias to this clause, for future siblings to find.
554
		$clause['alias'] = $alias;
555
556
		// Determine the data type.
557
		$_meta_type = isset( $clause['type'] ) ? $clause['type'] : '';
558
		$meta_type  = $this->get_cast_for_type( $_meta_type );
559
		$clause['cast'] = $meta_type;
560
561
		// Fallback for clause keys is the table alias. Key must be a string.
562
		if ( is_int( $clause_key ) || ! $clause_key ) {
563
			$clause_key = $clause['alias'];
564
		}
565
566
		// Ensure unique clause keys, so none are overwritten.
567
		$iterator = 1;
568
		$clause_key_base = $clause_key;
569
		while ( isset( $this->clauses[ $clause_key ] ) ) {
570
			$clause_key = $clause_key_base . '-' . $iterator;
571
			$iterator++;
572
		}
573
574
		// Store the clause in our flat array.
575
		$this->clauses[ $clause_key ] =& $clause;
576
577
		// Next, build the WHERE clause.
578
579
		// meta_key.
580
		if ( array_key_exists( 'key', $clause ) ) {
581
			if ( 'NOT EXISTS' === $meta_compare ) {
582
				$sql_chunks['where'][] = $alias . '.' . $this->meta_id_column . ' IS NULL';
583
			} else {
584
				$sql_chunks['where'][] = $wpdb->prepare( "$alias.meta_key = %s", trim( $clause['key'] ) );
585
			}
586
		}
587
588
		// meta_value.
589
		if ( array_key_exists( 'value', $clause ) ) {
590
			$meta_value = $clause['value'];
591
592
			if ( in_array( $meta_compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) {
593
				if ( ! is_array( $meta_value ) ) {
594
					$meta_value = preg_split( '/[,\s]+/', $meta_value );
595
				}
596
			} else {
597
				$meta_value = trim( $meta_value );
598
			}
599
600
			switch ( $meta_compare ) {
601
				case 'IN' :
602
				case 'NOT IN' :
603
					$meta_compare_string = '(' . substr( str_repeat( ',%s', count( $meta_value ) ), 1 ) . ')';
604
					$where = $wpdb->prepare( $meta_compare_string, $meta_value );
605
					break;
606
607
				case 'BETWEEN' :
608
				case 'NOT BETWEEN' :
609
					$meta_value = array_slice( $meta_value, 0, 2 );
610
					$where = $wpdb->prepare( '%s AND %s', $meta_value );
611
					break;
612
613
				case 'LIKE' :
614
				case 'NOT LIKE' :
615
					$meta_value = '%' . $wpdb->esc_like( $meta_value ) . '%';
616
					$where = $wpdb->prepare( '%s', $meta_value );
617
					break;
618
619
				// EXISTS with a value is interpreted as '='.
620
				case 'EXISTS' :
621
					$meta_compare = '=';
622
					$where = $wpdb->prepare( '%s', $meta_value );
623
					break;
624
625
				// 'value' is ignored for NOT EXISTS.
626
				case 'NOT EXISTS' :
627
					$where = '';
628
					break;
629
630
				default :
631
					$where = $wpdb->prepare( '%s', $meta_value );
632
					break;
633
634
			}
635
636
			if ( $where ) {
637
				if ( 'CHAR' === $meta_type ) {
638
					$sql_chunks['where'][] = "$alias.meta_value {$meta_compare} {$where}";
639
				} else {
640
					$sql_chunks['where'][] = "CAST($alias.meta_value AS {$meta_type}) {$meta_compare} {$where}";
641
				}
642
			}
643
		}
644
645
		/*
646
		 * Multiple WHERE clauses (for meta_key and meta_value) should
647
		 * be joined in parentheses.
648
		 */
649
		if ( 1 < count( $sql_chunks['where'] ) ) {
650
			$sql_chunks['where'] = array( '( ' . implode( ' AND ', $sql_chunks['where'] ) . ' )' );
651
		}
652
653
		return $sql_chunks;
654
	}
655
656
	/**
657
	 * Get a flattened list of sanitized meta clauses.
658
	 *
659
	 * This array should be used for clause lookup, as when the table alias and CAST type must be determined for
660
	 * a value of 'orderby' corresponding to a meta clause.
661
	 *
662
	 * @since 4.2.0
663
	 * @access public
664
	 *
665
	 * @return array Meta clauses.
666
	 */
667
	public function get_clauses() {
668
		return $this->clauses;
669
	}
670
671
	/**
672
	 * Identify an existing table alias that is compatible with the current
673
	 * query clause.
674
	 *
675
	 * We avoid unnecessary table joins by allowing each clause to look for
676
	 * an existing table alias that is compatible with the query that it
677
	 * needs to perform.
678
	 *
679
	 * An existing alias is compatible if (a) it is a sibling of `$clause`
680
	 * (ie, it's under the scope of the same relation), and (b) the combination
681
	 * of operator and relation between the clauses allows for a shared table join.
682
	 * In the case of WP_Meta_Query, this only applies to 'IN' clauses that are
683
	 * connected by the relation 'OR'.
684
	 *
685
	 * @since 4.1.0
686
	 * @access protected
687
	 *
688
	 * @param  array       $clause       Query clause.
689
	 * @param  array       $parent_query Parent query of $clause.
690
	 * @return string|bool Table alias if found, otherwise false.
691
	 */
692
	protected function find_compatible_table_alias( $clause, $parent_query ) {
693
		$alias = false;
694
695
		foreach ( $parent_query as $sibling ) {
696
			// If the sibling has no alias yet, there's nothing to check.
697
			if ( empty( $sibling['alias'] ) ) {
698
				continue;
699
			}
700
701
			// We're only interested in siblings that are first-order clauses.
702
			if ( ! is_array( $sibling ) || ! $this->is_first_order_clause( $sibling ) ) {
703
				continue;
704
			}
705
706
			$compatible_compares = array();
707
708
			// Clauses connected by OR can share joins as long as they have "positive" operators.
709
			if ( 'OR' === $parent_query['relation'] ) {
710
				$compatible_compares = array( '=', 'IN', 'BETWEEN', 'LIKE', 'REGEXP', 'RLIKE', '>', '>=', '<', '<=' );
711
712
			// Clauses joined by AND with "negative" operators share a join only if they also share a key.
713
			} elseif ( isset( $sibling['key'] ) && isset( $clause['key'] ) && $sibling['key'] === $clause['key'] ) {
714
				$compatible_compares = array( '!=', 'NOT IN', 'NOT LIKE' );
715
			}
716
717
			$clause_compare  = strtoupper( $clause['compare'] );
718
			$sibling_compare = strtoupper( $sibling['compare'] );
719
			if ( in_array( $clause_compare, $compatible_compares ) && in_array( $sibling_compare, $compatible_compares ) ) {
720
				$alias = $sibling['alias'];
721
				break;
722
			}
723
		}
724
725
		/**
726
		 * Filters the table alias identified as compatible with the current clause.
727
		 *
728
		 * @since 4.1.0
729
		 *
730
		 * @param string|bool $alias        Table alias, or false if none was found.
731
		 * @param array       $clause       First-order query clause.
732
		 * @param array       $parent_query Parent of $clause.
733
		 * @param object      $this         WP_Meta_Query object.
734
		 */
735
		return apply_filters( 'meta_query_find_compatible_table_alias', $alias, $clause, $parent_query, $this ) ;
736
	}
737
738
	/**
739
	 * Checks whether the current query has any OR relations.
740
	 *
741
	 * In some cases, the presence of an OR relation somewhere in the query will require
742
	 * the use of a `DISTINCT` or `GROUP BY` keyword in the `SELECT` clause. The current
743
	 * method can be used in these cases to determine whether such a clause is necessary.
744
	 *
745
	 * @since 4.3.0
746
	 *
747
	 * @return bool True if the query contains any `OR` relations, otherwise false.
748
	 */
749
	public function has_or_relation() {
750
		return $this->has_or_relation;
751
	}
752
}
753