GetPaid_Customers_Query   F
last analyzed

Complexity

Total Complexity 70

Size/Duplication

Total Lines 497
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 161
dl 0
loc 497
rs 2.8
c 1
b 0
f 0
wmc 70

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 2
A parse_orderby() 0 14 5
A get_results() 0 2 1
A get_total() 0 2 1
A prepare_query_fields() 0 22 6
A set() 0 2 1
B prepare_query() 0 39 8
A get() 0 6 2
A fill_query_vars() 0 24 4
B query() 0 30 9
A set_cache() 0 7 2
A get_search_sql() 0 16 4
A parse_order() 0 9 4
B prepare_query_order() 0 40 7
D prepare_query_where() 0 49 14

How to fix   Complexity   

Complex Class

Complex classes like GetPaid_Customers_Query often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use GetPaid_Customers_Query, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * GetPaid_Customers_Query class
4
 *
5
 * Contains core class used to query for customers.
6
 *
7
 * @since 1.0.19
8
 */
9
10
/**
11
 * Main class used for querying customers.
12
 *
13
 * @since 1.0.19
14
 *
15
 * @see GetPaid_Subscriptions_Query::prepare_query() for information on accepted arguments.
16
 */
17
class GetPaid_Customers_Query {
18
19
	/**
20
	 * Query vars, after parsing
21
	 *
22
	 * @since 1.0.19
23
	 * @var array
24
	 */
25
	public $query_vars = array();
26
27
	/**
28
	 * List of found customers.
29
	 *
30
	 * @since 1.0.19
31
	 * @var array
32
	 */
33
	private $results;
34
35
	/**
36
	 * Total number of found customers for the current query
37
	 *
38
	 * @since 1.0.19
39
	 * @var int
40
	 */
41
	private $total_customers = 0;
42
43
	/**
44
	 * The SQL query used to fetch matching customers.
45
	 *
46
	 * @since 1.0.19
47
	 * @var string
48
	 */
49
	public $request;
50
51
	// SQL clauses
52
53
	/**
54
	 * Contains the 'FIELDS' sql clause
55
	 *
56
	 * @since 1.0.19
57
	 * @var string
58
	 */
59
	public $query_fields;
60
61
	/**
62
	 * Contains the 'FROM' sql clause
63
	 *
64
	 * @since 1.0.19
65
	 * @var string
66
	 */
67
	public $query_from;
68
69
	/**
70
	 * Contains the 'WHERE' sql clause
71
	 *
72
	 * @since 1.0.19
73
	 * @var string
74
	 */
75
	public $query_where;
76
77
	/**
78
	 * Contains the 'ORDER BY' sql clause
79
	 *
80
	 * @since 1.0.19
81
	 * @var string
82
	 */
83
	public $query_orderby;
84
85
	/**
86
	 * Contains the 'LIMIT' sql clause
87
	 *
88
	 * @since 1.0.19
89
	 * @var string
90
	 */
91
	public $query_limit;
92
93
	/**
94
	 * Class constructor.
95
	 *
96
	 * @since 1.0.19
97
	 *
98
	 * @param null|string|array $query Optional. The query variables.
99
	 */
100
	public function __construct( $query = null ) {
101
		if ( ! is_null( $query ) ) {
102
			$this->prepare_query( $query );
103
			$this->query();
104
		}
105
	}
106
107
	/**
108
	 * Fills in missing query variables with default values.
109
	 *
110
	 * @since 1.0.19
111
	 *
112
	 * @param  string|array $args Query vars, as passed to `GetPaid_Subscriptions_Query`.
113
	 * @return array Complete query variables with undefined ones filled in with defaults.
114
	 */
115
	public static function fill_query_vars( $args ) {
116
		$defaults = array(
117
			'include'     => array(),
118
			'exclude'     => array(),
119
			'orderby'     => 'id',
120
			'order'       => 'DESC',
121
			'offset'      => '',
122
			'number'      => 10,
123
			'paged'       => 1,
124
			'count_total' => true,
125
			'fields'      => 'all',
126
			's'           => '',
127
		);
128
129
		foreach ( GetPaid_Customer_Data_Store::get_database_fields() as $field => $type ) {
130
			$defaults[ $field ] = 'any';
131
132
			if ( '%f' === $type || '%d' === $type ) {
133
				$defaults[ $field . '_min' ] = '';
134
				$defaults[ $field . '_max' ] = '';
135
			}
136
		}
137
138
		return wp_parse_args( $args, $defaults );
0 ignored issues
show
Security Variable Injection introduced by
$args can contain request data and is used in variable name context(s) leading to a potential security vulnerability.

2 paths for user data to reach this point

  1. Path: Read from $_GET, and Data is passed through rawurlencode_deep(), and Data is passed through wpinv_clean(), and wpinv_clean(rawurlencode_deep($_GET[$field])) is assigned to $query in includes/admin/class-wpinv-customers-table.php on line 239
  1. Read from $_GET, and Data is passed through rawurlencode_deep(), and Data is passed through wpinv_clean(), and wpinv_clean(rawurlencode_deep($_GET[$field])) is assigned to $query
    in includes/admin/class-wpinv-customers-table.php on line 239
  2. getpaid_get_customers() is called
    in includes/admin/class-wpinv-customers-table.php on line 256
  3. Enters via parameter $args
    in includes/user-functions.php on line 20
  4. GetPaid_Customers_Query::__construct() is called
    in includes/user-functions.php on line 33
  5. Enters via parameter $query
    in includes/class-getpaid-customers-query.php on line 100
  6. GetPaid_Customers_Query::prepare_query() is called
    in includes/class-getpaid-customers-query.php on line 102
  7. Enters via parameter $query
    in includes/class-getpaid-customers-query.php on line 148
  8. GetPaid_Customers_Query::fill_query_vars() is called
    in includes/class-getpaid-customers-query.php on line 153
  9. Enters via parameter $args
    in includes/class-getpaid-customers-query.php on line 115
  2. Path: Read from $_GET, and Data is passed through rawurlencode_deep(), and Data is passed through wpinv_clean(), and wpinv_clean(rawurlencode_deep($_GET[$field])) is assigned to $query in includes/admin/class-wpinv-customers-table.php on line 232
  1. Read from $_GET, and Data is passed through rawurlencode_deep(), and Data is passed through wpinv_clean(), and wpinv_clean(rawurlencode_deep($_GET[$field])) is assigned to $query
    in includes/admin/class-wpinv-customers-table.php on line 232
  2. getpaid_get_customers() is called
    in includes/admin/class-wpinv-customers-table.php on line 256
  3. Enters via parameter $args
    in includes/user-functions.php on line 20
  4. GetPaid_Customers_Query::__construct() is called
    in includes/user-functions.php on line 33
  5. Enters via parameter $query
    in includes/class-getpaid-customers-query.php on line 100
  6. GetPaid_Customers_Query::prepare_query() is called
    in includes/class-getpaid-customers-query.php on line 102
  7. Enters via parameter $query
    in includes/class-getpaid-customers-query.php on line 148
  8. GetPaid_Customers_Query::fill_query_vars() is called
    in includes/class-getpaid-customers-query.php on line 153
  9. Enters via parameter $args
    in includes/class-getpaid-customers-query.php on line 115

Used in variable context

  1. wp_parse_args() is called
    in includes/class-getpaid-customers-query.php on line 137
  2. Enters via parameter $args
    in wordpress/wp-includes/functions.php on line 4821
  3. wp_parse_str() is called
    in wordpress/wp-includes/functions.php on line 4827
  4. Enters via parameter $input_string
    in wordpress/wp-includes/formatting.php on line 5148
  5. parse_str() is called
    in wordpress/wp-includes/formatting.php on line 5149

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
139
	}
140
141
	/**
142
	 * Prepare the query variables.
143
	 *
144
	 * @since 1.0.19
145
	 *
146
	 * @see self::fill_query_vars() For allowede args and their defaults.
147
	 */
148
	public function prepare_query( $query = array() ) {
149
		global $wpdb;
150
151
		if ( empty( $this->query_vars ) || ! empty( $query ) ) {
152
			$this->query_limit = null;
153
			$this->query_vars  = $this->fill_query_vars( $query );
154
		}
155
156
		if ( ! empty( $this->query_vars['fields'] ) && 'all' !== $this->query_vars['fields'] ) {
157
			$this->query_vars['fields'] = wpinv_parse_list( $this->query_vars['fields'] );
158
		}
159
160
		do_action( 'getpaid_pre_get_customers', array( &$this ) );
161
162
		// Ensure that query vars are filled after 'getpaid_pre_get_customers'.
163
		$qv                = & $this->query_vars;
164
		$qv                = $this->fill_query_vars( $qv );
165
		$table             = $wpdb->prefix . 'getpaid_customers';
166
		$this->query_from  = "FROM $table";
167
168
		// Prepare query fields.
169
		$this->prepare_query_fields( $qv, $table );
170
171
		// Prepare query where.
172
		$this->prepare_query_where( $qv, $table );
173
174
		// Prepare query order.
175
		$this->prepare_query_order( $qv, $table );
176
177
		// limit
178
		if ( isset( $qv['number'] ) && $qv['number'] > 0 ) {
179
			if ( $qv['offset'] ) {
180
				$this->query_limit = $wpdb->prepare( 'LIMIT %d, %d', $qv['offset'], $qv['number'] );
181
			} else {
182
				$this->query_limit = $wpdb->prepare( 'LIMIT %d, %d', $qv['number'] * ( $qv['paged'] - 1 ), $qv['number'] );
183
			}
184
		}
185
186
		do_action_ref_array( 'getpaid_after_customers_query', array( &$this ) );
187
	}
188
189
	/**
190
	 * Prepares the query fields.
191
	 *
192
	 * @since 1.0.19
193
	 *
194
	 * @param array $qv Query vars.
195
	 * @param string $table Table name.
196
	 */
197
	protected function prepare_query_fields( &$qv, $table ) {
198
199
		if ( is_array( $qv['fields'] ) ) {
200
			$qv['fields']   = array_unique( $qv['fields'] );
201
			$allowed_fields = array_keys( GetPaid_Customer_Data_Store::get_database_fields() );
202
203
			$query_fields = array();
204
			foreach ( $qv['fields'] as $field ) {
205
				if ( ! in_array( $field, $allowed_fields ) ) {
206
					continue;
207
				}
208
209
				$field          = sanitize_key( $field );
210
				$query_fields[] = "$table.`$field`";
211
			}
212
			$this->query_fields = implode( ',', $query_fields );
213
		} else {
214
			$this->query_fields = "$table.*";
215
		}
216
217
		if ( isset( $qv['count_total'] ) && $qv['count_total'] ) {
218
			$this->query_fields = 'SQL_CALC_FOUND_ROWS ' . $this->query_fields;
219
		}
220
221
	}
222
223
	/**
224
	 * Prepares the query where.
225
	 *
226
	 * @since 1.0.19
227
	 *
228
	 * @param array $qv Query vars.
229
	 * @param string $table Table name.
230
	 */
231
	protected function prepare_query_where( &$qv, $table ) {
232
		global $wpdb;
233
		$this->query_where = 'WHERE 1=1';
234
235
		// Fields.
236
		foreach ( GetPaid_Customer_Data_Store::get_database_fields() as $field => $type ) {
237
			if ( 'any' !== $qv[ $field ] ) {
238
239
				// In.
240
				if ( is_array( $qv[ $field ] ) ) {
241
					$in                 = join( ',', array_fill( 0, count( $qv[ $field ] ), $type ) );
242
					$this->query_where .= $wpdb->prepare( " AND $table.`status` IN ( $in )", $qv[ $field ] );
243
				} elseif ( ! empty( $qv[ $field ] ) ) {
244
					$this->query_where .= $wpdb->prepare( " AND $table.`$field` = $type", $qv[ $field ] );
245
				}
246
			}
247
248
			// Min/Max.
249
			if ( '%f' === $type || '%d' === $type ) {
250
251
				// Min.
252
				if ( is_numeric( $qv[ $field . '_min' ] ) ) {
253
					$this->query_where .= $wpdb->prepare( " AND $table.`$field` >= $type", $qv[ $field . '_min' ] );
254
				}
255
256
				// Max.
257
				if ( is_numeric( $qv[ $field . '_max' ] ) ) {
258
					$this->query_where .= $wpdb->prepare( " AND $table.`$field` <= $type", $qv[ $field . '_max' ] );
259
				}
260
			}
261
		}
262
263
		if ( ! empty( $qv['include'] ) ) {
264
			$include            = implode( ',', wp_parse_id_list( $qv['include'] ) );
265
			$this->query_where .= " AND $table.`id` IN ($include)";
266
		} elseif ( ! empty( $qv['exclude'] ) ) {
267
			$exclude            = implode( ',', wp_parse_id_list( $qv['exclude'] ) );
268
			$this->query_where .= " AND $table.`id` NOT IN ($exclude)";
269
		}
270
271
		// Date queries are allowed for the customer creation date.
272
		if ( ! empty( $qv['date_created_query'] ) && is_array( $qv['date_created_query'] ) ) {
273
			$date_created_query = new WP_Date_Query( $qv['date_created_query'], "$table.date_created" );
274
			$this->query_where .= $date_created_query->get_sql();
275
		}
276
277
		// Search.
278
		if ( ! empty( $qv['s'] ) ) {
279
			$this->query_where .= $this->get_search_sql( $qv['s'] );
280
		}
281
	}
282
283
	/**
284
	 * Used internally to generate an SQL string for searching across multiple columns
285
	 *
286
	 * @since 1.2.7
287
	 *
288
	 * @global wpdb $wpdb WordPress database abstraction object.
289
	 *
290
	 * @param string $string The string to search for.
291
	 * @return string
292
	 */
293
	protected function get_search_sql( $string ) {
294
		global $wpdb;
295
296
		$searches = array();
297
		$string   = trim( $string, '%' );
298
		$like     = '%' . $wpdb->esc_like( $string ) . '%';
299
300
		foreach ( array_keys( GetPaid_Customer_Data_Store::get_database_fields() ) as $col ) {
301
			if ( 'id' === $col || 'user_id' === $col ) {
302
				$searches[] = $wpdb->prepare( "$col = %s", $string );  // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
303
			} else {
304
				$searches[] = $wpdb->prepare( "$col LIKE %s", $like );  // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
305
			}
306
		}
307
308
		return ' AND (' . implode( ' OR ', $searches ) . ')';
309
	}
310
311
	/**
312
	 * Prepares the query order.
313
	 *
314
	 * @since 1.0.19
315
	 *
316
	 * @param array $qv Query vars.
317
	 * @param string $table Table name.
318
	 */
319
	protected function prepare_query_order( &$qv, $table ) {
320
321
		// sorting.
322
		$qv['order'] = isset( $qv['order'] ) ? strtoupper( $qv['order'] ) : '';
323
		$order       = $this->parse_order( $qv['order'] );
324
325
		// Default order is by 'id' (latest customers).
326
		if ( empty( $qv['orderby'] ) ) {
327
			$qv['orderby'] = array( 'id' );
328
		}
329
330
		// 'orderby' values may be an array, comma- or space-separated list.
331
		$ordersby      = array_filter( wpinv_parse_list( $qv['orderby'] ) );
332
333
		$orderby_array = array();
334
		foreach ( $ordersby as $_key => $_value ) {
335
336
			if ( is_int( $_key ) ) {
337
				// Integer key means this is a flat array of 'orderby' fields.
338
				$_orderby = $_value;
339
				$_order   = $order;
340
			} else {
341
				// Non-integer key means that the key is the field and the value is ASC/DESC.
342
				$_orderby = $_key;
343
				$_order   = $_value;
344
			}
345
346
			$parsed = $this->parse_orderby( $_orderby, $table );
347
348
			if ( $parsed ) {
349
				$orderby_array[] = $parsed . ' ' . $this->parse_order( $_order );
350
			}
351
		}
352
353
		// If no valid clauses were found, order by id.
354
		if ( empty( $orderby_array ) ) {
355
			$orderby_array[] = "id $order";
356
		}
357
358
		$this->query_orderby = 'ORDER BY ' . implode( ', ', $orderby_array );
359
360
	}
361
362
	/**
363
	 * Execute the query, with the current variables.
364
	 *
365
	 * @since 1.0.19
366
	 *
367
	 * @global wpdb $wpdb WordPress database abstraction object.
368
	 */
369
	public function query() {
370
		global $wpdb;
371
372
		$qv =& $this->query_vars;
373
374
		// Return a non-null value to bypass the default GetPaid customers query and remember to set the
375
		// total_customers property.
376
		$this->results = apply_filters_ref_array( 'getpaid_customers_pre_query', array( null, &$this ) );
377
378
		if ( null === $this->results ) {
379
			$this->request = "SELECT $this->query_fields $this->query_from $this->query_where $this->query_orderby $this->query_limit";
380
381
			if ( ( is_array( $qv['fields'] ) && 1 !== count( $qv['fields'] ) ) || 'all' === $qv['fields'] ) {
382
				$this->results = $wpdb->get_results( $this->request );
383
			} else {
384
				$this->results = $wpdb->get_col( $this->request );
385
			}
386
387
			if ( isset( $qv['count_total'] ) && $qv['count_total'] ) {
388
				$found_customers_query = apply_filters( 'getpaid_found_customers_query', 'SELECT FOUND_ROWS()', $this );
389
				$this->total_customers = (int) $wpdb->get_var( $found_customers_query );
390
			}
391
		}
392
393
		if ( 'all' === $qv['fields'] ) {
394
			foreach ( $this->results as $key => $customer ) {
395
				$this->set_cache( $customer->id, $customer, 'getpaid_customers' );
396
				$this->set_cache( $customer->user_id, $customer->id, 'getpaid_customer_ids_by_user_id' );
397
				$this->set_cache( $customer->email, $customer->id, 'getpaid_customer_ids_by_email' );
398
				$this->results[ $key ] = new GetPaid_Customer( $customer );
399
			}
400
		}
401
402
	}
403
404
	/**
405
	 * Set cache
406
	 *
407
	 * @param string  $id
408
	 * @param mixed   $data
409
	 * @param string  $group
410
	 * @param integer $expire
411
	 * @return boolean
412
	 */
413
	public function set_cache( $key, $data, $group = '', $expire = 0 ) {
414
415
		if ( empty( $key ) ) {
416
			return false;
417
		}
418
419
		wp_cache_set( $key, $data, $group, $expire );
420
	}
421
422
	/**
423
	 * Retrieve query variable.
424
	 *
425
	 * @since 1.0.19
426
	 *
427
	 * @param string $query_var Query variable key.
428
	 * @return mixed
429
	 */
430
	public function get( $query_var ) {
431
		if ( isset( $this->query_vars[ $query_var ] ) ) {
432
			return $this->query_vars[ $query_var ];
433
		}
434
435
		return null;
436
	}
437
438
	/**
439
	 * Set query variable.
440
	 *
441
	 * @since 1.0.19
442
	 *
443
	 * @param string $query_var Query variable key.
444
	 * @param mixed $value Query variable value.
445
	 */
446
	public function set( $query_var, $value ) {
447
		$this->query_vars[ $query_var ] = $value;
448
	}
449
450
	/**
451
	 * Return the list of customers.
452
	 *
453
	 * @since 1.0.19
454
	 *
455
	 * @return GetPaid_Customer[]|array Found customers.
456
	 */
457
	public function get_results() {
458
		return $this->results;
459
	}
460
461
	/**
462
	 * Return the total number of customers for the current query.
463
	 *
464
	 * @since 1.0.19
465
	 *
466
	 * @return int Number of total customers.
467
	 */
468
	public function get_total() {
469
		return $this->total_customers;
470
	}
471
472
	/**
473
	 * Parse and sanitize 'orderby' keys passed to the customers query.
474
	 *
475
	 * @since 1.0.19
476
	 *
477
	 * @param string $orderby Alias for the field to order by.
478
	 *  @param string $table The current table.
479
	 * @return string Value to use in the ORDER clause, if `$orderby` is valid.
480
	 */
481
	protected function parse_orderby( $orderby, $table ) {
482
483
		$_orderby = '';
484
		if ( in_array( $orderby, array_keys( GetPaid_Customer_Data_Store::get_database_fields() ), true ) ) {
485
			$_orderby = "$table.`$orderby`";
486
		} elseif ( 'id' === strtolower( $orderby ) ) {
487
			$_orderby = "$table.id";
488
		} elseif ( 'include' === $orderby && ! empty( $this->query_vars['include'] ) ) {
489
			$include     = wp_parse_id_list( $this->query_vars['include'] );
490
			$include_sql = implode( ',', $include );
491
			$_orderby    = "FIELD( $table.id, $include_sql )";
492
		}
493
494
		return $_orderby;
495
	}
496
497
	/**
498
	 * Parse an 'order' query variable and cast it to ASC or DESC as necessary.
499
	 *
500
	 * @since 1.0.19
501
	 *
502
	 * @param string $order The 'order' query variable.
503
	 * @return string The sanitized 'order' query variable.
504
	 */
505
	protected function parse_order( $order ) {
506
		if ( ! is_string( $order ) || empty( $order ) ) {
0 ignored issues
show
introduced by
The condition is_string($order) is always true.
Loading history...
507
			return 'DESC';
508
		}
509
510
		if ( 'ASC' === strtoupper( $order ) ) {
511
			return 'ASC';
512
		} else {
513
			return 'DESC';
514
		}
515
	}
516
517
}
518