1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Database query class. |
5
|
|
|
* |
6
|
|
|
* @package WordPoints |
7
|
|
|
* @since 2.1.0 |
8
|
|
|
*/ |
9
|
|
|
|
10
|
|
|
/** |
11
|
|
|
* Database query bootstrap. |
12
|
|
|
* |
13
|
|
|
* This class provides a bootstrap that can be extended to provide a simple, common |
14
|
|
|
* interface for querying a database. The child class defines the table schema, and |
15
|
|
|
* this bootstrap takes care of the rest. |
16
|
|
|
* |
17
|
|
|
* @since 2.1.0 |
18
|
|
|
*/ |
19
|
|
|
class WordPoints_DB_Query { |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* The name of the table this query class is for. |
23
|
|
|
* |
24
|
|
|
* This should be the full name of the table, including the prefix. You will |
25
|
|
|
* therefore likely need to define it from inside your constructor. |
26
|
|
|
* |
27
|
|
|
* @since 2.1.0 |
28
|
|
|
* |
29
|
|
|
* @var string |
30
|
|
|
*/ |
31
|
|
|
protected $table_name; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* The columns in the table being queried. |
35
|
|
|
* |
36
|
|
|
* The keys are the names of the columns. The values are arrays that support the |
37
|
|
|
* following keys: |
38
|
|
|
* |
39
|
|
|
* - format (required) The format (%s, %d, or %f) to use when passing the values |
40
|
|
|
* for this format to $wpdb->prepare(). |
41
|
|
|
* - values (optional) An array of values that this column can have. Any values |
42
|
|
|
* that aren't in this list will be discarded from a query. |
43
|
|
|
* - unsigned (optional) Whether the value is unsigned. If this is true, values |
44
|
|
|
* for this column will be rejected if they are not positive. |
45
|
|
|
* - is_date (optional) Whether this is a DATETIME field. If so date queries will |
46
|
|
|
* be supported. |
47
|
|
|
* |
48
|
|
|
* For each column in this array, the following query args are supported: |
49
|
|
|
* |
50
|
|
|
* - "{$column}" A single value that this column should have. |
51
|
|
|
* - "{$column}__compare" How to compare the above value to the value in the DB. |
52
|
|
|
* The default is '='. |
53
|
|
|
* - "{$column}__in" An array of values that this column may have. |
54
|
|
|
* - "{$column}__not_in" An array of values that this column may not have. |
55
|
|
|
* |
56
|
|
|
* Where {$column} is the name of the column. |
57
|
|
|
* |
58
|
|
|
* The "{$column}" query arg takes precedence over the "{$column}__in" and |
59
|
|
|
* "{$column}__not_in" query args. |
60
|
|
|
* |
61
|
|
|
* However, if the column specifies that is_date is true, then the above are not |
62
|
|
|
* supported, and the following are offered instead: |
63
|
|
|
* |
64
|
|
|
* - "{$column}_query" Arguments to pass to a WP_Date_Query. |
65
|
|
|
* |
66
|
|
|
* @since 2.1.0 |
67
|
|
|
* |
68
|
|
|
* @var array[] |
69
|
|
|
*/ |
70
|
|
|
protected $columns = array(); |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* The slug of the meta type. |
74
|
|
|
* |
75
|
|
|
* If this is defined, the 'meta_query', 'meta_key', 'meta_value', |
76
|
|
|
* 'meta_compare', and 'meta_type' args are supported, and will be passed to |
77
|
|
|
* WP_Meta_Query. |
78
|
|
|
* |
79
|
|
|
* @since 2.1.0 |
80
|
|
|
* |
81
|
|
|
* @var string |
82
|
|
|
*/ |
83
|
|
|
protected $meta_type; |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* The default values for the query args. |
87
|
|
|
* |
88
|
|
|
* You can override this entirely if needed, or just modify it in your |
89
|
|
|
* constructor before calling parent::__construct(). |
90
|
|
|
* |
91
|
|
|
* @since 2.1.0 |
92
|
|
|
* |
93
|
|
|
* @var array |
94
|
|
|
*/ |
95
|
|
|
protected $defaults = array( |
96
|
|
|
'start' => 0, |
97
|
|
|
'order' => 'DESC', |
98
|
|
|
); |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* A list of args that are deprecated and information about their replacements. |
102
|
|
|
* |
103
|
|
|
* Each element of the array should contain the following key-value pairs: |
104
|
|
|
* |
105
|
|
|
* - 'replacement' - The replacement arg. |
106
|
|
|
* - 'version' - The version in which this arg was deprecated. |
107
|
|
|
* - 'class' - The class this arg is from. Usually you will just want to |
108
|
|
|
* use `__CLASS__` here. |
109
|
|
|
* |
110
|
|
|
* @since 2.3.0 |
111
|
|
|
* |
112
|
|
|
* @var string[][] |
113
|
|
|
*/ |
114
|
|
|
protected $deprecated_args = array(); |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* The query arguments. |
118
|
|
|
* |
119
|
|
|
* @since 2.1.0 |
120
|
|
|
* |
121
|
|
|
* @type array $args |
122
|
|
|
*/ |
123
|
|
|
protected $args = array(); |
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* Whether the query is ready for execution, or still needs to be prepared. |
127
|
|
|
* |
128
|
|
|
* @since 2.1.0 |
129
|
|
|
* |
130
|
|
|
* @type bool $is_query_ready |
131
|
|
|
*/ |
132
|
|
|
protected $is_query_ready = false; |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* The SELECT statement for the query. |
136
|
|
|
* |
137
|
|
|
* @since 2.1.0 |
138
|
|
|
* |
139
|
|
|
* @type string $select |
140
|
|
|
*/ |
141
|
|
|
protected $select; |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* The SELECT COUNT statement for a count query. |
145
|
|
|
* |
146
|
|
|
* @since 2.1.0 |
147
|
|
|
* |
148
|
|
|
* @type string $select_count |
149
|
|
|
*/ |
150
|
|
|
protected $select_count = 'SELECT COUNT(*)'; |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* The JOIN query with the meta table. |
154
|
|
|
* |
155
|
|
|
* @since 2.1.0 |
156
|
|
|
* |
157
|
|
|
* @type string $meta_join |
158
|
|
|
*/ |
159
|
|
|
protected $meta_join; |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* The WHERE clause for the query. |
163
|
|
|
* |
164
|
|
|
* @since 2.1.0 |
165
|
|
|
* |
166
|
|
|
* @type string $where |
167
|
|
|
*/ |
168
|
|
|
protected $where; |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* The array of conditions for the WHERE clause. |
172
|
|
|
* |
173
|
|
|
* @since 2.1.0 |
174
|
|
|
* |
175
|
|
|
* @type array $wheres |
176
|
|
|
*/ |
177
|
|
|
protected $wheres = array(); |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* The LIMIT clause for the query. |
181
|
|
|
* |
182
|
|
|
* @since 2.1.0 |
183
|
|
|
* |
184
|
|
|
* @type string $limit |
185
|
|
|
*/ |
186
|
|
|
protected $limit; |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* The ORDER clause for the query. |
190
|
|
|
* |
191
|
|
|
* @since 2.1.0 |
192
|
|
|
* |
193
|
|
|
* @type string $order |
194
|
|
|
*/ |
195
|
|
|
protected $order; |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* Holds the meta query object when a meta query is being performed. |
199
|
|
|
* |
200
|
|
|
* @since 2.1.0 |
201
|
|
|
* |
202
|
|
|
* @type WP_Meta_Query $meta_query |
203
|
|
|
*/ |
204
|
|
|
protected $meta_query; |
205
|
|
|
|
206
|
|
|
// |
207
|
|
|
// Public Methods. |
208
|
|
|
// |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Construct the class. |
212
|
|
|
* |
213
|
|
|
* All of the arguments are expected *not* to be SQL escaped. |
214
|
|
|
* |
215
|
|
|
* @since 2.1.0 |
216
|
|
|
* |
217
|
|
|
* @see WP_Meta_Query for the proper arguments for 'meta_query', 'meta_key', 'meta_value', 'meta_compare', and 'meta_type'. |
218
|
|
|
* |
219
|
|
|
* @param array $args { |
220
|
|
|
* The arguments for the query. |
221
|
|
|
* |
222
|
|
|
* @type string|array $fields Fields to include in the results. Default is all fields. |
223
|
|
|
* @type int $limit The maximum number of results to return. Default is null (no limit). |
224
|
|
|
* @type int $start The start for the LIMIT clause. Default: 0. |
225
|
|
|
* @type string $order_by The field to use to order the results. |
226
|
|
|
* @type string $order The order for the query: ASC or DESC (default). |
227
|
|
|
* @type string $meta_key See WP_Meta_Query. |
228
|
|
|
* @type mixed $meta_value See WP_Meta_Query. |
229
|
|
|
* @type string $meta_compare See WP_Meta_Query. |
230
|
|
|
* @type string $meta_type See WP_Meta_Query. |
231
|
|
|
* @type array $meta_query See WP_Meta_Query. |
232
|
|
|
* } |
233
|
|
|
*/ |
234
|
|
|
public function __construct( $args = array() ) { |
235
|
|
|
|
236
|
|
View Code Duplication |
foreach ( $this->deprecated_args as $arg => $data ) { |
|
|
|
|
237
|
|
|
if ( isset( $args[ $arg ] ) ) { |
238
|
|
|
|
239
|
|
|
_deprecated_argument( |
240
|
|
|
esc_html( "{$data['class']}::__construct" ) |
241
|
|
|
, esc_html( $data['version'] ) |
242
|
|
|
, esc_html( "{$arg} is deprecated, use {$data['replacement']} instead" ) |
243
|
|
|
); |
244
|
|
|
|
245
|
|
|
$args[ $data['replacement'] ] = $args[ $arg ]; |
246
|
|
|
|
247
|
|
|
unset( $args[ $arg ] ); |
248
|
|
|
} |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
$this->args = array_merge( $this->defaults, $args ); |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
/** |
255
|
|
|
* Get a query arg. |
256
|
|
|
* |
257
|
|
|
* @since 2.1.0 |
258
|
|
|
* |
259
|
|
|
* @param string $arg The query arg whose value to retrieve. |
260
|
|
|
* |
261
|
|
|
* @return mixed|null The query arg's value, or null if it isn't set. |
262
|
|
|
*/ |
263
|
|
|
public function get_arg( $arg ) { |
264
|
|
|
|
265
|
|
|
if ( isset( $this->deprecated_args[ $arg ] ) ) { |
266
|
|
|
|
267
|
|
|
_deprecated_argument( |
268
|
|
|
esc_html( "{$this->deprecated_args[ $arg ]['class']}::get_arg" ) |
269
|
|
|
, esc_html( $this->deprecated_args[ $arg ]['version'] ) |
270
|
|
|
, esc_html( "{$arg} is deprecated, use {$this->deprecated_args[ $arg ]['replacement']} instead" ) |
271
|
|
|
); |
272
|
|
|
|
273
|
|
|
$arg = $this->deprecated_args[ $arg ]['replacement']; |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
if ( isset( $this->args[ $arg ] ) ) { |
277
|
|
|
return $this->args[ $arg ]; |
278
|
|
|
} else { |
279
|
|
|
return null; |
280
|
|
|
} |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
/** |
284
|
|
|
* Set arguments for the query. |
285
|
|
|
* |
286
|
|
|
* All of the arguments supported by the constructor may be passed in here, and |
287
|
|
|
* will be merged into the array of existing args. |
288
|
|
|
* |
289
|
|
|
* @since 2.1.0 |
290
|
|
|
* |
291
|
|
|
* @param array $args A list of arguments to set and their values. |
292
|
|
|
* |
293
|
|
|
* @return WordPoints_DB_Query To allow for method chaining. |
294
|
|
|
*/ |
295
|
|
|
public function set_args( array $args ) { |
296
|
|
|
|
297
|
|
View Code Duplication |
foreach ( $this->deprecated_args as $arg => $data ) { |
|
|
|
|
298
|
|
|
if ( isset( $args[ $arg ] ) ) { |
299
|
|
|
|
300
|
|
|
_deprecated_argument( |
301
|
|
|
esc_html( "{$data['class']}::set_args" ) |
302
|
|
|
, esc_html( $data['version'] ) |
303
|
|
|
, esc_html( "{$arg} is deprecated, use {$data['replacement']} instead" ) |
304
|
|
|
); |
305
|
|
|
|
306
|
|
|
$args[ $data['replacement'] ] = $args[ $arg ]; |
307
|
|
|
|
308
|
|
|
unset( $args[ $arg ] ); |
309
|
|
|
} |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
$this->args = array_merge( $this->args, $args ); |
313
|
|
|
|
314
|
|
|
$this->is_query_ready = false; |
315
|
|
|
|
316
|
|
|
return $this; |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
/** |
320
|
|
|
* Count the number of results. |
321
|
|
|
* |
322
|
|
|
* When used with a query that contains a LIMIT clause, this method currently |
323
|
|
|
* returns the count of the query ignoring the LIMIT, as would be the case with |
324
|
|
|
* any similar query. However, this behaviour is not hardened and should not be |
325
|
|
|
* relied upon. Make inquiry before assuming the constancy of this behaviour. |
326
|
|
|
* |
327
|
|
|
* @since 2.1.0 |
328
|
|
|
* |
329
|
|
|
* @return int The number of results. |
330
|
|
|
*/ |
331
|
|
|
public function count() { |
332
|
|
|
|
333
|
|
|
global $wpdb; |
334
|
|
|
|
335
|
|
|
$count = (int) $wpdb->get_var( $this->get_sql( 'SELECT COUNT' ) ); // WPCS: unprepared SQL, cache OK |
336
|
|
|
|
337
|
|
|
return $count; |
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
/** |
341
|
|
|
* Get the results for the query. |
342
|
|
|
* |
343
|
|
|
* @since 2.1.0 |
344
|
|
|
* |
345
|
|
|
* @param string $method The method to use. Options are 'results', 'row', 'col', |
346
|
|
|
* and 'var'. |
347
|
|
|
* |
348
|
|
|
* @return mixed The results of the query, or false on failure. |
349
|
|
|
*/ |
350
|
|
|
public function get( $method = 'results' ) { |
351
|
|
|
|
352
|
|
|
global $wpdb; |
353
|
|
|
|
354
|
|
|
$methods = array( 'results', 'row', 'col', 'var' ); |
355
|
|
|
|
356
|
|
|
if ( ! in_array( $method, $methods ) ) { |
357
|
|
|
|
358
|
|
|
_doing_it_wrong( __METHOD__, esc_html( sprintf( 'WordPoints Debug Error: invalid get method %s, possible values are %s', $method, implode( ', ', $methods ) ) ), '1.0.0' ); |
359
|
|
|
|
360
|
|
|
return false; |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
$result = $wpdb->{"get_{$method}"}( $this->get_sql() ); |
364
|
|
|
|
365
|
|
|
return $result; |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
/** |
369
|
|
|
* Get the SQL for the query. |
370
|
|
|
* |
371
|
|
|
* This function can return the SQL for a SELECT or SELECT COUNT query. To |
372
|
|
|
* specify which one to return, set the $select_type parameter. Defaults to |
373
|
|
|
* SELECT. |
374
|
|
|
* |
375
|
|
|
* This function is public for debugging purposes. |
376
|
|
|
* |
377
|
|
|
* @since 2.1.0 |
378
|
|
|
* |
379
|
|
|
* @param string $select_type The type of query, SELECT, or SELECT COUNT. |
380
|
|
|
* |
381
|
|
|
* @return string The SQL for the query. |
382
|
|
|
*/ |
383
|
|
|
public function get_sql( $select_type = 'SELECT' ) { |
384
|
|
|
|
385
|
|
|
$this->prepare_query(); |
386
|
|
|
|
387
|
|
|
$select = ( 'SELECT COUNT' === $select_type ) |
388
|
|
|
? $this->select_count |
389
|
|
|
: $this->select; |
390
|
|
|
|
391
|
|
|
return $select |
392
|
|
|
. "\nFROM `{$this->table_name}`\n" |
393
|
|
|
. $this->meta_join |
394
|
|
|
. $this->where |
395
|
|
|
. $this->order |
396
|
|
|
. $this->limit; |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
// |
400
|
|
|
// Filter Methods. |
401
|
|
|
// |
402
|
|
|
|
403
|
|
|
/** |
404
|
|
|
* Filter date query valid columns for WP_Date_Query. |
405
|
|
|
* |
406
|
|
|
* @since 2.1.0 |
407
|
|
|
* |
408
|
|
|
* @WordPress\filter date_query_valid_columns Added and subsequently removed by |
409
|
|
|
* self::prepare_date_where(). |
410
|
|
|
* |
411
|
|
|
* @param string[] $valid_columns The names of the valid columns for date queries. |
412
|
|
|
* |
413
|
|
|
* @return string[] The valid columns. |
414
|
|
|
*/ |
415
|
|
|
public function date_query_valid_columns_filter( $valid_columns ) { |
416
|
|
|
|
417
|
|
|
$valid_columns = array_merge( |
418
|
|
|
$valid_columns |
419
|
|
|
, array_keys( |
420
|
|
|
wp_list_filter( $this->columns, array( 'is_date' => true ) ) |
421
|
|
|
) |
422
|
|
|
); |
423
|
|
|
|
424
|
|
|
return $valid_columns; |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
// |
428
|
|
|
// Protected Methods. |
429
|
|
|
// |
430
|
|
|
|
431
|
|
|
/** |
432
|
|
|
* Prepare the query. |
433
|
|
|
* |
434
|
|
|
* @since 2.1.0 |
435
|
|
|
*/ |
436
|
|
|
protected function prepare_query() { |
437
|
|
|
|
438
|
|
|
if ( ! $this->is_query_ready ) { |
439
|
|
|
|
440
|
|
|
$this->prepare_select(); |
441
|
|
|
$this->prepare_where(); |
442
|
|
|
$this->prepare_order_by(); |
443
|
|
|
$this->prepare_limit(); |
444
|
|
|
|
445
|
|
|
$this->is_query_ready = true; |
446
|
|
|
} |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
/** |
450
|
|
|
* Prepare the select statement. |
451
|
|
|
* |
452
|
|
|
* @since 2.1.0 |
453
|
|
|
*/ |
454
|
|
|
protected function prepare_select() { |
455
|
|
|
|
456
|
|
|
$all_fields = array_keys( $this->columns ); |
457
|
|
|
$fields = array(); |
458
|
|
|
|
459
|
|
|
if ( ! empty( $this->args['fields'] ) ) { |
460
|
|
|
|
461
|
|
|
$fields = (array) $this->args['fields']; |
462
|
|
|
$diff = array_diff( $fields, $all_fields ); |
463
|
|
|
$fields = array_intersect( $all_fields, $fields ); |
464
|
|
|
|
465
|
|
|
if ( ! empty( $diff ) ) { |
466
|
|
|
_doing_it_wrong( __METHOD__, esc_html( 'WordPoints Debug Error: invalid field(s) "' . implode( '", "', $diff ) . '" given' ), '1.0.0' ); |
467
|
|
|
} |
468
|
|
|
} |
469
|
|
|
|
470
|
|
|
// Pull all fields by default. |
471
|
|
|
if ( empty( $fields ) ) { |
472
|
|
|
$fields = $all_fields; |
473
|
|
|
} |
474
|
|
|
|
475
|
|
|
$fields = implode( ', ', array_map( 'wordpoints_escape_mysql_identifier', $fields ) ); |
476
|
|
|
|
477
|
|
|
$this->select = "SELECT {$fields}"; |
478
|
|
|
} |
479
|
|
|
|
480
|
|
|
/** |
481
|
|
|
* Validates a value against an array of sanitizing functions. |
482
|
|
|
* |
483
|
|
|
* @since 2.1.0 |
484
|
|
|
* |
485
|
|
|
* @param mixed $value The value to validate. |
486
|
|
|
* @param callable[] $validators The validators to validate it against. |
487
|
|
|
* |
488
|
|
|
* @return mixed The validated value, or false if invalid. |
489
|
|
|
*/ |
490
|
|
|
protected function validate_value( $value, $validators ) { |
491
|
|
|
|
492
|
|
|
foreach ( $validators as $validator ) { |
493
|
|
|
|
494
|
|
|
$value = call_user_func_array( $validator, array( &$value ) ); |
495
|
|
|
|
496
|
|
|
if ( false === $value ) { |
497
|
|
|
break; |
498
|
|
|
} |
499
|
|
|
} |
500
|
|
|
|
501
|
|
|
return $value; |
502
|
|
|
} |
503
|
|
|
|
504
|
|
|
/** |
505
|
|
|
* Validates an array of values against an array of sanitizing functions. |
506
|
|
|
* |
507
|
|
|
* @since 2.1.0 |
508
|
|
|
* |
509
|
|
|
* @param array $values The values to validate. |
510
|
|
|
* @param callable[] $validators The validators to validate each value against. |
511
|
|
|
* |
512
|
|
|
* @return array The validated values, with any invalid ones removed. |
513
|
|
|
*/ |
514
|
|
|
protected function validate_values( $values, $validators ) { |
515
|
|
|
|
516
|
|
|
foreach ( $values as $index => $value ) { |
517
|
|
|
|
518
|
|
|
$value = $this->validate_value( $value, $validators ); |
519
|
|
|
|
520
|
|
|
if ( false === $value ) { |
521
|
|
|
unset( $values[ $index ] ); |
522
|
|
|
} |
523
|
|
|
} |
524
|
|
|
|
525
|
|
|
return $values; |
526
|
|
|
} |
527
|
|
|
|
528
|
|
|
/** |
529
|
|
|
* Validate an unsigned column. |
530
|
|
|
* |
531
|
|
|
* The value must be positive, zero-inclusive. We can't just use |
532
|
|
|
* wordpoints_posint() because it is zero exclusive. |
533
|
|
|
* |
534
|
|
|
* @since 2.1.0 |
535
|
|
|
* |
536
|
|
|
* @param mixed $value The value to validate. |
537
|
|
|
* |
538
|
|
|
* @return int|false The validated value or false. |
539
|
|
|
*/ |
540
|
|
|
protected function validate_unsigned_column( $value ) { |
541
|
|
|
|
542
|
|
|
if ( false !== wordpoints_int( $value ) && $value >= 0 ) { |
543
|
|
|
return $value; |
544
|
|
|
} |
545
|
|
|
|
546
|
|
|
return false; |
547
|
|
|
} |
548
|
|
|
|
549
|
|
|
/** |
550
|
|
|
* Get an array of validating/sanitizing functions for the values of a column. |
551
|
|
|
* |
552
|
|
|
* @since 2.1.0 |
553
|
|
|
* |
554
|
|
|
* @param array $data The data for the column. |
555
|
|
|
* |
556
|
|
|
* @return callable[] The validation functions. |
557
|
|
|
*/ |
558
|
|
|
protected function get_validators_for_column( $data ) { |
559
|
|
|
|
560
|
|
|
$validators = array(); |
561
|
|
|
|
562
|
|
|
// Default validators for integer columns. |
563
|
|
|
if ( '%d' === $data['format'] ) { |
564
|
|
|
if ( ! empty( $data['unsigned'] ) ) { |
565
|
|
|
$validators[] = array( $this, 'validate_unsigned_column' ); |
566
|
|
|
} else { |
567
|
|
|
$validators[] = 'wordpoints_int'; |
568
|
|
|
} |
569
|
|
|
} |
570
|
|
|
|
571
|
|
|
return $validators; |
572
|
|
|
} |
573
|
|
|
|
574
|
|
|
/** |
575
|
|
|
* Prepare the conditions for the WHERE clause for a column. |
576
|
|
|
* |
577
|
|
|
* @since 2.1.0 |
578
|
|
|
* |
579
|
|
|
* @param string $column The column name. |
580
|
|
|
* @param array $data The column data. |
581
|
|
|
*/ |
582
|
|
|
protected function prepare_column_where( $column, $data ) { |
583
|
|
|
|
584
|
|
|
// If a single value has been supplied for the column, it takes precedence. |
585
|
|
|
if ( isset( $this->args[ $column ] ) ) { |
586
|
|
|
$this->prepare_column( $column, $data ); |
587
|
|
|
} elseif ( isset( $this->args[ "{$column}__in" ] ) ) { |
588
|
|
|
$this->prepare_column__in( $column, $data ); |
589
|
|
|
} elseif ( isset( $this->args[ "{$column}__not_in" ] ) ) { |
590
|
|
|
$this->prepare_column__in( $column, $data, 'NOT IN' ); |
591
|
|
|
} |
592
|
|
|
} |
593
|
|
|
|
594
|
|
|
/** |
595
|
|
|
* Prepare a single-value condition for the WHERE clause for a column. |
596
|
|
|
* |
597
|
|
|
* @since 2.1.0 |
598
|
|
|
* |
599
|
|
|
* @param string $column The name of the column |
600
|
|
|
* @param array $data The column data. |
601
|
|
|
*/ |
602
|
|
|
protected function prepare_column( $column, $data ) { |
603
|
|
|
|
604
|
|
|
global $wpdb; |
605
|
|
|
|
606
|
|
|
if ( |
607
|
|
|
isset( $data['values'] ) |
608
|
|
|
&& ! in_array( $this->args[ $column ], $data['values'], true ) |
609
|
|
|
) { |
610
|
|
|
return; |
611
|
|
|
} |
612
|
|
|
|
613
|
|
|
$value = $this->validate_value( |
614
|
|
|
$this->args[ $column ] |
615
|
|
|
, $this->get_validators_for_column( $data ) |
616
|
|
|
); |
617
|
|
|
|
618
|
|
|
if ( false === $value ) { |
619
|
|
|
return; |
620
|
|
|
} |
621
|
|
|
|
622
|
|
|
$compare = $this->get_comparator_for_column( $column, $data ); |
623
|
|
|
|
624
|
|
|
$column = wordpoints_escape_mysql_identifier( $column ); |
625
|
|
|
|
626
|
|
|
$this->wheres[] = $wpdb->prepare( // WPCS: unprepared SQL OK. |
627
|
|
|
"{$column} {$compare} {$data['format']}" |
628
|
|
|
, $value |
629
|
|
|
); |
630
|
|
|
} |
631
|
|
|
|
632
|
|
|
/** |
633
|
|
|
* Get the comparator for a column. |
634
|
|
|
* |
635
|
|
|
* @since 2.1.0 |
636
|
|
|
* |
637
|
|
|
* @param string $column The column name. |
638
|
|
|
* @param array $data The column data. |
639
|
|
|
* |
640
|
|
|
* @return string The comparator for the column. |
641
|
|
|
*/ |
642
|
|
|
protected function get_comparator_for_column( $column, $data ) { |
643
|
|
|
|
644
|
|
|
$comparisons = array( '=', '<', '>', '<>', '!=', '<=', '>=' ); |
645
|
|
|
|
646
|
|
|
// MySQL doesn't support LIKE and NOT LIKE for int columns. |
647
|
|
|
// See https://stackoverflow.com/q/8422455/1924128 |
648
|
|
|
if ( '%s' === $data['format'] ) { |
649
|
|
|
$comparisons = array_merge( $comparisons, array( 'LIKE', 'NOT LIKE' ) ); |
650
|
|
|
} |
651
|
|
|
|
652
|
|
|
$comparator = '='; |
653
|
|
|
|
654
|
|
|
if ( |
655
|
|
|
isset( $this->args[ "{$column}__compare" ] ) |
656
|
|
|
&& in_array( $this->args[ "{$column}__compare" ], $comparisons, true ) |
657
|
|
|
) { |
658
|
|
|
$comparator = $this->args[ "{$column}__compare" ]; |
659
|
|
|
} |
660
|
|
|
|
661
|
|
|
return $comparator; |
662
|
|
|
} |
663
|
|
|
|
664
|
|
|
/** |
665
|
|
|
* Prepare the IN or NOT IN conditions for a column. |
666
|
|
|
* |
667
|
|
|
* @since 2.1.0 |
668
|
|
|
* |
669
|
|
|
* @param string $column The name of the column. |
670
|
|
|
* @param array $data The column data. |
671
|
|
|
* @param string $type The type of IN clause, IN or NOT IN. |
672
|
|
|
*/ |
673
|
|
|
protected function prepare_column__in( $column, $data, $type = 'IN' ) { |
674
|
|
|
|
675
|
|
|
$key = "{$column}__" . strtolower( str_replace( ' ', '_', $type ) ); |
676
|
|
|
|
677
|
|
|
if ( empty( $this->args[ $key ] ) || ! is_array( $this->args[ $key ] ) ) { |
678
|
|
|
return; |
679
|
|
|
} |
680
|
|
|
|
681
|
|
|
$values = $this->args[ $key ]; |
682
|
|
|
|
683
|
|
|
if ( isset( $data['values'] ) ) { |
684
|
|
|
$values = array_intersect( $values, $data['values'] ); |
685
|
|
|
} else { |
686
|
|
|
$values = $this->validate_values( |
687
|
|
|
$values |
688
|
|
|
, $this->get_validators_for_column( $data ) |
689
|
|
|
); |
690
|
|
|
} |
691
|
|
|
|
692
|
|
|
if ( empty( $values ) ) { |
693
|
|
|
return; |
694
|
|
|
} |
695
|
|
|
|
696
|
|
|
$in = wordpoints_prepare__in( $values, $data['format'] ); |
697
|
|
|
|
698
|
|
|
if ( false === $in ) { |
699
|
|
|
return; |
700
|
|
|
} |
701
|
|
|
|
702
|
|
|
$column = wordpoints_escape_mysql_identifier( $column ); |
703
|
|
|
|
704
|
|
|
$this->wheres[] = "{$column} {$type} ({$in})"; |
705
|
|
|
} |
706
|
|
|
|
707
|
|
|
/** |
708
|
|
|
* Prepare the WHERE clause for the query. |
709
|
|
|
* |
710
|
|
|
* @since 2.1.0 |
711
|
|
|
*/ |
712
|
|
|
protected function prepare_where() { |
713
|
|
|
|
714
|
|
|
$this->wheres = array(); |
715
|
|
|
|
716
|
|
|
foreach ( $this->columns as $column => $data ) { |
717
|
|
|
|
718
|
|
|
if ( ! empty( $data['is_date'] ) ) { |
719
|
|
|
$this->prepare_date_where( $column ); |
720
|
|
|
} else { |
721
|
|
|
$this->prepare_column_where( $column, $data ); |
722
|
|
|
} |
723
|
|
|
} |
724
|
|
|
|
725
|
|
|
$this->prepare_meta_where(); |
726
|
|
|
|
727
|
|
|
if ( ! empty( $this->wheres ) ) { |
728
|
|
|
$this->where = 'WHERE ' . implode( ' AND ', $this->wheres ) . "\n"; |
729
|
|
|
} |
730
|
|
|
} |
731
|
|
|
|
732
|
|
|
/** |
733
|
|
|
* Prepare the LIMIT clause for the query. |
734
|
|
|
* |
735
|
|
|
* @since 2.1.0 |
736
|
|
|
*/ |
737
|
|
|
protected function prepare_limit() { |
738
|
|
|
|
739
|
|
|
// MySQL doesn't allow for the offset without a limit, so if no limit is set |
740
|
|
|
// we can ignore the start arg. See https://stackoverflow.com/a/271650/1924128 |
741
|
|
|
if ( ! isset( $this->args['limit'] ) ) { |
742
|
|
|
return; |
743
|
|
|
} |
744
|
|
|
|
745
|
|
|
foreach ( array( 'limit', 'start' ) as $key ) { |
746
|
|
|
|
747
|
|
|
// Save a backup of the arg value since wordpoints_int() is by reference. |
748
|
|
|
$arg = $this->args[ $key ]; |
749
|
|
|
|
750
|
|
|
if ( false === wordpoints_int( $this->args[ $key ] ) ) { |
751
|
|
|
|
752
|
|
|
_doing_it_wrong( |
753
|
|
|
__METHOD__ |
754
|
|
|
, sprintf( |
755
|
|
|
"WordPoints Debug Error: '%s' must be a positive integer, %s given" |
756
|
|
|
, esc_html( $key ) |
757
|
|
|
, esc_html( strval( $arg ) ? $arg : gettype( $arg ) ) |
758
|
|
|
) |
759
|
|
|
, '1.0.0' |
760
|
|
|
); |
761
|
|
|
|
762
|
|
|
$this->args[ $key ] = 0; |
763
|
|
|
} |
764
|
|
|
} |
765
|
|
|
|
766
|
|
|
if ( $this->args['limit'] > 0 && $this->args['start'] >= 0 ) { |
767
|
|
|
$this->limit = "LIMIT {$this->args['start']}, {$this->args['limit']}"; |
768
|
|
|
} |
769
|
|
|
} |
770
|
|
|
|
771
|
|
|
/** |
772
|
|
|
* Prepare the ORDER BY clause for the query. |
773
|
|
|
* |
774
|
|
|
* @since 2.1.0 |
775
|
|
|
*/ |
776
|
|
|
protected function prepare_order_by() { |
777
|
|
|
|
778
|
|
|
if ( empty( $this->args['order_by'] ) ) { |
779
|
|
|
return; |
780
|
|
|
} |
781
|
|
|
|
782
|
|
|
$order = $this->args['order']; |
783
|
|
|
$order_by = $this->args['order_by']; |
784
|
|
|
|
785
|
|
|
if ( ! in_array( $order, array( 'DESC', 'ASC' ) ) ) { |
786
|
|
|
|
787
|
|
|
_doing_it_wrong( __METHOD__, esc_html( "WordPoints Debug Error: invalid 'order' \"{$order}\", possible values are DESC and ASC" ), '1.0.0' ); |
788
|
|
|
$order = 'DESC'; |
789
|
|
|
} |
790
|
|
|
|
791
|
|
|
if ( 'meta_value' === $order_by ) { |
792
|
|
|
|
793
|
|
|
global $wpdb; |
794
|
|
|
|
795
|
|
|
$meta_table_name = wordpoints_escape_mysql_identifier( |
796
|
|
|
$wpdb->{"{$this->meta_type}meta"} |
797
|
|
|
); |
798
|
|
|
|
799
|
|
|
if ( isset( $this->args['meta_type'] ) ) { |
800
|
|
|
|
801
|
|
|
$meta_type = $this->meta_query->get_cast_for_type( $this->args['meta_type'] ); |
802
|
|
|
$order_by = "CAST({$meta_table_name}.meta_value AS {$meta_type})"; |
803
|
|
|
|
804
|
|
|
} else { |
805
|
|
|
|
806
|
|
|
$order_by = "{$meta_table_name}.meta_value"; |
807
|
|
|
} |
808
|
|
|
|
809
|
|
|
} elseif ( isset( $this->columns[ $order_by ] ) ) { |
810
|
|
|
|
811
|
|
|
$order_by = wordpoints_escape_mysql_identifier( $order_by ); |
812
|
|
|
|
813
|
|
|
} else { |
814
|
|
|
|
815
|
|
|
_doing_it_wrong( __METHOD__, esc_html( "WordPoints Debug Error: invalid 'order_by' \"{$order_by}\", possible values are " . implode( ', ', array_keys( $this->columns ) ) ), '1.0.0' ); |
816
|
|
|
return; |
817
|
|
|
} |
818
|
|
|
|
819
|
|
|
$this->order = "ORDER BY {$order_by} {$order}\n"; |
820
|
|
|
} |
821
|
|
|
|
822
|
|
|
/** |
823
|
|
|
* Prepare the date query for a column. |
824
|
|
|
* |
825
|
|
|
* @since 2.1.0 |
826
|
|
|
* |
827
|
|
|
* @param string $column The name of the column. |
828
|
|
|
*/ |
829
|
|
|
protected function prepare_date_where( $column ) { |
830
|
|
|
|
831
|
|
|
if ( |
832
|
|
|
empty( $this->args[ "{$column}_query" ] ) |
833
|
|
|
|| ! is_array( $this->args[ "{$column}_query" ] ) |
834
|
|
|
) { |
835
|
|
|
return; |
836
|
|
|
} |
837
|
|
|
|
838
|
|
|
add_filter( 'date_query_valid_columns', array( $this, 'date_query_valid_columns_filter' ) ); |
839
|
|
|
|
840
|
|
|
$date_query = new WP_Date_Query( $this->args[ "{$column}_query" ], $column ); |
841
|
|
|
$date_query = $date_query->get_sql(); |
842
|
|
|
|
843
|
|
|
if ( ! empty( $date_query ) ) { |
844
|
|
|
$this->wheres[] = ltrim( $date_query, ' AND' ); |
845
|
|
|
} |
846
|
|
|
|
847
|
|
|
remove_filter( 'date_query_valid_columns', array( $this, 'date_query_valid_columns_filter' ) ); |
848
|
|
|
} |
849
|
|
|
|
850
|
|
|
/** |
851
|
|
|
* Prepare the meta query. |
852
|
|
|
* |
853
|
|
|
* @since 2.1.0 |
854
|
|
|
*/ |
855
|
|
|
protected function prepare_meta_where() { |
856
|
|
|
|
857
|
|
|
if ( empty( $this->meta_type ) ) { |
858
|
|
|
return; |
859
|
|
|
} |
860
|
|
|
|
861
|
|
|
$meta_args = array_intersect_key( |
862
|
|
|
$this->args |
863
|
|
|
, array( |
864
|
|
|
'meta_key' => '', |
865
|
|
|
'meta_value' => '', |
866
|
|
|
'meta_compare' => '', |
867
|
|
|
'meta_type' => '', |
868
|
|
|
'meta_query' => '', |
869
|
|
|
) |
870
|
|
|
); |
871
|
|
|
|
872
|
|
|
if ( empty( $meta_args ) ) { |
873
|
|
|
return; |
874
|
|
|
} |
875
|
|
|
|
876
|
|
|
$this->meta_query = new WP_Meta_Query(); |
877
|
|
|
$this->meta_query->parse_query_vars( $meta_args ); |
878
|
|
|
|
879
|
|
|
$meta_query = $this->meta_query->get_sql( |
880
|
|
|
$this->meta_type |
881
|
|
|
, $this->table_name |
882
|
|
|
, 'id' |
883
|
|
|
, $this |
884
|
|
|
); |
885
|
|
|
|
886
|
|
|
if ( ! empty( $meta_query['where'] ) ) { |
887
|
|
|
$this->wheres[] = ltrim( $meta_query['where'], ' AND' ); |
888
|
|
|
} |
889
|
|
|
|
890
|
|
|
$this->meta_join = $meta_query['join'] . "\n"; |
891
|
|
|
} |
892
|
|
|
} |
893
|
|
|
|
894
|
|
|
// EOF |
895
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.