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/date.php (10 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
 * Class for generating SQL clauses that filter a primary query according to date.
4
 *
5
 * WP_Date_Query is a helper that allows primary query classes, such as WP_Query, to filter
6
 * their results by date columns, by generating `WHERE` subclauses to be attached to the
7
 * primary SQL query string.
8
 *
9
 * Attempting to filter by an invalid date value (eg month=13) will generate SQL that will
10
 * return no results. In these cases, a _doing_it_wrong() error notice is also thrown.
11
 * See WP_Date_Query::validate_date_values().
12
 *
13
 * @link https://codex.wordpress.org/Function_Reference/WP_Query Codex page.
14
 *
15
 * @since 3.7.0
16
 */
17
class WP_Date_Query {
18
	/**
19
	 * Array of date queries.
20
	 *
21
	 * See WP_Date_Query::__construct() for information on date query arguments.
22
	 *
23
	 * @since 3.7.0
24
	 * @access public
25
	 * @var array
26
	 */
27
	public $queries = array();
28
29
	/**
30
	 * The default relation between top-level queries. Can be either 'AND' or 'OR'.
31
	 *
32
	 * @since 3.7.0
33
	 * @access public
34
	 * @var string
35
	 */
36
	public $relation = 'AND';
37
38
	/**
39
	 * The column to query against. Can be changed via the query arguments.
40
	 *
41
	 * @since 3.7.0
42
	 * @access public
43
	 * @var string
44
	 */
45
	public $column = 'post_date';
46
47
	/**
48
	 * The value comparison operator. Can be changed via the query arguments.
49
	 *
50
	 * @since 3.7.0
51
	 * @access public
52
	 * @var array
53
	 */
54
	public $compare = '=';
55
56
	/**
57
	 * Supported time-related parameter keys.
58
	 *
59
	 * @since 4.1.0
60
	 * @access public
61
	 * @var array
62
	 */
63
	public $time_keys = array( 'after', 'before', 'year', 'month', 'monthnum', 'week', 'w', 'dayofyear', 'day', 'dayofweek', 'dayofweek_iso', 'hour', 'minute', 'second' );
64
65
	/**
66
	 * Constructor.
67
	 *
68
	 * Time-related parameters that normally require integer values ('year', 'month', 'week', 'dayofyear', 'day',
69
	 * 'dayofweek', 'dayofweek_iso', 'hour', 'minute', 'second') accept arrays of integers for some values of
70
	 * 'compare'. When 'compare' is 'IN' or 'NOT IN', arrays are accepted; when 'compare' is 'BETWEEN' or 'NOT
71
	 * BETWEEN', arrays of two valid values are required. See individual argument descriptions for accepted values.
72
	 *
73
	 * @since 3.7.0
74
	 * @since 4.0.0 The $inclusive logic was updated to include all times within the date range.
75
	 * @since 4.1.0 Introduced 'dayofweek_iso' time type parameter.
76
	 * @access public
77
	 *
78
	 * @param array $date_query {
79
	 *     Array of date query clauses.
80
	 *
81
	 *     @type array {
82
	 *         @type string $column   Optional. The column to query against. If undefined, inherits the value of
83
	 *                                the `$default_column` parameter. Accepts 'post_date', 'post_date_gmt',
84
	 *                                'post_modified','post_modified_gmt', 'comment_date', 'comment_date_gmt'.
85
	 *                                Default 'post_date'.
86
	 *         @type string $compare  Optional. The comparison operator. Accepts '=', '!=', '>', '>=', '<', '<=',
87
	 *                                'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'. Default '='.
88
	 *         @type string $relation Optional. The boolean relationship between the date queries. Accepts 'OR' or 'AND'.
89
	 *                                Default 'OR'.
90
	 *         @type array {
91
	 *             Optional. An array of first-order clause parameters, or another fully-formed date query.
92
	 *
93
	 *             @type string|array $before {
94
	 *                 Optional. Date to retrieve posts before. Accepts `strtotime()`-compatible string,
95
	 *                 or array of 'year', 'month', 'day' values.
96
	 *
97
	 *                 @type string $year  The four-digit year. Default empty. Accepts any four-digit year.
98
	 *                 @type string $month Optional when passing array.The month of the year.
99
	 *                                     Default (string:empty)|(array:1). Accepts numbers 1-12.
100
	 *                 @type string $day   Optional when passing array.The day of the month.
101
	 *                                     Default (string:empty)|(array:1). Accepts numbers 1-31.
102
	 *             }
103
	 *             @type string|array $after {
104
	 *                 Optional. Date to retrieve posts after. Accepts `strtotime()`-compatible string,
105
	 *                 or array of 'year', 'month', 'day' values.
106
	 *
107
	 *                 @type string $year  The four-digit year. Accepts any four-digit year. Default empty.
108
	 *                 @type string $month Optional when passing array. The month of the year. Accepts numbers 1-12.
109
	 *                                     Default (string:empty)|(array:12).
110
	 *                 @type string $day   Optional when passing array.The day of the month. Accepts numbers 1-31.
111
	 *                                     Default (string:empty)|(array:last day of month).
112
	 *             }
113
	 *             @type string       $column        Optional. Used to add a clause comparing a column other than the
114
	 *                                               column specified in the top-level `$column` parameter. Accepts
115
	 *                                               'post_date', 'post_date_gmt', 'post_modified', 'post_modified_gmt',
116
	 *                                               'comment_date', 'comment_date_gmt'. Default is the value of
117
	 *                                               top-level `$column`.
118
	 *             @type string       $compare       Optional. The comparison operator. Accepts '=', '!=', '>', '>=',
119
	 *                                               '<', '<=', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'. 'IN',
120
	 *                                               'NOT IN', 'BETWEEN', and 'NOT BETWEEN'. Comparisons support
121
	 *                                               arrays in some time-related parameters. Default '='.
122
	 *             @type bool         $inclusive     Optional. Include results from dates specified in 'before' or
123
	 *                                               'after'. Default false.
124
	 *             @type int|array    $year          Optional. The four-digit year number. Accepts any four-digit year
125
	 *                                               or an array of years if `$compare` supports it. Default empty.
126
	 *             @type int|array    $month         Optional. The two-digit month number. Accepts numbers 1-12 or an
127
	 *                                               array of valid numbers if `$compare` supports it. Default empty.
128
	 *             @type int|array    $week          Optional. The week number of the year. Accepts numbers 0-53 or an
129
	 *                                               array of valid numbers if `$compare` supports it. Default empty.
130
	 *             @type int|array    $dayofyear     Optional. The day number of the year. Accepts numbers 1-366 or an
131
	 *                                               array of valid numbers if `$compare` supports it.
132
	 *             @type int|array    $day           Optional. The day of the month. Accepts numbers 1-31 or an array
133
	 *                                               of valid numbers if `$compare` supports it. Default empty.
134
	 *             @type int|array    $dayofweek     Optional. The day number of the week. Accepts numbers 1-7 (1 is
135
	 *                                               Sunday) or an array of valid numbers if `$compare` supports it.
136
	 *                                               Default empty.
137
	 *             @type int|array    $dayofweek_iso Optional. The day number of the week (ISO). Accepts numbers 1-7
138
	 *                                               (1 is Monday) or an array of valid numbers if `$compare` supports it.
139
	 *                                               Default empty.
140
	 *             @type int|array    $hour          Optional. The hour of the day. Accepts numbers 0-23 or an array
141
	 *                                               of valid numbers if `$compare` supports it. Default empty.
142
	 *             @type int|array    $minute        Optional. The minute of the hour. Accepts numbers 0-60 or an array
143
	 *                                               of valid numbers if `$compare` supports it. Default empty.
144
	 *             @type int|array    $second        Optional. The second of the minute. Accepts numbers 0-60 or an
145
	 *                                               array of valid numbers if `$compare` supports it. Default empty.
146
	 *         }
147
	 *     }
148
	 * }
149
	 * @param array $default_column Optional. Default column to query against. Default 'post_date'.
0 ignored issues
show
Should the type for parameter $default_column not be string|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...
150
	 *                              Accepts 'post_date', 'post_date_gmt', 'post_modified', 'post_modified_gmt',
151
	 *                              'comment_date', 'comment_date_gmt'.
152
	 */
153
	public function __construct( $date_query, $default_column = 'post_date' ) {
154 View Code Duplication
		if ( isset( $date_query['relation'] ) && 'OR' === strtoupper( $date_query['relation'] ) ) {
155
			$this->relation = 'OR';
156
		} else {
157
			$this->relation = 'AND';
158
		}
159
160
		if ( ! is_array( $date_query ) ) {
161
			return;
162
		}
163
164
		// Support for passing time-based keys in the top level of the $date_query array.
165
		if ( ! isset( $date_query[0] ) && ! empty( $date_query ) ) {
166
			$date_query = array( $date_query );
167
		}
168
169
		if ( empty( $date_query ) ) {
170
			return;
171
		}
172
173
		if ( ! empty( $date_query['column'] ) ) {
174
			$date_query['column'] = esc_sql( $date_query['column'] );
175
		} else {
176
			$date_query['column'] = esc_sql( $default_column );
177
		}
178
179
		$this->column = $this->validate_column( $this->column );
180
181
		$this->compare = $this->get_compare( $date_query );
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->get_compare($date_query) can also be of type string. However, the property $compare is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
182
183
		$this->queries = $this->sanitize_query( $date_query );
184
	}
185
186
	/**
187
	 * Recursive-friendly query sanitizer.
188
	 *
189
	 * Ensures that each query-level clause has a 'relation' key, and that
190
	 * each first-order clause contains all the necessary keys from
191
	 * `$defaults`.
192
	 *
193
	 * @since 4.1.0
194
	 * @access public
195
	 *
196
	 * @param array $queries
197
	 * @param array $parent_query
0 ignored issues
show
Should the type for parameter $parent_query not be array|null? 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...
198
	 *
199
	 * @return array Sanitized queries.
200
	 */
201
	public function sanitize_query( $queries, $parent_query = null ) {
202
		$cleaned_query = array();
203
204
		$defaults = array(
205
			'column'   => 'post_date',
206
			'compare'  => '=',
207
			'relation' => 'AND',
208
		);
209
210
		// Numeric keys should always have array values.
211
		foreach ( $queries as $qkey => $qvalue ) {
212
			if ( is_numeric( $qkey ) && ! is_array( $qvalue ) ) {
213
				unset( $queries[ $qkey ] );
214
			}
215
		}
216
217
		// Each query should have a value for each default key. Inherit from the parent when possible.
218
		foreach ( $defaults as $dkey => $dvalue ) {
219
			if ( isset( $queries[ $dkey ] ) ) {
220
				continue;
221
			}
222
223
			if ( isset( $parent_query[ $dkey ] ) ) {
224
				$queries[ $dkey ] = $parent_query[ $dkey ];
225
			} else {
226
				$queries[ $dkey ] = $dvalue;
227
			}
228
		}
229
230
		// Validate the dates passed in the query.
231
		if ( $this->is_first_order_clause( $queries ) ) {
232
			$this->validate_date_values( $queries );
233
		}
234
235
		foreach ( $queries as $key => $q ) {
236
			if ( ! is_array( $q ) || in_array( $key, $this->time_keys, true ) ) {
237
				// This is a first-order query. Trust the values and sanitize when building SQL.
238
				$cleaned_query[ $key ] = $q;
239
			} else {
240
				// Any array without a time key is another query, so we recurse.
241
				$cleaned_query[] = $this->sanitize_query( $q, $queries );
242
			}
243
		}
244
245
		return $cleaned_query;
246
	}
247
248
	/**
249
	 * Determine whether this is a first-order clause.
250
	 *
251
	 * Checks to see if the current clause has any time-related keys.
252
	 * If so, it's first-order.
253
	 *
254
	 * @since 4.1.0
255
	 * @access protected
256
	 *
257
	 * @param  array $query Query clause.
258
	 * @return bool True if this is a first-order clause.
259
	 */
260
	protected function is_first_order_clause( $query ) {
261
		$time_keys = array_intersect( $this->time_keys, array_keys( $query ) );
262
		return ! empty( $time_keys );
263
	}
264
265
	/**
266
	 * Determines and validates what comparison operator to use.
267
	 *
268
	 * @since 3.7.0
269
	 * @access public
270
	 *
271
	 * @param array $query A date query or a date subquery.
272
	 * @return string The comparison operator.
0 ignored issues
show
Should the return type not be string|array? Also, consider making the array more specific, something like array<String>, or String[].

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.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
273
	 */
274
	public function get_compare( $query ) {
275
		if ( ! empty( $query['compare'] ) && in_array( $query['compare'], array( '=', '!=', '>', '>=', '<', '<=', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) )
276
			return strtoupper( $query['compare'] );
277
278
		return $this->compare;
279
	}
280
281
	/**
282
	 * Validates the given date_query values and triggers errors if something is not valid.
283
	 *
284
	 * Note that date queries with invalid date ranges are allowed to
285
	 * continue (though of course no items will be found for impossible dates).
286
	 * This method only generates debug notices for these cases.
287
	 *
288
	 * @since  4.1.0
289
	 * @access public
290
	 *
291
	 * @param  array $date_query The date_query array.
292
	 * @return bool  True if all values in the query are valid, false if one or more fail.
293
	 */
294
	public function validate_date_values( $date_query = array() ) {
295
		if ( empty( $date_query ) ) {
296
			return false;
297
		}
298
299
		$valid = true;
300
301
		/*
302
		 * Validate 'before' and 'after' up front, then let the
303
		 * validation routine continue to be sure that all invalid
304
		 * values generate errors too.
305
		 */
306 View Code Duplication
		if ( array_key_exists( 'before', $date_query ) && is_array( $date_query['before'] ) ){
307
			$valid = $this->validate_date_values( $date_query['before'] );
308
		}
309
310 View Code Duplication
		if ( array_key_exists( 'after', $date_query ) && is_array( $date_query['after'] ) ){
311
			$valid = $this->validate_date_values( $date_query['after'] );
312
		}
313
314
		// Array containing all min-max checks.
315
		$min_max_checks = array();
316
317
		// Days per year.
318
		if ( array_key_exists( 'year', $date_query ) ) {
319
			/*
320
			 * If a year exists in the date query, we can use it to get the days.
321
			 * If multiple years are provided (as in a BETWEEN), use the first one.
322
			 */
323
			if ( is_array( $date_query['year'] ) ) {
324
				$_year = reset( $date_query['year'] );
325
			} else {
326
				$_year = $date_query['year'];
327
			}
328
329
			$max_days_of_year = date( 'z', mktime( 0, 0, 0, 12, 31, $_year ) ) + 1;
330
		} else {
331
			// otherwise we use the max of 366 (leap-year)
332
			$max_days_of_year = 366;
333
		}
334
335
		$min_max_checks['dayofyear'] = array(
336
			'min' => 1,
337
			'max' => $max_days_of_year
338
		);
339
340
		// Days per week.
341
		$min_max_checks['dayofweek'] = array(
342
			'min' => 1,
343
			'max' => 7
344
		);
345
346
		// Days per week.
347
		$min_max_checks['dayofweek_iso'] = array(
348
			'min' => 1,
349
			'max' => 7
350
		);
351
352
		// Months per year.
353
		$min_max_checks['month'] = array(
354
			'min' => 1,
355
			'max' => 12
356
		);
357
358
		// Weeks per year.
359
		if ( isset( $_year ) ) {
360
			/*
361
			 * If we have a specific year, use it to calculate number of weeks.
362
			 * Note: the number of weeks in a year is the date in which Dec 28 appears.
363
			 */
364
			$week_count = date( 'W', mktime( 0, 0, 0, 12, 28, $_year ) );
365
366
		} else {
367
			// Otherwise set the week-count to a maximum of 53.
368
			$week_count = 53;
369
		}
370
371
		$min_max_checks['week'] = array(
372
			'min' => 1,
373
			'max' => $week_count
374
		);
375
376
		// Days per month.
377
		$min_max_checks['day'] = array(
378
			'min' => 1,
379
			'max' => 31
380
		);
381
382
		// Hours per day.
383
		$min_max_checks['hour'] = array(
384
			'min' => 0,
385
			'max' => 23
386
		);
387
388
		// Minutes per hour.
389
		$min_max_checks['minute'] = array(
390
			'min' => 0,
391
			'max' => 59
392
		);
393
394
		// Seconds per minute.
395
		$min_max_checks['second'] = array(
396
			'min' => 0,
397
			'max' => 59
398
		);
399
400
		// Concatenate and throw a notice for each invalid value.
401
		foreach ( $min_max_checks as $key => $check ) {
402
			if ( ! array_key_exists( $key, $date_query ) ) {
403
				continue;
404
			}
405
406
			// Throw a notice for each failing value.
407
			foreach ( (array) $date_query[ $key ] as $_value ) {
408
				$is_between = $_value >= $check['min'] && $_value <= $check['max'];
409
410
				if ( ! is_numeric( $_value ) || ! $is_between ) {
411
					$error = sprintf(
412
						/* translators: Date query invalid date message: 1: invalid value, 2: type of value, 3: minimum valid value, 4: maximum valid value */
413
						__( 'Invalid value %1$s for %2$s. Expected value should be between %3$s and %4$s.' ),
414
						'<code>' . esc_html( $_value ) . '</code>',
415
						'<code>' . esc_html( $key ) . '</code>',
416
						'<code>' . esc_html( $check['min'] ) . '</code>',
417
						'<code>' . esc_html( $check['max'] ) . '</code>'
418
					);
419
420
					_doing_it_wrong( __CLASS__, $error, '4.1.0' );
421
422
					$valid = false;
423
				}
424
			}
425
		}
426
427
		// If we already have invalid date messages, don't bother running through checkdate().
428
		if ( ! $valid ) {
429
			return $valid;
430
		}
431
432
		$day_month_year_error_msg = '';
433
434
		$day_exists   = array_key_exists( 'day', $date_query ) && is_numeric( $date_query['day'] );
435
		$month_exists = array_key_exists( 'month', $date_query ) && is_numeric( $date_query['month'] );
436
		$year_exists  = array_key_exists( 'year', $date_query ) && is_numeric( $date_query['year'] );
437
438
		if ( $day_exists && $month_exists && $year_exists ) {
439
			// 1. Checking day, month, year combination.
440
			if ( ! wp_checkdate( $date_query['month'], $date_query['day'], $date_query['year'], sprintf( '%s-%s-%s', $date_query['year'], $date_query['month'], $date_query['day'] ) ) ) {
441
				/* translators: 1: year, 2: month, 3: day of month */
442
				$day_month_year_error_msg = sprintf(
443
					__( 'The following values do not describe a valid date: year %1$s, month %2$s, day %3$s.' ),
444
					'<code>' . esc_html( $date_query['year'] ) . '</code>',
445
					'<code>' . esc_html( $date_query['month'] ) . '</code>',
446
					'<code>' . esc_html( $date_query['day'] ) . '</code>'
447
				);
448
449
				$valid = false;
450
			}
451
452
		} elseif ( $day_exists && $month_exists ) {
453
			/*
454
			 * 2. checking day, month combination
455
			 * We use 2012 because, as a leap year, it's the most permissive.
456
			 */
457
			if ( ! wp_checkdate( $date_query['month'], $date_query['day'], 2012, sprintf( '2012-%s-%s', $date_query['month'], $date_query['day'] ) ) ) {
458
				/* translators: 1: month, 2: day of month */
459
				$day_month_year_error_msg = sprintf(
460
					__( 'The following values do not describe a valid date: month %1$s, day %2$s.' ),
461
					'<code>' . esc_html( $date_query['month'] ) . '</code>',
462
					'<code>' . esc_html( $date_query['day'] ) . '</code>'
463
				);
464
465
				$valid = false;
466
			}
467
		}
468
469
		if ( ! empty( $day_month_year_error_msg ) ) {
470
			_doing_it_wrong( __CLASS__, $day_month_year_error_msg, '4.1.0' );
471
		}
472
473
		return $valid;
474
	}
475
476
	/**
477
	 * Validates a column name parameter.
478
	 *
479
	 * Column names without a table prefix (like 'post_date') are checked against a whitelist of
480
	 * known tables, and then, if found, have a table prefix (such as 'wp_posts.') prepended.
481
	 * Prefixed column names (such as 'wp_posts.post_date') bypass this whitelist check,
482
	 * and are only sanitized to remove illegal characters.
483
	 *
484
	 * @since 3.7.0
485
	 * @access public
486
	 *
487
	 * @param string $column The user-supplied column name.
488
	 * @return string A validated column name value.
489
	 */
490
	public function validate_column( $column ) {
491
		global $wpdb;
492
493
		$valid_columns = array(
494
			'post_date', 'post_date_gmt', 'post_modified',
495
			'post_modified_gmt', 'comment_date', 'comment_date_gmt',
496
			'user_registered', 'registered', 'last_updated',
497
		);
498
499
		// Attempt to detect a table prefix.
500
		if ( false === strpos( $column, '.' ) ) {
501
			/**
502
			 * Filters the list of valid date query columns.
503
			 *
504
			 * @since 3.7.0
505
			 * @since 4.1.0 Added 'user_registered' to the default recognized columns.
506
			 *
507
			 * @param array $valid_columns An array of valid date query columns. Defaults
508
			 *                             are 'post_date', 'post_date_gmt', 'post_modified',
509
			 *                             'post_modified_gmt', 'comment_date', 'comment_date_gmt',
510
			 *	                           'user_registered'
511
			 */
512
			if ( ! in_array( $column, apply_filters( 'date_query_valid_columns', $valid_columns ) ) ) {
513
				$column = 'post_date';
514
			}
515
516
			$known_columns = array(
517
				$wpdb->posts => array(
518
					'post_date',
519
					'post_date_gmt',
520
					'post_modified',
521
					'post_modified_gmt',
522
				),
523
				$wpdb->comments => array(
524
					'comment_date',
525
					'comment_date_gmt',
526
				),
527
				$wpdb->users => array(
528
					'user_registered',
529
				),
530
				$wpdb->blogs => array(
531
					'registered',
532
					'last_updated',
533
				),
534
			);
535
536
			// If it's a known column name, add the appropriate table prefix.
537
			foreach ( $known_columns as $table_name => $table_columns ) {
538
				if ( in_array( $column, $table_columns ) ) {
539
					$column = $table_name . '.' . $column;
540
					break;
541
				}
542
			}
543
544
		}
545
546
		// Remove unsafe characters.
547
		return preg_replace( '/[^a-zA-Z0-9_$\.]/', '', $column );
548
	}
549
550
	/**
551
	 * Generate WHERE clause to be appended to a main query.
552
	 *
553
	 * @since 3.7.0
554
	 * @access public
555
	 *
556
	 * @return string MySQL WHERE clause.
557
	 */
558
	public function get_sql() {
559
		$sql = $this->get_sql_clauses();
560
561
		$where = $sql['where'];
562
563
		/**
564
		 * Filters the date query WHERE clause.
565
		 *
566
		 * @since 3.7.0
567
		 *
568
		 * @param string        $where WHERE clause of the date query.
569
		 * @param WP_Date_Query $this  The WP_Date_Query instance.
570
		 */
571
		return apply_filters( 'get_date_sql', $where, $this );
572
	}
573
574
	/**
575
	 * Generate SQL clauses to be appended to a main query.
576
	 *
577
	 * Called by the public WP_Date_Query::get_sql(), this method is abstracted
578
	 * out to maintain parity with the other Query classes.
579
	 *
580
	 * @since 4.1.0
581
	 * @access protected
582
	 *
583
	 * @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...
584
	 *     Array containing JOIN and WHERE SQL clauses to append to the main query.
585
	 *
586
	 *     @type string $join  SQL fragment to append to the main JOIN clause.
587
	 *     @type string $where SQL fragment to append to the main WHERE clause.
588
	 * }
589
	 */
590 View Code Duplication
	protected function get_sql_clauses() {
591
		$sql = $this->get_sql_for_query( $this->queries );
592
593
		if ( ! empty( $sql['where'] ) ) {
594
			$sql['where'] = ' AND ' . $sql['where'];
595
		}
596
597
		return $sql;
598
	}
599
600
	/**
601
	 * Generate SQL clauses for a single query array.
602
	 *
603
	 * If nested subqueries are found, this method recurses the tree to
604
	 * produce the properly nested SQL.
605
	 *
606
	 * @since 4.1.0
607
	 * @access protected
608
	 *
609
	 * @param array $query Query to parse.
610
	 * @param int   $depth Optional. Number of tree levels deep we currently are.
611
	 *                     Used to calculate indentation. Default 0.
612
	 * @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...
613
	 *     Array containing JOIN and WHERE SQL clauses to append to a single query array.
614
	 *
615
	 *     @type string $join  SQL fragment to append to the main JOIN clause.
616
	 *     @type string $where SQL fragment to append to the main WHERE clause.
617
	 * }
618
	 */
619 View Code Duplication
	protected function get_sql_for_query( $query, $depth = 0 ) {
620
		$sql_chunks = array(
621
			'join'  => array(),
622
			'where' => array(),
623
		);
624
625
		$sql = array(
626
			'join'  => '',
627
			'where' => '',
628
		);
629
630
		$indent = '';
631
		for ( $i = 0; $i < $depth; $i++ ) {
632
			$indent .= "  ";
633
		}
634
635
		foreach ( $query as $key => $clause ) {
636
			if ( 'relation' === $key ) {
637
				$relation = $query['relation'];
638
			} elseif ( is_array( $clause ) ) {
639
640
				// This is a first-order clause.
641
				if ( $this->is_first_order_clause( $clause ) ) {
642
					$clause_sql = $this->get_sql_for_clause( $clause, $query );
643
644
					$where_count = count( $clause_sql['where'] );
645
					if ( ! $where_count ) {
646
						$sql_chunks['where'][] = '';
647
					} elseif ( 1 === $where_count ) {
648
						$sql_chunks['where'][] = $clause_sql['where'][0];
649
					} else {
650
						$sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )';
651
					}
652
653
					$sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] );
654
				// This is a subquery, so we recurse.
655
				} else {
656
					$clause_sql = $this->get_sql_for_query( $clause, $depth + 1 );
657
658
					$sql_chunks['where'][] = $clause_sql['where'];
659
					$sql_chunks['join'][]  = $clause_sql['join'];
660
				}
661
			}
662
		}
663
664
		// Filter to remove empties.
665
		$sql_chunks['join']  = array_filter( $sql_chunks['join'] );
666
		$sql_chunks['where'] = array_filter( $sql_chunks['where'] );
667
668
		if ( empty( $relation ) ) {
669
			$relation = 'AND';
670
		}
671
672
		// Filter duplicate JOIN clauses and combine into a single string.
673
		if ( ! empty( $sql_chunks['join'] ) ) {
674
			$sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) );
675
		}
676
677
		// Generate a single WHERE clause with proper brackets and indentation.
678
		if ( ! empty( $sql_chunks['where'] ) ) {
679
			$sql['where'] = '( ' . "\n  " . $indent . implode( ' ' . "\n  " . $indent . $relation . ' ' . "\n  " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')';
680
		}
681
682
		return $sql;
683
	}
684
685
	/**
686
	 * Turns a single date clause into pieces for a WHERE clause.
687
	 *
688
	 * A wrapper for get_sql_for_clause(), included here for backward
689
	 * compatibility while retaining the naming convention across Query classes.
690
	 *
691
	 * @since  3.7.0
692
	 * @access protected
693
	 *
694
	 * @param  array $query Date query arguments.
695
	 * @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...
696
	 *     Array containing JOIN and WHERE SQL clauses to append to the main query.
697
	 *
698
	 *     @type string $join  SQL fragment to append to the main JOIN clause.
699
	 *     @type string $where SQL fragment to append to the main WHERE clause.
700
	 * }
701
	 */
702
	protected function get_sql_for_subquery( $query ) {
703
		return $this->get_sql_for_clause( $query, '' );
704
	}
705
706
	/**
707
	 * Turns a first-order date query into SQL for a WHERE clause.
708
	 *
709
	 * @since  4.1.0
710
	 * @access protected
711
	 *
712
	 * @param  array $query        Date query clause.
713
	 * @param  array $parent_query Parent query of the current date query.
714
	 * @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...
715
	 *     Array containing JOIN and WHERE SQL clauses to append to the main query.
716
	 *
717
	 *     @type string $join  SQL fragment to append to the main JOIN clause.
718
	 *     @type string $where SQL fragment to append to the main WHERE clause.
719
	 * }
720
	 */
721
	protected function get_sql_for_clause( $query, $parent_query ) {
0 ignored issues
show
The parameter $parent_query is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
722
		global $wpdb;
723
724
		// The sub-parts of a $where part.
725
		$where_parts = array();
726
727
		$column = ( ! empty( $query['column'] ) ) ? esc_sql( $query['column'] ) : $this->column;
728
729
		$column = $this->validate_column( $column );
0 ignored issues
show
It seems like $column can also be of type array; however, WP_Date_Query::validate_column() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
730
731
		$compare = $this->get_compare( $query );
732
733
		$inclusive = ! empty( $query['inclusive'] );
734
735
		// Assign greater- and less-than values.
736
		$lt = '<';
737
		$gt = '>';
738
739
		if ( $inclusive ) {
740
			$lt .= '=';
741
			$gt .= '=';
742
		}
743
744
		// Range queries.
745
		if ( ! empty( $query['after'] ) ) {
746
			$where_parts[] = $wpdb->prepare( "$column $gt %s", $this->build_mysql_datetime( $query['after'], ! $inclusive ) );
747
		}
748
		if ( ! empty( $query['before'] ) ) {
749
			$where_parts[] = $wpdb->prepare( "$column $lt %s", $this->build_mysql_datetime( $query['before'], $inclusive ) );
750
		}
751
		// Specific value queries.
752
753
		if ( isset( $query['year'] ) && $value = $this->build_value( $compare, $query['year'] ) )
754
			$where_parts[] = "YEAR( $column ) $compare $value";
755
756
		if ( isset( $query['month'] ) && $value = $this->build_value( $compare, $query['month'] ) ) {
757
			$where_parts[] = "MONTH( $column ) $compare $value";
758 View Code Duplication
		} elseif ( isset( $query['monthnum'] ) && $value = $this->build_value( $compare, $query['monthnum'] ) ) {
759
			$where_parts[] = "MONTH( $column ) $compare $value";
760
		}
761
		if ( isset( $query['week'] ) && false !== ( $value = $this->build_value( $compare, $query['week'] ) ) ) {
762
			$where_parts[] = _wp_mysql_week( $column ) . " $compare $value";
763 View Code Duplication
		} elseif ( isset( $query['w'] ) && false !== ( $value = $this->build_value( $compare, $query['w'] ) ) ) {
764
			$where_parts[] = _wp_mysql_week( $column ) . " $compare $value";
765
		}
766
		if ( isset( $query['dayofyear'] ) && $value = $this->build_value( $compare, $query['dayofyear'] ) )
767
			$where_parts[] = "DAYOFYEAR( $column ) $compare $value";
768
769
		if ( isset( $query['day'] ) && $value = $this->build_value( $compare, $query['day'] ) )
770
			$where_parts[] = "DAYOFMONTH( $column ) $compare $value";
771
772
		if ( isset( $query['dayofweek'] ) && $value = $this->build_value( $compare, $query['dayofweek'] ) )
773
			$where_parts[] = "DAYOFWEEK( $column ) $compare $value";
774
775
		if ( isset( $query['dayofweek_iso'] ) && $value = $this->build_value( $compare, $query['dayofweek_iso'] ) )
776
			$where_parts[] = "WEEKDAY( $column ) + 1 $compare $value";
777
778
		if ( isset( $query['hour'] ) || isset( $query['minute'] ) || isset( $query['second'] ) ) {
779
			// Avoid notices.
780
			foreach ( array( 'hour', 'minute', 'second' ) as $unit ) {
781
				if ( ! isset( $query[ $unit ] ) ) {
782
					$query[ $unit ] = null;
783
				}
784
			}
785
786
			if ( $time_query = $this->build_time_query( $column, $compare, $query['hour'], $query['minute'], $query['second'] ) ) {
787
				$where_parts[] = $time_query;
788
			}
789
		}
790
791
		/*
792
		 * Return an array of 'join' and 'where' for compatibility
793
		 * with other query classes.
794
		 */
795
		return array(
796
			'where' => $where_parts,
797
			'join'  => array(),
798
		);
799
	}
800
801
	/**
802
	 * Builds and validates a value string based on the comparison operator.
803
	 *
804
	 * @since 3.7.0
805
	 * @access public
806
	 *
807
	 * @param string $compare The compare operator to use
808
	 * @param string|array $value The value
809
	 * @return string|false|int The value to be used in SQL or false on error.
810
	 */
811
	public function build_value( $compare, $value ) {
812
		if ( ! isset( $value ) )
813
			return false;
814
815
		switch ( $compare ) {
816
			case 'IN':
817
			case 'NOT IN':
818
				$value = (array) $value;
819
820
				// Remove non-numeric values.
821
				$value = array_filter( $value, 'is_numeric' );
822
823
				if ( empty( $value ) ) {
824
					return false;
825
				}
826
827
				return '(' . implode( ',', array_map( 'intval', $value ) ) . ')';
828
829
			case 'BETWEEN':
830
			case 'NOT BETWEEN':
831
				if ( ! is_array( $value ) || 2 != count( $value ) ) {
832
					$value = array( $value, $value );
833
				} else {
834
					$value = array_values( $value );
835
				}
836
837
				// If either value is non-numeric, bail.
838
				foreach ( $value as $v ) {
839
					if ( ! is_numeric( $v ) ) {
840
						return false;
841
					}
842
				}
843
844
				$value = array_map( 'intval', $value );
845
846
				return $value[0] . ' AND ' . $value[1];
847
848
			default;
849
				if ( ! is_numeric( $value ) ) {
850
					return false;
851
				}
852
853
				return (int) $value;
854
		}
855
	}
856
857
	/**
858
	 * Builds a MySQL format date/time based on some query parameters.
859
	 *
860
	 * You can pass an array of values (year, month, etc.) with missing parameter values being defaulted to
861
	 * either the maximum or minimum values (controlled by the $default_to parameter). Alternatively you can
862
	 * pass a string that will be run through strtotime().
863
	 *
864
	 * @since 3.7.0
865
	 * @access public
866
	 *
867
	 * @param string|array $datetime       An array of parameters or a strotime() string
868
	 * @param bool         $default_to_max Whether to round up incomplete dates. Supported by values
869
	 *                                     of $datetime that are arrays, or string values that are a
870
	 *                                     subset of MySQL date format ('Y', 'Y-m', 'Y-m-d', 'Y-m-d H:i').
871
	 *                                     Default: false.
872
	 * @return string|false A MySQL format date/time or false on failure
873
	 */
874
	public function build_mysql_datetime( $datetime, $default_to_max = false ) {
875
		$now = current_time( 'timestamp' );
876
877
		if ( ! is_array( $datetime ) ) {
878
879
			/*
880
			 * Try to parse some common date formats, so we can detect
881
			 * the level of precision and support the 'inclusive' parameter.
882
			 */
883
			if ( preg_match( '/^(\d{4})$/', $datetime, $matches ) ) {
884
				// Y
885
				$datetime = array(
886
					'year' => intval( $matches[1] ),
887
				);
888
889
			} elseif ( preg_match( '/^(\d{4})\-(\d{2})$/', $datetime, $matches ) ) {
890
				// Y-m
891
				$datetime = array(
892
					'year'  => intval( $matches[1] ),
893
					'month' => intval( $matches[2] ),
894
				);
895
896
			} elseif ( preg_match( '/^(\d{4})\-(\d{2})\-(\d{2})$/', $datetime, $matches ) ) {
897
				// Y-m-d
898
				$datetime = array(
899
					'year'  => intval( $matches[1] ),
900
					'month' => intval( $matches[2] ),
901
					'day'   => intval( $matches[3] ),
902
				);
903
904
			} elseif ( preg_match( '/^(\d{4})\-(\d{2})\-(\d{2}) (\d{2}):(\d{2})$/', $datetime, $matches ) ) {
905
				// Y-m-d H:i
906
				$datetime = array(
907
					'year'   => intval( $matches[1] ),
908
					'month'  => intval( $matches[2] ),
909
					'day'    => intval( $matches[3] ),
910
					'hour'   => intval( $matches[4] ),
911
					'minute' => intval( $matches[5] ),
912
				);
913
			}
914
915
			// If no match is found, we don't support default_to_max.
916
			if ( ! is_array( $datetime ) ) {
917
				// @todo Timezone issues here possibly
918
				return gmdate( 'Y-m-d H:i:s', strtotime( $datetime, $now ) );
919
			}
920
		}
921
922
		$datetime = array_map( 'absint', $datetime );
923
924
		if ( ! isset( $datetime['year'] ) )
925
			$datetime['year'] = gmdate( 'Y', $now );
926
927
		if ( ! isset( $datetime['month'] ) )
928
			$datetime['month'] = ( $default_to_max ) ? 12 : 1;
929
930
		if ( ! isset( $datetime['day'] ) )
931
			$datetime['day'] = ( $default_to_max ) ? (int) date( 't', mktime( 0, 0, 0, $datetime['month'], 1, $datetime['year'] ) ) : 1;
932
933
		if ( ! isset( $datetime['hour'] ) )
934
			$datetime['hour'] = ( $default_to_max ) ? 23 : 0;
935
936
		if ( ! isset( $datetime['minute'] ) )
937
			$datetime['minute'] = ( $default_to_max ) ? 59 : 0;
938
939
		if ( ! isset( $datetime['second'] ) )
940
			$datetime['second'] = ( $default_to_max ) ? 59 : 0;
941
942
		return sprintf( '%04d-%02d-%02d %02d:%02d:%02d', $datetime['year'], $datetime['month'], $datetime['day'], $datetime['hour'], $datetime['minute'], $datetime['second'] );
943
	}
944
945
	/**
946
	 * Builds a query string for comparing time values (hour, minute, second).
947
	 *
948
	 * If just hour, minute, or second is set than a normal comparison will be done.
949
	 * However if multiple values are passed, a pseudo-decimal time will be created
950
	 * in order to be able to accurately compare against.
951
	 *
952
	 * @since 3.7.0
953
	 * @access public
954
	 *
955
	 * @param string $column The column to query against. Needs to be pre-validated!
956
	 * @param string $compare The comparison operator. Needs to be pre-validated!
957
	 * @param int|null $hour Optional. An hour value (0-23).
958
	 * @param int|null $minute Optional. A minute value (0-59).
959
	 * @param int|null $second Optional. A second value (0-59).
960
	 * @return string|false A query part or false on failure.
961
	 */
962
	public function build_time_query( $column, $compare, $hour = null, $minute = null, $second = null ) {
963
		global $wpdb;
964
965
		// Have to have at least one
966
		if ( ! isset( $hour ) && ! isset( $minute ) && ! isset( $second ) )
967
			return false;
968
969
		// Complex combined queries aren't supported for multi-value queries
970
		if ( in_array( $compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ) ) ) {
971
			$return = array();
972
973 View Code Duplication
			if ( isset( $hour ) && false !== ( $value = $this->build_value( $compare, $hour ) ) )
974
				$return[] = "HOUR( $column ) $compare $value";
975
976 View Code Duplication
			if ( isset( $minute ) && false !== ( $value = $this->build_value( $compare, $minute ) ) )
977
				$return[] = "MINUTE( $column ) $compare $value";
978
979 View Code Duplication
			if ( isset( $second ) && false !== ( $value = $this->build_value( $compare, $second ) ) )
980
				$return[] = "SECOND( $column ) $compare $value";
981
982
			return implode( ' AND ', $return );
983
		}
984
985
		// Cases where just one unit is set
986
		if ( isset( $hour ) && ! isset( $minute ) && ! isset( $second ) && false !== ( $value = $this->build_value( $compare, $hour ) ) ) {
987
			return "HOUR( $column ) $compare $value";
988
		} elseif ( ! isset( $hour ) && isset( $minute ) && ! isset( $second ) && false !== ( $value = $this->build_value( $compare, $minute ) ) ) {
989
			return "MINUTE( $column ) $compare $value";
990
		} elseif ( ! isset( $hour ) && ! isset( $minute ) && isset( $second ) && false !== ( $value = $this->build_value( $compare, $second ) ) ) {
991
			return "SECOND( $column ) $compare $value";
992
		}
993
994
		// Single units were already handled. Since hour & second isn't allowed, minute must to be set.
995
		if ( ! isset( $minute ) )
996
			return false;
997
998
		$format = $time = '';
999
1000
		// Hour
1001
		if ( null !== $hour ) {
1002
			$format .= '%H.';
1003
			$time   .= sprintf( '%02d', $hour ) . '.';
1004
		} else {
1005
			$format .= '0.';
1006
			$time   .= '0.';
1007
		}
1008
1009
		// Minute
1010
		$format .= '%i';
1011
		$time   .= sprintf( '%02d', $minute );
1012
1013
		if ( isset( $second ) ) {
1014
			$format .= '%s';
1015
			$time   .= sprintf( '%02d', $second );
1016
		}
1017
1018
		return $wpdb->prepare( "DATE_FORMAT( $column, %s ) $compare %f", $format, $time );
1019
	}
1020
}
1021