Issues (850)

Security Analysis    4 potential vulnerabilities

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  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.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  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.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  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.
  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.
  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.
  Code Injection (1)
Code Injection enables an attacker to execute arbitrary code on the server.
  Variable Injection (2)
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
  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.
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  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.
  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.
  Cross-Site Scripting (1)
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.
  Header Injection
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.

includes/class-getpaid-subscriptions-query.php (1 issue)

Severity
1
<?php
2
/**
3
 * GetPaid_Subscriptions_Query class
4
 *
5
 * Contains core class used to query for subscriptions.
6
 *
7
 * @since 1.0.19
8
 */
9
10
/**
11
 * Main class used for querying subscriptions.
12
 *
13
 * @since 1.0.19
14
 *
15
 * @see GetPaid_Subscriptions_Query::prepare_query() for information on accepted arguments.
16
 */
17
class GetPaid_Subscriptions_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 subscriptions.
29
	 *
30
	 * @since 1.0.19
31
	 * @var array
32
	 */
33
	private $results;
34
35
	/**
36
	 * Total number of found subscriptions for the current query
37
	 *
38
	 * @since 1.0.19
39
	 * @var int
40
	 */
41
	private $total_subscriptions = 0;
42
43
	/**
44
	 * The SQL query used to fetch matching subscriptions.
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
			'status'          => 'all',
118
			'customer_in'     => array(),
119
			'customer_not_in' => array(),
120
			'product_in'      => array(),
121
			'product_not_in'  => array(),
122
			'include'         => array(),
123
			'exclude'         => array(),
124
			'orderby'         => 'id',
125
			'order'           => 'DESC',
126
			'offset'          => '',
127
			'number'          => 10,
128
			'paged'           => 1,
129
			'count_total'     => true,
130
			'fields'          => 'all',
131
		);
132
133
		return wp_parse_args( $args, $defaults );
134
	}
135
136
	/**
137
	 * Prepare the query variables.
138
	 *
139
	 * @since 1.0.19
140
	 *
141
	 * @global wpdb $wpdb WordPress database abstraction object.
142
	 *
143
	 * @param string|array $query {
144
	 *     Optional. Array or string of Query parameters.
145
	 *
146
	 *     @type string|array $status              The subscription status to filter by. Can either be a single status or an array of statuses.
147
	 *                                             Default is all.
148
	 *     @type int[]        $customer_in         An array of customer ids to filter by.
149
	 *     @type int[]        $customer_not_in     An array of customer ids whose subscriptions should be excluded.
150
	 *     @type int[]        $invoice_in          An array of invoice ids to filter by.
151
	 *     @type int[]        $invoice_not_in      An array of invoice ids whose subscriptions should be excluded.
152
	 *     @type int[]        $product_in          An array of product ids to filter by.
153
	 *     @type int[]        $product_not_in      An array of product ids whose subscriptions should be excluded.
154
	 *     @type array        $date_created_query  A WP_Date_Query compatible array use to filter subscriptions by their date of creation.
155
	 *     @type array        $date_expires_query  A WP_Date_Query compatible array use to filter subscriptions by their expiration date.
156
	 *     @type array        $include             An array of subscription IDs to include. Default empty array.
157
	 *     @type array        $exclude             An array of subscription IDs to exclude. Default empty array.
158
	 *     @type string|array $orderby             Field(s) to sort the retrieved subscription by. May be a single value,
159
	 *                                             an array of values, or a multi-dimensional array with fields as
160
	 *                                             keys and orders ('ASC' or 'DESC') as values. Accepted values are
161
	 *                                             'id', 'customer_id', 'frequency', 'period', 'initial_amount,
162
	 *                                             'recurring_amount', 'bill_times', 'parent_payment_id', 'created', 'expiration'
163
	 *                                             'transaction_id', 'product_id', 'trial_period', 'include', 'status', 'profile_id'. Default array( 'id' ).
164
	 *     @type string       $order               Designates ascending or descending order of subscriptions. Order values
165
	 *                                             passed as part of an `$orderby` array take precedence over this
166
	 *                                             parameter. Accepts 'ASC', 'DESC'. Default 'DESC'.
167
	 *     @type int          $offset              Number of subscriptions to offset in retrieved results. Can be used in
168
	 *                                             conjunction with pagination. Default 0.
169
	 *     @type int          $number              Number of subscriptions to limit the query for. Can be used in
170
	 *                                             conjunction with pagination. Value -1 (all) is supported, but
171
	 *                                             should be used with caution on larger sites.
172
	 *                                             Default 10.
173
	 *     @type int          $paged               When used with number, defines the page of results to return.
174
	 *                                             Default 1.
175
	 *     @type bool         $count_total         Whether to count the total number of subscriptions found. If pagination
176
	 *                                             is not needed, setting this to false can improve performance.
177
	 *                                             Default true.
178
	 *     @type string|array $fields              Which fields to return. Single or all fields (string), or array
179
	 *                                             of fields. Accepts 'id', 'customer_id', 'frequency', 'period', 'initial_amount,
180
	 *                                             'recurring_amount', 'bill_times', 'parent_payment_id', 'created', 'expiration'
181
	 *                                             'transaction_id', 'product_id', 'trial_period', 'status', 'profile_id'.
182
	 *                                             Use 'all' for all fields. Default 'all'.
183
	 * }
184
	 */
185
	public function prepare_query( $query = array() ) {
186
		global $wpdb;
187
188
		if ( empty( $this->query_vars ) || ! empty( $query ) ) {
189
			$this->query_limit = null;
190
			$this->query_vars  = $this->fill_query_vars( $query );
191
		}
192
193
		if ( ! empty( $this->query_vars['fields'] ) && 'all' !== $this->query_vars['fields'] ) {
194
			$this->query_vars['fields'] = wpinv_parse_list( $this->query_vars['fields'] );
195
		}
196
197
		do_action( 'getpaid_pre_get_subscriptions', array( &$this ) );
198
199
		// Ensure that query vars are filled after 'getpaid_pre_get_subscriptions'.
200
		$qv                =& $this->query_vars;
201
		$qv                = $this->fill_query_vars( $qv );
202
		$table             = $wpdb->prefix . 'wpinv_subscriptions';
203
		$this->query_from  = "FROM $table";
204
205
		// Prepare query fields.
206
		$this->prepare_query_fields( $qv, $table );
207
208
		// Prepare query where.
209
		$this->prepare_query_where( $qv, $table );
210
211
		// Prepare query order.
212
		$this->prepare_query_order( $qv, $table );
213
214
		// limit
215
		if ( isset( $qv['number'] ) && $qv['number'] > 0 ) {
216
			if ( $qv['offset'] ) {
217
				$this->query_limit = $wpdb->prepare( 'LIMIT %d, %d', $qv['offset'], $qv['number'] );
218
			} else {
219
				$this->query_limit = $wpdb->prepare( 'LIMIT %d, %d', $qv['number'] * ( $qv['paged'] - 1 ), $qv['number'] );
220
			}
221
		}
222
223
		do_action_ref_array( 'getpaid_after_subscriptions_query', array( &$this ) );
224
	}
225
226
	/**
227
	 * Prepares the query fields.
228
	 *
229
	 * @since 1.0.19
230
	 *
231
	 * @param array $qv Query vars.
232
	 * @param string $table Table name.
233
	 */
234
	protected function prepare_query_fields( &$qv, $table ) {
235
236
		if ( is_array( $qv['fields'] ) ) {
237
			$qv['fields'] = array_unique( $qv['fields'] );
238
239
			$query_fields = array();
240
			foreach ( $qv['fields'] as $field ) {
241
				$field          = sanitize_key( $field );
242
				$query_fields[] = "$table.`$field`";
243
			}
244
			$this->query_fields = implode( ',', $query_fields );
245
		} else {
246
			$this->query_fields = "$table.*";
247
		}
248
249
		if ( isset( $qv['count_total'] ) && $qv['count_total'] ) {
250
			$this->query_fields = 'SQL_CALC_FOUND_ROWS ' . $this->query_fields;
251
		}
252
253
	}
254
255
	/**
256
	 * Prepares the query where.
257
	 *
258
	 * @since 1.0.19
259
	 *
260
	 * @param array $qv Query vars.
261
	 * @param string $table Table name.
262
	 */
263
	protected function prepare_query_where( &$qv, $table ) {
264
		global $wpdb;
265
		$this->query_where = 'WHERE 1=1';
266
267
		// Status.
268
		if ( 'all' !== $qv['status'] ) {
269
			$statuses           = wpinv_clean( wpinv_parse_list( $qv['status'] ) );
270
			$prepared_statuses  = join( ',', array_fill( 0, count( $statuses ), '%s' ) );
271
			$this->query_where .= $wpdb->prepare( " AND $table.`status` IN ( $prepared_statuses )", $statuses );
272
		}
273
274
		if ( ! empty( $qv['customer_in'] ) ) {
275
			$customer_in        = implode( ',', wp_parse_id_list( $qv['customer_in'] ) );
276
			$this->query_where .= " AND $table.`customer_id` IN ($customer_in)";
277
		} elseif ( ! empty( $qv['customer_not_in'] ) ) {
278
			$customer_not_in    = implode( ',', wp_parse_id_list( $qv['customer_not_in'] ) );
279
			$this->query_where .= " AND $table.`customer_id` NOT IN ($customer_not_in)";
280
		}
281
282
		if ( ! empty( $qv['product_in'] ) ) {
283
			$product_in         = implode( ',', wp_parse_id_list( $qv['product_in'] ) );
284
			$this->query_where .= " AND $table.`product_id` IN ($product_in)";
285
		} elseif ( ! empty( $qv['product_not_in'] ) ) {
286
			$product_not_in     = implode( ',', wp_parse_id_list( $qv['product_not_in'] ) );
287
			$this->query_where .= " AND $table.`product_id` NOT IN ($product_not_in)";
288
		}
289
290
		if ( ! empty( $qv['invoice_in'] ) ) {
291
			$invoice_in         = implode( ',', wp_parse_id_list( $qv['invoice_in'] ) );
292
			$this->query_where .= " AND $table.`parent_payment_id` IN ($invoice_in)";
293
		} elseif ( ! empty( $qv['invoice_not_in'] ) ) {
294
			$invoice_not_in     = implode( ',', wp_parse_id_list( $qv['invoice_not_in'] ) );
295
			$this->query_where .= " AND $table.`parent_payment_id` NOT IN ($invoice_not_in)";
296
		}
297
298
		if ( ! empty( $qv['include'] ) ) {
299
			$include            = implode( ',', wp_parse_id_list( $qv['include'] ) );
300
			$this->query_where .= " AND $table.`id` IN ($include)";
301
		} elseif ( ! empty( $qv['exclude'] ) ) {
302
			$exclude            = implode( ',', wp_parse_id_list( $qv['exclude'] ) );
303
			$this->query_where .= " AND $table.`id` NOT IN ($exclude)";
304
		}
305
306
		// Date queries are allowed for the subscription creation date.
307
		if ( ! empty( $qv['date_created_query'] ) && is_array( $qv['date_created_query'] ) ) {
308
			$date_created_query = new WP_Date_Query( $qv['date_created_query'], "$table.created" );
309
			$this->query_where .= $date_created_query->get_sql();
310
		}
311
312
		// Date queries are also allowed for the subscription expiration date.
313
		if ( ! empty( $qv['date_expires_query'] ) && is_array( $qv['date_expires_query'] ) ) {
314
			$date_expires_query = new WP_Date_Query( $qv['date_expires_query'], "$table.expiration" );
315
			$this->query_where .= $date_expires_query->get_sql();
316
		}
317
318
	}
319
320
	/**
321
	 * Prepares the query order.
322
	 *
323
	 * @since 1.0.19
324
	 *
325
	 * @param array $qv Query vars.
326
	 * @param string $table Table name.
327
	 */
328
	protected function prepare_query_order( &$qv, $table ) {
329
330
		// sorting.
331
		$qv['order'] = isset( $qv['order'] ) ? strtoupper( $qv['order'] ) : '';
332
		$order       = $this->parse_order( $qv['order'] );
333
334
		// Default order is by 'id' (latest subscriptions).
335
		if ( empty( $qv['orderby'] ) ) {
336
			$qv['orderby'] = array( 'id' );
337
		}
338
339
		// 'orderby' values may be an array, comma- or space-separated list.
340
		$ordersby      = array_filter( wpinv_parse_list( $qv['orderby'] ) );
341
342
		$orderby_array = array();
343
		foreach ( $ordersby as $_key => $_value ) {
344
345
			if ( is_int( $_key ) ) {
346
				// Integer key means this is a flat array of 'orderby' fields.
347
				$_orderby = $_value;
348
				$_order   = $order;
349
			} else {
350
				// Non-integer key means that the key is the field and the value is ASC/DESC.
351
				$_orderby = $_key;
352
				$_order   = $_value;
353
			}
354
355
			$parsed = $this->parse_orderby( $_orderby, $table );
356
357
			if ( $parsed ) {
358
				$orderby_array[] = $parsed . ' ' . $this->parse_order( $_order );
359
			}
360
}
361
362
		// If no valid clauses were found, order by id.
363
		if ( empty( $orderby_array ) ) {
364
			$orderby_array[] = "id $order";
365
		}
366
367
		$this->query_orderby = 'ORDER BY ' . implode( ', ', $orderby_array );
368
369
	}
370
371
	/**
372
	 * Execute the query, with the current variables.
373
	 *
374
	 * @since 1.0.19
375
	 *
376
	 * @global wpdb $wpdb WordPress database abstraction object.
377
	 */
378
	public function query() {
379
		global $wpdb;
380
381
		$qv =& $this->query_vars;
382
383
		// Return a non-null value to bypass the default GetPaid subscriptions query and remember to set the
384
		// total_subscriptions property.
385
		$this->results = apply_filters_ref_array( 'getpaid_subscriptions_pre_query', array( null, &$this ) );
386
387
		if ( null === $this->results ) {
388
			$this->request = "SELECT $this->query_fields $this->query_from $this->query_where $this->query_orderby $this->query_limit";
389
390
			if ( ( is_array( $qv['fields'] ) && 1 != count( $qv['fields'] ) ) || 'all' == $qv['fields'] ) {
391
				$this->results = $wpdb->get_results( $this->request );
392
			} else {
393
				$this->results = $wpdb->get_col( $this->request );
394
			}
395
396
			if ( isset( $qv['count_total'] ) && $qv['count_total'] ) {
397
				$found_subscriptions_query = apply_filters( 'getpaid_found_subscriptions_query', 'SELECT FOUND_ROWS()', $this );
398
				$this->total_subscriptions   = (int) $wpdb->get_var( $found_subscriptions_query );
399
			}
400
		}
401
402
		if ( 'all' == $qv['fields'] ) {
403
			foreach ( $this->results as $key => $subscription ) {
404
				$this->set_cache( $subscription->id, $subscription, 'getpaid_subscriptions' );
405
				$this->set_cache( $subscription->profile_id, $subscription->id, 'getpaid_subscription_profile_ids_to_subscription_ids' );
406
				$this->set_cache( $subscription->transaction_id, $subscription->id, 'getpaid_subscription_transaction_ids_to_subscription_ids' );
407
				$this->set_cache( $subscription->transaction_id, $subscription->id, 'getpaid_subscription_transaction_ids_to_subscription_ids' );
408
				$this->results[ $key ] = new WPInv_Subscription( $subscription );
409
			}
410
		}
411
412
	}
413
414
	/**
415
	 * Set cache
416
	 *
417
	 * @param string  $id
418
	 * @param mixed   $data
419
	 * @param string  $group
420
	 * @param integer $expire
421
	 * @return boolean
422
	 */
423
	public function set_cache( $key, $data, $group = '', $expire = 0 ) {
424
425
		if ( empty( $key ) ) {
426
			return false;
427
		}
428
429
		wp_cache_set( $key, $data, $group, $expire );
430
	}
431
432
	/**
433
	 * Retrieve query variable.
434
	 *
435
	 * @since 1.0.19
436
	 *
437
	 * @param string $query_var Query variable key.
438
	 * @return mixed
439
	 */
440
	public function get( $query_var ) {
441
		if ( isset( $this->query_vars[ $query_var ] ) ) {
442
			return $this->query_vars[ $query_var ];
443
		}
444
445
		return null;
446
	}
447
448
	/**
449
	 * Set query variable.
450
	 *
451
	 * @since 1.0.19
452
	 *
453
	 * @param string $query_var Query variable key.
454
	 * @param mixed $value Query variable value.
455
	 */
456
	public function set( $query_var, $value ) {
457
		$this->query_vars[ $query_var ] = $value;
458
	}
459
460
	/**
461
	 * Return the list of subscriptions.
462
	 *
463
	 * @since 1.0.19
464
	 *
465
	 * @return WPInv_Subscription[]|array Found subscriptions.
466
	 */
467
	public function get_results() {
468
		return $this->results;
469
	}
470
471
	/**
472
	 * Return the total number of subscriptions for the current query.
473
	 *
474
	 * @since 1.0.19
475
	 *
476
	 * @return int Number of total subscriptions.
477
	 */
478
	public function get_total() {
479
		return $this->total_subscriptions;
480
	}
481
482
	/**
483
	 * Parse and sanitize 'orderby' keys passed to the subscriptions query.
484
	 *
485
	 * @since 1.0.19
486
	 *
487
	 * @param string $orderby Alias for the field to order by.
488
	 *  @param string $table The current table.
489
	 * @return string Value to use in the ORDER clause, if `$orderby` is valid.
490
	 */
491
	protected function parse_orderby( $orderby, $table ) {
492
493
		$_orderby = '';
494
		if ( in_array( $orderby, array( 'customer_id', 'frequency', 'period', 'initial_amount', 'recurring_amount', 'bill_times', 'transaction_id', 'parent_payment_id', 'product_id', 'created', 'expiration', 'trial_period', 'status', 'profile_id' ) ) ) {
495
			$_orderby = "$table.`$orderby`";
496
		} elseif ( 'id' === strtolower( $orderby ) ) {
497
			$_orderby = "$table.id";
498
		} elseif ( 'include' === $orderby && ! empty( $this->query_vars['include'] ) ) {
499
			$include     = wp_parse_id_list( $this->query_vars['include'] );
500
			$include_sql = implode( ',', $include );
501
			$_orderby    = "FIELD( $table.id, $include_sql )";
502
		}
503
504
		return $_orderby;
505
	}
506
507
	/**
508
	 * Parse an 'order' query variable and cast it to ASC or DESC as necessary.
509
	 *
510
	 * @since 1.0.19
511
	 *
512
	 * @param string $order The 'order' query variable.
513
	 * @return string The sanitized 'order' query variable.
514
	 */
515
	protected function parse_order( $order ) {
516
		if ( ! is_string( $order ) || empty( $order ) ) {
0 ignored issues
show
The condition is_string($order) is always true.
Loading history...
517
			return 'DESC';
518
		}
519
520
		if ( 'ASC' === strtoupper( $order ) ) {
521
			return 'ASC';
522
		} else {
523
			return 'DESC';
524
		}
525
	}
526
527
}
528