Issues (1282)

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.

includes/api/class-give-api.php (35 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
 * Give API
4
 *
5
 * A front-facing JSON/XML API that makes it possible to query donation data.
6
 *
7
 * @package     Give
8
 * @subpackage  Classes/API
9
 * @copyright   Copyright (c) 2016, GiveWP
10
 * @license     https://opensource.org/licenses/gpl-license GNU Public License
11
 * @since       1.1
12
 */
13
14
// Exit if accessed directly.
15
if ( ! defined( 'ABSPATH' ) ) {
16
	exit;
17
}
18
19
/**
20
 * Give_API Class
21
 *
22
 * Renders API returns as a JSON/XML array
23
 *
24
 * @since 1.1
25
 */
26
class Give_API {
27
28
	/**
29
	 * Latest API Version
30
	 */
31
	const VERSION = 1;
32
33
	/**
34
	 * Pretty Print?
35
	 *
36
	 * @var bool
37
	 * @access private
38
	 * @since  1.1
39
	 */
40
	private $pretty_print = false;
41
42
	/**
43
	 * Log API requests?
44
	 *
45
	 * @var bool
46
	 * @access public
47
	 * @since  1.1
48
	 */
49
	public $log_requests = true;
50
51
	/**
52
	 * Is this a valid request?
53
	 *
54
	 * @var bool
55
	 * @access private
56
	 * @since  1.1
57
	 */
58
	private $is_valid_request = false;
59
60
	/**
61
	 * User ID Performing the API Request
62
	 *
63
	 * @var int
64
	 * @access public
65
	 * @since  1.1
66
	 */
67
	public $user_id = 0;
68
69
	/**
70
	 * Instance of Give Stats class
71
	 *
72
	 * @var object
73
	 * @access private
74
	 * @since  1.1
75
	 */
76
	private $stats;
77
78
	/**
79
	 * Response data to return
80
	 *
81
	 * @var array
82
	 * @access private
83
	 * @since  1.1
84
	 */
85
	private $data = array();
86
87
	/**
88
	 * Whether or not to override api key validation.
89
	 *
90
	 * @var bool
91
	 * @access public
92
	 * @since  1.1
93
	 */
94
	public $override = true;
95
96
	/**
97
	 * Version of the API queried
98
	 *
99
	 * @var string
100
	 * @access public
101
	 * @since  1.1
102
	 */
103
	private $queried_version;
104
105
	/**
106
	 * All versions of the API
107
	 *
108
	 * @var array
109
	 * @access protected
110
	 * @since  1.1
111
	 */
112
	protected $versions = array();
113
114
	/**
115
	 * Queried endpoint
116
	 *
117
	 * @var string
118
	 * @access private
119
	 * @since  1.1
120
	 */
121
	private $endpoint;
122
123
	/**
124
	 * Endpoints routes
125
	 *
126
	 * @var object
127
	 * @access private
128
	 * @since  1.1
129
	 */
130
	private $routes;
131
132
	/**
133
	 * Setup the Give API
134
	 *
135
	 * @since  1.1
136
	 * @access public
137
	 */
138
	public function __construct() {
139
140
		$this->versions = array(
141
			'v1' => 'GIVE_API_V1',
142
		);
143
144
		foreach ( $this->get_versions() as $version => $class ) {
145
			require_once GIVE_PLUGIN_DIR . 'includes/api/class-give-api-' . $version . '.php';
146
		}
147
148
		add_action( 'init', array( $this, 'add_endpoint' ) );
149
		add_action( 'wp', array( $this, 'process_query' ), - 1 );
150
		add_filter( 'query_vars', array( $this, 'query_vars' ) );
151
		add_action( 'show_user_profile', array( $this, 'user_key_field' ) );
152
		add_action( 'edit_user_profile', array( $this, 'user_key_field' ) );
153
		add_action( 'personal_options_update', array( $this, 'generate_api_key' ) );
154
		add_action( 'edit_user_profile_update', array( $this, 'generate_api_key' ) );
155
		add_action( 'give_process_api_key', array( $this, 'process_api_key' ) );
156
157
		// Setup a backwards compatibility check for user API Keys
158
		add_filter( 'get_user_metadata', array( $this, 'api_key_backwards_compat' ), 10, 4 );
159
160
		// Determine if JSON_PRETTY_PRINT is available
161
		$this->pretty_print = defined( 'JSON_PRETTY_PRINT' ) ? JSON_PRETTY_PRINT : null;
0 ignored issues
show
Documentation Bug introduced by
It seems like defined('JSON_PRETTY_PRI...SON_PRETTY_PRINT : null can also be of type integer. However, the property $pretty_print is declared as type boolean. 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...
162
163
		// Allow API request logging to be turned off
164
		$this->log_requests = apply_filters( 'give_api_log_requests', $this->log_requests );
165
166
		// Setup Give_Payment_Stats instance
167
		$this->stats = new Give_Payment_Stats();
168
169
	}
170
171
	/**
172
	 * There are certain responsibility of this function:
173
	 *  1. handle backward compatibility for deprecated functions
174
	 *
175
	 * @since 2.0
176
	 *
177
	 * @param $name
178
	 * @param $arguments
179
	 *
180
	 * @return mixed
181
	 */
182
	public function __call( $name, $arguments ) {
183
		$deprecated_function_arr = array(
184
			'get_customers',
185
		);
186
187
		if ( in_array( $name, $deprecated_function_arr, true ) ) {
188
			switch ( $name ) {
189
				case 'get_customers':
190
					$args = ! empty( $arguments[0] ) ? $arguments[0] : array();
191
192
					return $this->get_donors( $args );
193
			}
194
		}
195
	}
196
197
	/**
198
	 * Registers a new rewrite endpoint for accessing the API
199
	 *
200
	 * @access public
201
	 *
202
	 * @since  1.1
203
	 */
204
	public function add_endpoint() {
205
		add_rewrite_endpoint( 'give-api', EP_ALL );
206
	}
207
208
	/**
209
	 * Registers query vars for API access
210
	 *
211
	 * @access public
212
	 * @since  1.1
213
	 *
214
	 * @param array $vars Query vars
215
	 *
216
	 * @return string[] $vars New query vars
217
	 */
218
	public function query_vars( $vars ) {
219
220
		$vars[] = 'token';
221
		$vars[] = 'key';
222
		$vars[] = 'query';
223
		$vars[] = 'type';
224
		$vars[] = 'form';
225
		$vars[] = 'number';
226
		$vars[] = 'date';
227
		$vars[] = 'startdate';
228
		$vars[] = 'enddate';
229
		$vars[] = 'donor';
230
		$vars[] = 'format';
231
		$vars[] = 'id';
232
		$vars[] = 'purchasekey';
233
		$vars[] = 'email';
234
235
		return $vars;
236
	}
237
238
	/**
239
	 * Retrieve the API versions
240
	 *
241
	 * @access public
242
	 * @since  1.1
243
	 * @return array
244
	 */
245
	public function get_versions() {
246
		return $this->versions;
247
	}
248
249
	/**
250
	 * Retrieve the API version that was queried
251
	 *
252
	 * @access public
253
	 * @since  1.1
254
	 * @return string
255
	 */
256
	public function get_queried_version() {
257
		return $this->queried_version;
258
	}
259
260
	/**
261
	 * Retrieves the default version of the API to use
262
	 *
263
	 * @access public
264
	 * @since  1.1
265
	 * @return string
266
	 */
267
	public function get_default_version() {
268
269
		$version = get_option( 'give_default_api_version' );
270
271
		if ( defined( 'GIVE_API_VERSION' ) ) {
272
			$version = GIVE_API_VERSION;
273
		} elseif ( ! $version ) {
274
			$version = 'v1';
275
		}
276
277
		return $version;
278
	}
279
280
	/**
281
	 * Sets the version of the API that was queried.
282
	 *
283
	 * Falls back to the default version if no version is specified
284
	 *
285
	 * @access private
286
	 * @since  1.1
287
	 */
288
	private function set_queried_version() {
289
290
		global $wp_query;
291
292
		$version = $wp_query->query_vars['give-api'];
293
294
		if ( strpos( $version, '/' ) ) {
295
296
			$version = explode( '/', $version );
297
			$version = strtolower( $version[0] );
298
299
			$wp_query->query_vars['give-api'] = str_replace( $version . '/', '', $wp_query->query_vars['give-api'] );
300
301
			if ( array_key_exists( $version, $this->versions ) ) {
302
303
				$this->queried_version = $version;
304
305
			} else {
306
307
				$this->is_valid_request = false;
308
				$this->invalid_version();
309
			}
310
		} else {
311
312
			$this->queried_version = $this->get_default_version();
313
314
		}
315
316
	}
317
318
	/**
319
	 * Validate the API request
320
	 *
321
	 * Checks for the user's public key and token against the secret key.
322
	 *
323
	 * @access private
324
	 * @global object $wp_query WordPress Query
325
	 * @uses   Give_API::get_user()
326
	 * @uses   Give_API::invalid_key()
327
	 * @uses   Give_API::invalid_auth()
328
	 * @since  1.1
329
	 * @return bool
330
	 */
331
	private function validate_request() {
332
		global $wp_query;
333
334
		$this->override = false;
335
336
		// Make sure we have both user and api key
337
		if ( ! empty( $wp_query->query_vars['give-api'] ) && ( $wp_query->query_vars['give-api'] !== 'forms' || ! empty( $wp_query->query_vars['token'] ) ) ) {
338
339
			if ( empty( $wp_query->query_vars['token'] ) || empty( $wp_query->query_vars['key'] ) ) {
340
				$this->missing_auth();
341
342
				return false;
343
			}
344
345
			// Retrieve the user by public API key and ensure they exist
346
			if ( ! ( $user = $this->get_user( $wp_query->query_vars['key'] ) ) ) {
347
348
				$this->invalid_key();
349
350
				return false;
351
352
			} else {
353
354
				$token  = urldecode( $wp_query->query_vars['token'] );
355
				$secret = $this->get_user_secret_key( $user );
0 ignored issues
show
$user is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
356
				$public = urldecode( $wp_query->query_vars['key'] );
357
358
				if ( hash_equals( md5( $secret . $public ), $token ) ) {
359
					$this->is_valid_request = true;
360
				} else {
361
					$this->invalid_auth();
362
363
					return false;
364
				}
365
366
			}
367
		} elseif ( ! empty( $wp_query->query_vars['give-api'] ) && $wp_query->query_vars['give-api'] === 'forms' ) {
368
			$this->is_valid_request = true;
369
			$wp_query->set( 'key', 'public' );
370
		}
371
	}
372
373
	/**
374
	 * Retrieve the user ID based on the public key provided
375
	 *
376
	 * @access public
377
	 * @since  1.1
378
	 * @global WPDB  $wpdb  Used to query the database using the WordPress
379
	 *                      Database API
380
	 *
381
	 * @param string $key   Public Key
382
	 *
383
	 * @return bool if user ID is found, false otherwise
384
	 */
385
	public function get_user( $key = '' ) {
386
		global $wpdb, $wp_query;
387
388
		if ( empty( $key ) ) {
389
			$key = urldecode( $wp_query->query_vars['key'] );
390
		}
391
392
		if ( empty( $key ) ) {
393
			return false;
394
		}
395
396
		$user = Give_Cache::get( md5( 'give_api_user_' . $key ), true );
397
398
		if ( false === $user ) {
399
			$user = $wpdb->get_var( $wpdb->prepare( "SELECT user_id FROM $wpdb->usermeta WHERE meta_key = %s LIMIT 1", $key ) );
400
			Give_Cache::set( md5( 'give_api_user_' . $key ), $user, DAY_IN_SECONDS, true );
401
		}
402
403
		if ( $user != null ) {
404
			$this->user_id = $user;
405
406
			return $user;
407
		}
408
409
		return false;
410
	}
411
412
	/**
413
	 * Get user public key.
414
	 *
415
	 * @param int $user_id
416
	 *
417
	 * @return mixed|null|string
418
	 */
419 View Code Duplication
	public function get_user_public_key( $user_id = 0 ) {
0 ignored issues
show
This method seems to be duplicated in your project.

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.

Loading history...
420
		global $wpdb;
421
422
		if ( empty( $user_id ) ) {
423
			return '';
424
		}
425
426
		$cache_key       = md5( 'give_api_user_public_key' . $user_id );
427
		$user_public_key = Give_Cache::get( $cache_key, true );
428
429
		if ( empty( $user_public_key ) ) {
430
			$user_public_key = $wpdb->get_var( $wpdb->prepare( "SELECT meta_key FROM $wpdb->usermeta WHERE meta_value = 'give_user_public_key' AND user_id = %d", $user_id ) );
431
			Give_Cache::set( $cache_key, $user_public_key, HOUR_IN_SECONDS, true );
432
		}
433
434
		return $user_public_key;
435
	}
436
437
	/**
438
	 * Get user secret key.
439
	 *
440
	 * @param int $user_id
441
	 *
442
	 * @return mixed|null|string
443
	 */
444 View Code Duplication
	public function get_user_secret_key( $user_id = 0 ) {
0 ignored issues
show
This method seems to be duplicated in your project.

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.

Loading history...
445
		global $wpdb;
446
447
		if ( empty( $user_id ) ) {
448
			return '';
449
		}
450
451
		$cache_key       = md5( 'give_api_user_secret_key' . $user_id );
452
		$user_secret_key = Give_Cache::get( $cache_key, true );
453
454
		if ( empty( $user_secret_key ) ) {
455
			$user_secret_key = $wpdb->get_var( $wpdb->prepare( "SELECT meta_key FROM $wpdb->usermeta WHERE meta_value = 'give_user_secret_key' AND user_id = %d", $user_id ) );
456
			Give_Cache::set( $cache_key, $user_secret_key, HOUR_IN_SECONDS, true );
457
		}
458
459
		return $user_secret_key;
460
	}
461
462
	/**
463
	 * Displays a missing authentication error if all the parameters are not met.
464
	 * provided
465
	 *
466
	 * @access private
467
	 * @uses   Give_API::output()
468
	 * @since  1.1
469
	 */
470 View Code Duplication
	private function missing_auth() {
0 ignored issues
show
This method seems to be duplicated in your project.

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.

Loading history...
471
		$error          = array();
472
		$error['error'] = __( 'You must specify both a token and API key.', 'give' );
473
474
		$this->data = $error;
475
		$this->output( 401 );
476
	}
477
478
	/**
479
	 * Displays an authentication failed error if the user failed to provide valid
480
	 * credentials
481
	 *
482
	 * @access private
483
	 * @since  1.1
484
	 * @uses   Give_API::output()
485
	 * @return void
486
	 */
487 View Code Duplication
	private function invalid_auth() {
0 ignored issues
show
This method seems to be duplicated in your project.

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.

Loading history...
488
		$error          = array();
489
		$error['error'] = __( 'Your request could not be authenticated.', 'give' );
490
491
		$this->data = $error;
492
		$this->output( 403 );
493
	}
494
495
	/**
496
	 * Displays an invalid API key error if the API key provided couldn't be
497
	 * validated
498
	 *
499
	 * @access private
500
	 * @since  1.1
501
	 * @uses   Give_API::output()
502
	 * @return void
503
	 */
504 View Code Duplication
	private function invalid_key() {
0 ignored issues
show
This method seems to be duplicated in your project.

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.

Loading history...
505
		$error          = array();
506
		$error['error'] = __( 'Invalid API key.', 'give' );
507
508
		$this->data = $error;
509
		$this->output( 403 );
510
	}
511
512
	/**
513
	 * Displays an invalid version error if the version number passed isn't valid
514
	 *
515
	 * @access private
516
	 * @since  1.1
517
	 * @uses   Give_API::output()
518
	 * @return void
519
	 */
520 View Code Duplication
	private function invalid_version() {
0 ignored issues
show
This method seems to be duplicated in your project.

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.

Loading history...
521
		$error          = array();
522
		$error['error'] = __( 'Invalid API version.', 'give' );
523
524
		$this->data = $error;
525
		$this->output( 404 );
526
	}
527
528
	/**
529
	 * Listens for the API and then processes the API requests
530
	 *
531
	 * @access public
532
	 * @global $wp_query
533
	 * @since  1.1
534
	 * @return void
535
	 */
536
	public function process_query() {
537
538
		global $wp_query;
539
540
		// Start logging how long the request takes for logging
541
		$before = microtime( true );
542
543
		// Check for give-api var. Get out if not present
544
		if ( empty( $wp_query->query_vars['give-api'] ) ) {
545
			return;
546
		}
547
548
		// Determine which version was queried
549
		$this->set_queried_version();
550
551
		// Determine the kind of query
552
		$this->set_query_mode();
553
554
		// Check for a valid user and set errors if necessary
555
		$this->validate_request();
556
557
		// Only proceed if no errors have been noted
558
		if ( ! $this->is_valid_request ) {
559
			return;
560
		}
561
562
		if ( ! defined( 'GIVE_DOING_API' ) ) {
563
			define( 'GIVE_DOING_API', true );
564
		}
565
566
		$data         = array();
567
		$this->routes = new $this->versions[$this->get_queried_version()];
568
		$this->routes->validate_request();
569
570
		switch ( $this->endpoint ) :
571
572
			case 'stats' :
573
574
				$data = $this->routes->get_stats( array(
575
					'type'      => isset( $wp_query->query_vars['type'] ) ? $wp_query->query_vars['type'] : null,
576
					'form'      => isset( $wp_query->query_vars['form'] ) ? $wp_query->query_vars['form'] : null,
577
					'date'      => isset( $wp_query->query_vars['date'] ) ? $wp_query->query_vars['date'] : null,
578
					'startdate' => isset( $wp_query->query_vars['startdate'] ) ? $wp_query->query_vars['startdate'] : null,
579
					'enddate'   => isset( $wp_query->query_vars['enddate'] ) ? $wp_query->query_vars['enddate'] : null,
580
				) );
581
582
				break;
583
584
			case 'forms' :
585
586
				$form = isset( $wp_query->query_vars['form'] ) ? $wp_query->query_vars['form'] : null;
587
588
				$data = $this->routes->get_forms( $form );
589
590
				break;
591
592
			case 'donors' :
593
594
				$donor = isset( $wp_query->query_vars['donor'] ) ? $wp_query->query_vars['donor'] : null;
595
596
				$data = $this->routes->get_donors( $donor );
597
598
				break;
599
600
			case 'donations' :
601
602
				/**
603
				 *  Call to get recent donations
604
				 *
605
				 * @params text date | today, yesterday or range
606
				 * @params date startdate | required when date = range and format to be YYYYMMDD (i.e. 20170524)
607
				 * @params date enddate | required when date = range and format to be YYYYMMDD (i.e. 20170524)
608
				 */
609
				$data = $this->routes->get_recent_donations( array(
610
					'id'        => isset( $wp_query->query_vars['id'] ) ? $wp_query->query_vars['id'] : null,
611
					'date'      => isset( $wp_query->query_vars['date'] ) ? $wp_query->query_vars['date'] : null,
612
					'startdate' => isset( $wp_query->query_vars['startdate'] ) ? $wp_query->query_vars['startdate'] : null,
613
					'enddate'   => isset( $wp_query->query_vars['enddate'] ) ? $wp_query->query_vars['enddate'] : null,
614
				) );
615
616
				break;
617
618
		endswitch;
619
620
		// Allow extensions to setup their own return data
621
		$this->data = apply_filters( 'give_api_output_data', $data, $this->endpoint, $this );
622
623
		$after                       = microtime( true );
624
		$request_time                = ( $after - $before );
625
		$this->data['request_speed'] = $request_time;
626
627
		// Log this API request, if enabled. We log it here because we have access to errors.
628
		$this->log_request( $this->data );
629
630
		// Send out data to the output function
631
		$this->output();
632
	}
633
634
	/**
635
	 * Returns the API endpoint requested
636
	 *
637
	 * @access public
638
	 * @since  1.1
639
	 * @return string $query Query mode
640
	 */
641
	public function get_query_mode() {
642
643
		return $this->endpoint;
644
	}
645
646
	/**
647
	 * Determines the kind of query requested and also ensure it is a valid query
648
	 *
649
	 * @access public
650
	 * @since  1.1
651
	 * @global $wp_query
652
	 */
653
	public function set_query_mode() {
654
655
		global $wp_query;
656
657
		// Whitelist our query options
658
		$accepted = apply_filters( 'give_api_valid_query_modes', array(
659
			'stats',
660
			'forms',
661
			'donors',
662
			'donations',
663
		) );
664
665
		$query = isset( $wp_query->query_vars['give-api'] ) ? $wp_query->query_vars['give-api'] : null;
666
		$query = str_replace( $this->queried_version . '/', '', $query );
667
668
		$error = array();
669
670
		// Make sure our query is valid
671
		if ( ! in_array( $query, $accepted ) ) {
672
			$error['error'] = __( 'Invalid query.', 'give' );
673
674
			$this->data = $error;
675
			// 400 is Bad Request
676
			$this->output( 400 );
677
		}
678
679
		$this->endpoint = $query;
680
	}
681
682
	/**
683
	 * Get page number
684
	 *
685
	 * @access public
686
	 * @since  1.1
687
	 * @global $wp_query
688
	 * @return int $wp_query->query_vars['page'] if page number returned (default: 1)
689
	 */
690
	public function get_paged() {
691
		global $wp_query;
692
693
		return isset( $wp_query->query_vars['page'] ) ? $wp_query->query_vars['page'] : 1;
694
	}
695
696
697
	/**
698
	 * Number of results to display per page
699
	 *
700
	 * @access public
701
	 * @since  1.1
702
	 * @global $wp_query
703
	 * @return int $per_page Results to display per page (default: 10)
704
	 */
705
	public function per_page() {
706
		global $wp_query;
707
708
		$per_page = isset( $wp_query->query_vars['number'] ) ? $wp_query->query_vars['number'] : 10;
709
710
		if ( $per_page < 0 && $this->get_query_mode() == 'donors' ) {
711
			$per_page = 99999999;
712
		} // End if().
713
714
		return apply_filters( 'give_api_results_per_page', $per_page );
715
	}
716
717
	/**
718
	 * Sets up the dates used to retrieve earnings/donations
719
	 *
720
	 * @access public
721
	 * @since  1.2
722
	 *
723
	 * @param array $args Arguments to override defaults
724
	 *
725
	 * @return array $dates
726
	 */
727
	public function get_dates( $args = array() ) {
728
		$dates = array();
729
730
		$defaults = array(
731
			'type'      => '',
732
			'form'      => null,
733
			'date'      => null,
734
			'startdate' => null,
735
			'enddate'   => null,
736
		);
737
738
		$args = wp_parse_args( $args, $defaults );
739
740
		$current_time = current_time( 'timestamp' );
741
742
		if ( 'range' === $args['date'] ) {
743
			$startdate          = strtotime( $args['startdate'] );
744
			$enddate            = strtotime( $args['enddate'] );
745
			$dates['day_start'] = date( 'd', $startdate );
746
			$dates['day_end']   = date( 'd', $enddate );
747
			$dates['m_start']   = date( 'n', $startdate );
748
			$dates['m_end']     = date( 'n', $enddate );
749
			$dates['year']      = date( 'Y', $startdate );
750
			$dates['year_end']  = date( 'Y', $enddate );
751
		} else {
752
			// Modify dates based on predefined ranges
753
			switch ( $args['date'] ) :
754
755 View Code Duplication
				case 'this_month' :
0 ignored issues
show
This code seems to be duplicated across your project.

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.

Loading history...
756
					$dates['day']     = null;
757
					$dates['m_start'] = date( 'n', $current_time );
758
					$dates['m_end']   = date( 'n', $current_time );
759
					$dates['year']    = date( 'Y', $current_time );
760
					break;
761
762
				case 'last_month' :
763
					$dates['day']     = null;
764
					$dates['m_start'] = date( 'n', $current_time ) == 1 ? 12 : date( 'n', $current_time ) - 1;
765
					$dates['m_end']   = $dates['m_start'];
766
					$dates['year']    = date( 'n', $current_time ) == 1 ? date( 'Y', $current_time ) - 1 : date( 'Y', $current_time );
767
					break;
768
769 View Code Duplication
				case 'today' :
0 ignored issues
show
This code seems to be duplicated across your project.

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.

Loading history...
770
					$dates['day']     = date( 'd', $current_time );
771
					$dates['m_start'] = date( 'n', $current_time );
772
					$dates['m_end']   = date( 'n', $current_time );
773
					$dates['year']    = date( 'Y', $current_time );
774
					break;
775
776 View Code Duplication
				case 'yesterday' :
0 ignored issues
show
This code seems to be duplicated across your project.

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.

Loading history...
777
778
					$year  = date( 'Y', $current_time );
779
					$month = date( 'n', $current_time );
780
					$day   = date( 'd', $current_time );
781
782
					if ( $month == 1 && $day == 1 ) {
783
784
						$year  -= 1;
785
						$month = 12;
786
						$day   = cal_days_in_month( CAL_GREGORIAN, $month, $year );
787
788
					} elseif ( $month > 1 && $day == 1 ) {
789
790
						$month -= 1;
791
						$day   = cal_days_in_month( CAL_GREGORIAN, $month, $year );
792
793
					} else {
794
795
						$day -= 1;
796
797
					}
798
799
					$dates['day']     = $day;
800
					$dates['m_start'] = $month;
801
					$dates['m_end']   = $month;
802
					$dates['year']    = $year;
803
804
					break;
805
806 View Code Duplication
				case 'this_quarter' :
0 ignored issues
show
This code seems to be duplicated across your project.

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.

Loading history...
807
					$month_now = date( 'n', $current_time );
808
809
					$dates['day'] = null;
810
811
					if ( $month_now <= 3 ) {
812
813
						$dates['m_start'] = 1;
814
						$dates['m_end']   = 3;
815
						$dates['year']    = date( 'Y', $current_time );
816
817
					} elseif ( $month_now <= 6 ) {
818
819
						$dates['m_start'] = 4;
820
						$dates['m_end']   = 6;
821
						$dates['year']    = date( 'Y', $current_time );
822
823
					} elseif ( $month_now <= 9 ) {
824
825
						$dates['m_start'] = 7;
826
						$dates['m_end']   = 9;
827
						$dates['year']    = date( 'Y', $current_time );
828
829
					} else {
830
831
						$dates['m_start'] = 10;
832
						$dates['m_end']   = 12;
833
						$dates['year']    = date( 'Y', $current_time );
834
835
					}
836
					break;
837
838 View Code Duplication
				case 'last_quarter' :
0 ignored issues
show
This code seems to be duplicated across your project.

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.

Loading history...
839
					$month_now = date( 'n', $current_time );
840
841
					$dates['day'] = null;
842
843
					if ( $month_now <= 3 ) {
844
845
						$dates['m_start'] = 10;
846
						$dates['m_end']   = 12;
847
						$dates['year']    = date( 'Y', $current_time ) - 1; // Previous year
848
849
					} elseif ( $month_now <= 6 ) {
850
851
						$dates['m_start'] = 1;
852
						$dates['m_end']   = 3;
853
						$dates['year']    = date( 'Y', $current_time );
854
855
					} elseif ( $month_now <= 9 ) {
856
857
						$dates['m_start'] = 4;
858
						$dates['m_end']   = 6;
859
						$dates['year']    = date( 'Y', $current_time );
860
861
					} else {
862
863
						$dates['m_start'] = 7;
864
						$dates['m_end']   = 9;
865
						$dates['year']    = date( 'Y', $current_time );
866
867
					}
868
					break;
869
870 View Code Duplication
				case 'this_year' :
0 ignored issues
show
This code seems to be duplicated across your project.

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.

Loading history...
871
					$dates['day']     = null;
872
					$dates['m_start'] = null;
873
					$dates['m_end']   = null;
874
					$dates['year']    = date( 'Y', $current_time );
875
					break;
876
877 View Code Duplication
				case 'last_year' :
0 ignored issues
show
This code seems to be duplicated across your project.

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.

Loading history...
878
					$dates['day']     = null;
879
					$dates['m_start'] = null;
880
					$dates['m_end']   = null;
881
					$dates['year']    = date( 'Y', $current_time ) - 1;
882
					break;
883
884
			endswitch;
885
		}// End if().
886
887
		/**
888
		 * Returns the filters for the dates used to retrieve earnings.
889
		 *
890
		 * @since 1.2
891
		 *
892
		 * @param array $dates The dates used for retrieving earnings.
893
		 */
894
		return apply_filters( 'give_api_stat_dates', $dates );
895
	}
896
897
	/**
898
	 * Process Get Donors API Request.
899
	 *
900
	 * @access public
901
	 * @since  1.1
902
	 * @global WPDB $wpdb  Used to query the database using the WordPress Database API.
903
	 *
904
	 * @param int $donor Donor ID.
905
	 *
906
	 * @return array $donors Multidimensional array of the donors.
907
	 */
908
	public function get_donors( $donor = null ) {
909
910
		$donors = array();
911
		$error  = array();
912
		if ( ! user_can( $this->user_id, 'view_give_sensitive_data' ) && ! $this->override ) {
913
			return $donors;
914
		}
915
916
		$paged    = $this->get_paged();
917
		$per_page = $this->per_page();
918
		$offset   = $per_page * ( $paged - 1 );
919
920
		if ( is_numeric( $donor ) ) {
921
			$field = 'id';
922
		} else {
923
			$field = 'email';
924
		}
925
926
		$donor_query = Give()->donors->get_donors( array(
927
			'number' => $per_page,
928
			'offset' => $offset,
929
			$field   => $donor,
930
		) );
931
		$donor_count = 0;
932
933
		if ( $donor_query ) {
934
935
			foreach ( $donor_query as $donor_obj ) {
936
937
				$names      = explode( ' ', $donor_obj->name );
938
				$first_name = ! empty( $names[0] ) ? $names[0] : '';
939
				$last_name  = '';
940
				if ( ! empty( $names[1] ) ) {
941
					unset( $names[0] );
942
					$last_name = implode( ' ', $names );
943
				}
944
945
				$title_prefix = Give()->donor_meta->get_meta( $donor_obj->id, '_give_donor_title_prefix', true );
946
947
				// Set title prefix empty, if not available in db.
948
				if ( empty( $title_prefix ) ) {
949
					$title_prefix = '';
950
				}
951
952
				$donors['donors'][ $donor_count ]['info']['user_id']      = '';
953
				$donors['donors'][ $donor_count ]['info']['username']     = '';
954
				$donors['donors'][ $donor_count ]['info']['display_name'] = '';
955
				$donors['donors'][ $donor_count ]['info']['donor_id']     = $donor_obj->id;
956
				$donors['donors'][ $donor_count ]['info']['title_prefix'] = $title_prefix;
957
				$donors['donors'][ $donor_count ]['info']['first_name']   = $first_name;
958
				$donors['donors'][ $donor_count ]['info']['last_name']    = $last_name;
959
				$donors['donors'][ $donor_count ]['info']['email']        = $donor_obj->email;
960
961
				if ( ! empty( $donor_obj->user_id ) ) {
962
963
					$user_data = get_userdata( $donor_obj->user_id );
964
965
					// Donor with registered account.
966
					$donors['donors'][ $donor_count ]['info']['user_id']      = $donor_obj->user_id;
967
					$donors['donors'][ $donor_count ]['info']['username']     = $user_data->user_login;
968
					$donors['donors'][ $donor_count ]['info']['display_name'] = $user_data->display_name;
969
970
				}
971
972
				$donors['donors'][ $donor_count ]['stats']['total_donations'] = $donor_obj->purchase_count;
973
				$donors['donors'][ $donor_count ]['stats']['total_spent']     = $donor_obj->purchase_value;
974
975
				$donor = new Give_Donor( $donor_obj->id );
976
977
				// Get donor's addresses.
978
				$donors['donors'][ $donor_count ]['address'] = $donor->address;
979
980
				$donor_count ++;
981
982
			} // End foreach().
983
		} elseif ( $donor ) {
984
985
			$error['error'] = sprintf(
986
				/* translators: %s: donor */
987
				__( 'Donor %s not found.', 'give' ),
988
				$donor
989
			);
990
991
			return $error;
992
993
		} else {
994
995
			$error['error'] = __( 'No donors found.', 'give' );
996
997
			return $error;
998
999
		} // End if().
1000
1001
		return $donors;
1002
	}
1003
1004
	/**
1005
	 * Process Get Donation Forms API Request
1006
	 *
1007
	 * @access public
1008
	 * @since  1.1
1009
	 *
1010
	 * @param int $form Give Form ID.
1011
	 *
1012
	 * @return array $donors Multidimensional array of the forms.
1013
	 */
1014
	public function get_forms( $form = null ) {
1015
1016
		$forms = array();
1017
		$error = array();
1018
1019
		if ( $form == null ) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $form of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison === instead.
Loading history...
1020
			$forms['forms'] = array();
1021
1022
			$form_list = get_posts( array(
1023
				'post_type'        => 'give_forms',
1024
				'posts_per_page'   => $this->per_page(),
1025
				'suppress_filters' => true,
1026
				'paged'            => $this->get_paged(),
1027
			) );
1028
1029
			if ( $form_list ) {
1030
				$i = 0;
1031
				foreach ( $form_list as $form_info ) {
1032
					$forms['forms'][ $i ] = $this->get_form_data( $form_info );
1033
					$i ++;
1034
				}
1035
			}
1036
		} else {
1037
			if ( get_post_type( $form ) == 'give_forms' ) {
1038
				$form_info = get_post( $form );
1039
1040
				$forms['forms'][0] = $this->get_form_data( $form_info );
1041
1042
			} else {
1043
				$error['error'] = sprintf( /* translators: %s: form */
1044
					__( 'Form %s not found.', 'give' ), $form );
1045
1046
				return $error;
1047
			}
1048
		}
1049
1050
		return $forms;
1051
	}
1052
1053
	/**
1054
	 * Given a give_forms post object, generate the data for the API output
1055
	 *
1056
	 * @since  1.1
1057
	 *
1058
	 * @param  object $form_info The Give Form's Post Object.
1059
	 *
1060
	 * @return array                Array of post data to return back in the API.
1061
	 */
1062
	private function get_form_data( $form_info ) {
1063
1064
		$form = array();
1065
		$currency = give_get_option('currency');
1066
1067
		$form['info']['id']            = $form_info->ID;
1068
		$form['info']['slug']          = $form_info->post_name;
1069
		$form['info']['title']         = $form_info->post_title;
1070
		$form['info']['create_date']   = $form_info->post_date;
1071
		$form['info']['modified_date'] = $form_info->post_modified;
1072
		$form['info']['status']        = $form_info->post_status;
1073
		$form['info']['link']          = html_entity_decode( $form_info->guid );
1074
		$form['info']['content']       = give_get_meta( $form_info->ID, '_give_form_content', true );
1075
		$form['info']['thumbnail']     = wp_get_attachment_url( get_post_thumbnail_id( $form_info->ID ) );
1076
1077
		if ( give_is_setting_enabled( give_get_option( 'categories', 'disabled' ) ) ) {
1078
			$form['info']['category'] = get_the_terms( $form_info, 'give_forms_category' );
1079
			$form['info']['tags']     = get_the_terms( $form_info, 'give_forms_tag' );
1080
		}
1081
		if ( give_is_setting_enabled( give_get_option( 'tags', 'disabled' ) ) ) {
1082
			$form['info']['tags'] = get_the_terms( $form_info, 'give_forms_tag' );
1083
		}
1084
1085
		// Check whether any goal is to be achieved for the donation form.
1086
		$goal_option = give_get_meta( $form_info->ID, '_give_goal_option', true );
1087
		$goal_amount = give_get_meta( $form_info->ID, '_give_set_goal', true );
1088
		if ( give_is_setting_enabled( $goal_option ) && $goal_amount ) {
1089
			$total_income                         = give_get_form_earnings_stats( $form_info->ID );
1090
			$goal_percentage_completed            = ( $total_income < $goal_amount ) ? round( ( $total_income / $goal_amount ) * 100, 2 ) : 100;
1091
			$form['goal']['amount']               = isset( $goal_amount ) ? give_format_decimal( array( 'amount' => $goal_amount, 'currency' => $currency  ) ) : '';
1092
			$form['goal']['percentage_completed'] = isset( $goal_percentage_completed ) ? $goal_percentage_completed : '';
1093
		}
1094
1095
		if ( user_can( $this->user_id, 'view_give_reports' ) || $this->override ) {
1096
			$form['stats']['total']['donations']           = give_get_form_sales_stats( $form_info->ID );
1097
			$form['stats']['total']['earnings']            = give_format_decimal( array( 'amount' => give_get_form_earnings_stats( $form_info->ID ), 'currency' => $currency  ) );
1098
			$form['stats']['monthly_average']['donations'] = give_get_average_monthly_form_sales( $form_info->ID );
1099
			$form['stats']['monthly_average']['earnings']  = give_format_decimal( array( 'amount' => give_get_average_monthly_form_earnings( $form_info->ID ), 'currency' => $currency  ) );
1100
		}
1101
1102
		$counter = 0;
1103
		if ( give_has_variable_prices( $form_info->ID ) ) {
1104
			foreach ( give_get_variable_prices( $form_info->ID ) as $price ) {
0 ignored issues
show
The expression give_get_variable_prices($form_info->ID) of type false|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1105
				$counter ++;
1106
				// multi-level item
1107
				$level                                     = isset( $price['_give_text'] ) ? $price['_give_text'] : 'level-' . $counter;
1108
				$form['pricing'][ sanitize_key( $level ) ] = give_format_decimal( array( 'amount' => $price['_give_amount'], 'currency' => $currency ) );
1109
1110
			}
1111
		} else {
1112
			$form['pricing']['amount'] = give_format_decimal( array( 'amount' => give_get_form_price( $form_info->ID ), 'currency' => $currency ) );
1113
		}
1114
1115
		if ( user_can( $this->user_id, 'view_give_sensitive_data' ) || $this->override ) {
1116
1117
			/**
1118
			 * Fires when generating API sensitive data.
1119
			 *
1120
			 * @since 1.1
1121
			 */
1122
			do_action( 'give_api_sensitive_data' );
1123
1124
		}
1125
1126
		return apply_filters( 'give_api_forms_form', $form );
1127
1128
	}
1129
1130
	/**
1131
	 * Process Get Stats API Request
1132
	 *
1133
	 * @since 1.1
1134
	 *
1135
	 * @global WPDB $wpdb Used to query the database using the WordPress.
1136
	 *
1137
	 * @param array $args Arguments provided by API Request.
1138
	 *
1139
	 * @return array
1140
	 */
1141
	public function get_stats( $args = array() ) {
1142
		$defaults = array(
1143
			'type'      => null,
1144
			'form'      => null,
1145
			'date'      => null,
1146
			'startdate' => null,
1147
			'enddate'   => null,
1148
		);
1149
1150
		$args = wp_parse_args( $args, $defaults );
1151
1152
		$dates = $this->get_dates( $args );
1153
1154
		$currency = give_get_option('currency');
1155
		$stats     = array();
1156
		$earnings  = array(
1157
			'earnings' => array(),
1158
		);
1159
		$donations = array(
1160
			'donations' => array(),
1161
		);
1162
		$error     = array();
1163
1164
		if ( ! user_can( $this->user_id, 'view_give_reports' ) && ! $this->override ) {
1165
			return $stats;
1166
		}
1167
1168
		if ( $args['type'] == 'donations' ) {
1169
1170
			if ( $args['form'] == null ) {
1171
				if ( $args['date'] == null ) {
1172
					$donations = $this->get_default_sales_stats();
1173
				} elseif ( $args['date'] === 'range' ) {
1174
					// Return donations for a date range.
1175
					// Ensure the end date is later than the start date.
1176 View Code Duplication
					if ( $args['enddate'] < $args['startdate'] ) {
0 ignored issues
show
This code seems to be duplicated across your project.

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.

Loading history...
1177
						$error['error'] = __( 'The end date must be later than the start date.', 'give' );
1178
					}
1179
1180
					// Ensure both the start and end date are specified
1181 View Code Duplication
					if ( empty( $args['startdate'] ) || empty( $args['enddate'] ) ) {
0 ignored issues
show
This code seems to be duplicated across your project.

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.

Loading history...
1182
						$error['error'] = __( 'Invalid or no date range specified.', 'give' );
1183
					}
1184
1185
					$total = 0;
1186
1187
					// Loop through the years
1188
					$y = $dates['year'];
1189
					while ( $y <= $dates['year_end'] ) :
1190
1191 View Code Duplication
						if ( $dates['year'] == $dates['year_end'] ) {
0 ignored issues
show
This code seems to be duplicated across your project.

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.

Loading history...
1192
							$month_start = $dates['m_start'];
1193
							$month_end   = $dates['m_end'];
1194
						} elseif ( $y == $dates['year'] && $dates['year_end'] > $dates['year'] ) {
1195
							$month_start = $dates['m_start'];
1196
							$month_end   = 12;
1197
						} elseif ( $y == $dates['year_end'] ) {
1198
							$month_start = 1;
1199
							$month_end   = $dates['m_end'];
1200
						} else {
1201
							$month_start = 1;
1202
							$month_end   = 12;
1203
						}
1204
1205
						$i = $month_start;
1206
						while ( $i <= $month_end ) :
1207
1208
							if ( $i == $dates['m_start'] ) {
1209
								$d = $dates['day_start'];
1210
							} else {
1211
								$d = 1;
1212
							}
1213
1214 View Code Duplication
							if ( $i == $dates['m_end'] ) {
0 ignored issues
show
This code seems to be duplicated across your project.

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.

Loading history...
1215
								$num_of_days = $dates['day_end'];
1216
							} else {
1217
								$num_of_days = cal_days_in_month( CAL_GREGORIAN, $i, $y );
1218
							}
1219
1220
							while ( $d <= $num_of_days ) :
1221
								$sale_count = give_get_sales_by_date( $d, $i, $y );
1222
								$date_key   = date( 'Ymd', strtotime( $y . '/' . $i . '/' . $d ) );
1223
								if ( ! isset( $donations['sales'][ $date_key ] ) ) {
1224
									$donations['sales'][ $date_key ] = 0;
1225
								}
1226
								$donations['sales'][ $date_key ] += $sale_count;
1227
								$total                           += $sale_count;
1228
								$d ++;
1229
							endwhile;
1230
							$i ++;
1231
						endwhile;
1232
1233
						$y ++;
1234
					endwhile;
1235
1236
					$donations['totals'] = $total;
1237
				} else {
1238
					if ( $args['date'] == 'this_quarter' || $args['date'] == 'last_quarter' ) {
1239
						$donations_count = 0;
1240
1241
						// Loop through the months
1242
						$month = $dates['m_start'];
1243
1244
						while ( $month <= $dates['m_end'] ) :
1245
							$donations_count += give_get_sales_by_date( null, $month, $dates['year'] );
1246
							$month ++;
1247
						endwhile;
1248
1249
						$donations['donations'][ $args['date'] ] = $donations_count;
1250
					} else {
1251
						$donations['donations'][ $args['date'] ] = give_get_sales_by_date( $dates['day'], $dates['m_start'], $dates['year'] );
1252
					}
1253
				}// End if().
1254
			} elseif ( $args['form'] == 'all' ) {
1255
				$forms = get_posts( array(
1256
					'post_type' => 'give_forms',
1257
					'nopaging'  => true,
1258
				) );
1259
				$i     = 0;
1260
				foreach ( $forms as $form_info ) {
1261
					$donations['donations'][ $i ] = array(
1262
						$form_info->post_name => $this->stats->get_sales(
1263
							$form_info->ID,
1264
							is_numeric( $args['startdate'] )
1265
								? strtotime( $args['startdate'] )
1266
								: $args['startdate'],
1267
							is_numeric( $args['enddate'] )
1268
								? strtotime( $args['enddate'] )
1269
								: $args['enddate']
1270
						),
1271
					);
1272
					$i ++;
1273
				}
1274
			} else {
1275
				if ( get_post_type( $args['form'] ) == 'give_forms' ) {
1276
					$form_info                 = get_post( $args['form'] );
1277
					$donations['donations'][0] = array(
1278
						$form_info->post_name => $this->stats->get_sales(
1279
							$args['form'],
1280
							is_numeric( $args['startdate'] )
1281
								? strtotime( $args['startdate'] )
1282
								: $args['startdate'],
1283
							is_numeric( $args['enddate'] )
1284
								? strtotime( $args['enddate'] )
1285
								: $args['enddate']
1286
						),
1287
					);
1288
				} else {
1289
					$error['error'] = sprintf( /* translators: %s: form */
1290
						__( 'Form %s not found.', 'give' ), $args['form'] );
1291
				}
1292
			}// End if().
1293
1294
			if ( ! empty( $error ) ) {
1295
				return $error;
1296
			}
1297
1298
			return $donations;
1299
1300
		} elseif ( $args['type'] == 'earnings' ) {
1301
			if ( $args['form'] == null ) {
1302
				if ( $args['date'] == null ) {
1303
					$earnings = $this->get_default_earnings_stats();
1304
				} elseif ( $args['date'] === 'range' ) {
1305
					// Return sales for a date range
1306
					// Ensure the end date is later than the start date
1307 View Code Duplication
					if ( $args['enddate'] < $args['startdate'] ) {
0 ignored issues
show
This code seems to be duplicated across your project.

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.

Loading history...
1308
						$error['error'] = __( 'The end date must be later than the start date.', 'give' );
1309
					}
1310
1311
					// Ensure both the start and end date are specified
1312 View Code Duplication
					if ( empty( $args['startdate'] ) || empty( $args['enddate'] ) ) {
0 ignored issues
show
This code seems to be duplicated across your project.

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.

Loading history...
1313
						$error['error'] = __( 'Invalid or no date range specified.', 'give' );
1314
					}
1315
1316
					$total = (float) 0.00;
1317
1318
					// Loop through the years
1319
					$y = $dates['year'];
1320
					if ( ! isset( $earnings['earnings'] ) ) {
1321
						$earnings['earnings'] = array();
1322
					}
1323
					while ( $y <= $dates['year_end'] ) :
1324
1325 View Code Duplication
						if ( $dates['year'] == $dates['year_end'] ) {
0 ignored issues
show
This code seems to be duplicated across your project.

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.

Loading history...
1326
							$month_start = $dates['m_start'];
1327
							$month_end   = $dates['m_end'];
1328
						} elseif ( $y == $dates['year'] && $dates['year_end'] > $dates['year'] ) {
1329
							$month_start = $dates['m_start'];
1330
							$month_end   = 12;
1331
						} elseif ( $y == $dates['year_end'] ) {
1332
							$month_start = 1;
1333
							$month_end   = $dates['m_end'];
1334
						} else {
1335
							$month_start = 1;
1336
							$month_end   = 12;
1337
						}
1338
1339
						$i = $month_start;
1340
						while ( $i <= $month_end ) :
1341
1342
							if ( $i == $dates['m_start'] ) {
1343
								$d = $dates['day_start'];
1344
							} else {
1345
								$d = 1;
1346
							}
1347
1348 View Code Duplication
							if ( $i == $dates['m_end'] ) {
0 ignored issues
show
This code seems to be duplicated across your project.

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.

Loading history...
1349
								$num_of_days = $dates['day_end'];
1350
							} else {
1351
								$num_of_days = cal_days_in_month( CAL_GREGORIAN, $i, $y );
1352
							}
1353
1354
							while ( $d <= $num_of_days ) :
1355
								$earnings_stat = give_get_earnings_by_date( $d, $i, $y );
1356
								$date_key      = date( 'Ymd', strtotime( $y . '/' . $i . '/' . $d ) );
1357
								if ( ! isset( $earnings['earnings'][ $date_key ] ) ) {
1358
									$earnings['earnings'][ $date_key ] = 0;
1359
								}
1360
1361
								$earnings['earnings'][ $date_key ] += give_format_decimal( array( 'amount' => $earnings_stat, 'currency' => $currency ) );
1362
								$total                             += $earnings_stat;
1363
								$d ++;
1364
							endwhile;
1365
1366
							$i ++;
1367
						endwhile;
1368
1369
						$y ++;
1370
					endwhile;
1371
1372
					$earnings['totals'] = give_format_decimal( array( 'amount' => $total, 'currency' => $currency ) );
1373
				} else {
1374
					if ( $args['date'] == 'this_quarter' || $args['date'] == 'last_quarter' ) {
1375
						$earnings_count = (float) 0.00;
1376
1377
						// Loop through the months
1378
						$month = $dates['m_start'];
1379
1380
						while ( $month <= $dates['m_end'] ) :
1381
							$earnings_count += give_get_earnings_by_date( null, $month, $dates['year'] );
1382
							$month ++;
1383
						endwhile;
1384
1385
						$earnings['earnings'][ $args['date'] ] = give_format_decimal( array( 'amount' => $earnings_count, 'currency' => $currency ) );
1386
					} else {
1387
						$earnings['earnings'][ $args['date'] ] = give_format_decimal( array( 'amount' => give_get_earnings_by_date( $dates['day'], $dates['m_start'], $dates['year'] ), 'currency' => $currency ) );
1388
					}
1389
				}// End if().
1390
			} elseif ( $args['form'] == 'all' ) {
1391
				$forms = get_posts( array(
1392
					'post_type' => 'give_forms',
1393
					'nopaging'  => true,
1394
				) );
1395
1396
				$i = 0;
1397
				foreach ( $forms as $form_info ) {
1398
					$earnings['earnings'][ $i ] = array(
1399
						$form_info->post_name => give_format_decimal( array( 'amount' => give_get_form_earnings_stats( $form_info->ID ), 'currency' => $currency ) ),
1400
					);
1401
					$i ++;
1402
				}
1403
			} else {
1404
				if ( get_post_type( $args['form'] ) == 'give_forms' ) {
1405
					$form_info               = get_post( $args['form'] );
1406
					$earnings['earnings'][0] = array(
1407
						$form_info->post_name => give_format_decimal( array( 'amount' => $this->stats->get_earnings(
1408
							$args['form'],
1409
							is_numeric( $args['startdate'] )
1410
								? strtotime( $args['startdate'] )
1411
								: $args['startdate'],
1412
							is_numeric( $args['enddate'] )
1413
								? strtotime( $args['enddate'] )
1414
								: $args['enddate']
1415
						), 'currency' => $currency ) ),
1416
					);
1417
				} else {
1418
					$error['error'] = sprintf( /* translators: %s: form */
1419
						__( 'Form %s not found.', 'give' ), $args['form'] );
1420
				}
1421
			}// End if().
1422
1423
			if ( ! empty( $error ) ) {
1424
				return $error;
1425
			}
1426
1427
			return $earnings;
1428
		} elseif ( $args['type'] == 'donors' ) {
1429
			$donors                             = new Give_DB_Donors();
1430
			$stats['donations']['total_donors'] = $donors->count();
1431
1432
			return $stats;
1433
1434
		} elseif ( empty( $args['type'] ) ) {
1435
			$stats = array_merge( $stats, $this->get_default_sales_stats() );
1436
			$stats = array_merge( $stats, $this->get_default_earnings_stats() );
1437
1438
			return array(
1439
				'stats' => $stats,
1440
			);
1441
		}// End if().
1442
	}
1443
1444
	/**
1445
	 * Retrieves Recent Donations
1446
	 *
1447
	 * @access public
1448
	 * @since  1.1
1449
	 *
1450
	 * @param $args array
1451
	 *
1452
	 * @return array
1453
	 */
1454
	public function get_recent_donations( $args = array() ) {
1455
		global $wp_query;
1456
1457
		$defaults = array(
1458
			'id'        => null,
1459
			'date'      => null,
1460
			'startdate' => null,
1461
			'enddate'   => null,
1462
		);
1463
1464
		$args = wp_parse_args( $args, $defaults );
1465
1466
		$donations = array();
1467
1468
		if ( ! user_can( $this->user_id, 'view_give_reports' ) && ! $this->override ) {
1469
			return $donations;
1470
		}
1471
1472
		if ( isset( $wp_query->query_vars['id'] ) ) {
1473
			$query   = array();
1474
			$query[] = new Give_Payment( $wp_query->query_vars['id'] );
1475
		} elseif ( isset( $wp_query->query_vars['purchasekey'] ) ) {
1476
			$query   = array();
1477
			$query[] = give_get_payment_by( 'key', $wp_query->query_vars['purchasekey'] );
1478
		} elseif ( isset( $wp_query->query_vars['email'] ) ) {
1479
			$args  = array(
1480
				'fields'     => 'ids',
1481
				'meta_key'   => '_give_payment_donor_email',
1482
				'meta_value' => $wp_query->query_vars['email'],
1483
				'number'     => $this->per_page(),
1484
				'page'       => $this->get_paged(),
1485
			);
1486
			$query = give_get_payments( $args );
1487
		} elseif ( isset( $wp_query->query_vars['date'] ) ) {
1488
1489
			$current_time = current_time( 'timestamp' );
1490
			$dates        = $this->get_dates( $args );
1491
			$start_date   = '';
1492
			$end_date     = '';
1493
1494
			/**
1495
			 *  Switch case for date query argument
1496
			 *
1497
			 * @since  1.8.8
1498
			 *
1499
			 * @params text date | today, yesterday or range
1500
			 * @params date startdate | required when date = range and format to be YYYYMMDD (i.e. 20170524)
1501
			 * @params date enddate | required when date = range and format to be YYYYMMDD (i.e. 20170524)
1502
			 */
1503
			switch ( $wp_query->query_vars['date'] ) {
1504
1505
				case 'today':
1506
1507
					// Set and Format Start and End Date to be date of today.
1508
					$start_date = $end_date = date( 'Y/m/d', $current_time );
1509
1510
					break;
1511
1512
				case 'yesterday':
1513
1514
					// Set and Format Start and End Date to be date of yesterday.
1515
					$start_date = $end_date = date( 'Y/m', $current_time ) . '/' . ( date( 'd', $current_time ) - 1 );
1516
1517
					break;
1518
1519
				case 'range':
1520
1521
					// Format Start Date and End Date for filtering payment based on date range.
1522
					$start_date = $dates['year'] . '/' . $dates['m_start'] . '/' . $dates['day_start'];
1523
					$end_date   = $dates['year_end'] . '/' . $dates['m_end'] . '/' . $dates['day_end'];
1524
1525
					break;
1526
1527
			}
1528
1529
			$args = array(
1530
				'fields'     => 'ids',
1531
				'start_date' => $start_date,
1532
				'end_date'   => $end_date,
1533
				'number'     => $this->per_page(),
1534
				'page'       => $this->get_paged(),
1535
			);
1536
1537
			$query = give_get_payments( $args );
1538
		} else {
1539
			$args  = array(
1540
				'fields' => 'ids',
1541
				'number' => $this->per_page(),
1542
				'page'   => $this->get_paged(),
1543
			);
1544
			$query = give_get_payments( $args );
1545
		}// End if().
1546
1547
		if ( $query ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $query of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1548
			$i = 0;
1549
			foreach ( $query as $payment ) {
1550
1551
				if ( is_numeric( $payment ) ) {
1552
					$payment      = new Give_Payment( $payment );
1553
				}
1554
1555
				$payment_meta = $payment->get_meta();
1556
				$user_info    = $payment->user_info;
1557
1558
				$first_name = isset( $user_info['first_name'] ) ? $user_info['first_name'] : '';
1559
				$last_name  = isset( $user_info['last_name'] ) ? $user_info['last_name'] : '';
1560
1561
				$donations['donations'][ $i ]['ID']             = $payment->ID;
1562
				$donations['donations'][ $i ]['number']         = $payment->number;
1563
				$donations['donations'][ $i ]['transaction_id'] = $payment->transaction_id;
1564
				$donations['donations'][ $i ]['key']            = $payment->key;
1565
				$donations['donations'][ $i ]['total']          = give_format_decimal( array( 'donation_id' => $payment->ID, 'dp' => true ) );
1566
				$donations['donations'][ $i ]['status']         = give_get_payment_status( $payment, true );
1567
				$donations['donations'][ $i ]['gateway']        = $payment->gateway;
1568
				$donations['donations'][ $i ]['name']           = trim( "{$first_name} {$last_name}" );
1569
				$donations['donations'][ $i ]['fname']          = $first_name;
1570
				$donations['donations'][ $i ]['lname']          = $last_name;
1571
				$donations['donations'][ $i ]['email']          = $payment->email;
1572
				$donations['donations'][ $i ]['date']           = $payment->date;
1573
				$donations['donations'][ $i ]['payment_meta']   = array();
1574
1575
				$form_id  = isset( $payment_meta['form_id'] ) ? $payment_meta['form_id'] : $payment_meta;
1576
				$price    = isset( $payment_meta['form_id'] ) ? give_get_form_price( $payment_meta['form_id'] ) : false;
1577
				$price_id = isset( $payment_meta['price_id'] ) ? $payment_meta['price_id'] : null;
1578
1579
				$donations['donations'][ $i ]['form']['id']    = $form_id;
1580
				$donations['donations'][ $i ]['form']['name']  = get_the_title( $payment_meta['form_id'] );
1581
				$donations['donations'][ $i ]['form']['price'] = give_format_decimal( array( 'amount' => $price, 'currency' => give_get_option('currency' ), 'dp' => true ) );
1582
1583
				if ( give_has_variable_prices( $form_id ) ) {
1584
					if ( isset( $payment_meta['price_id'] ) ) {
1585
						$price_name                                         = give_get_price_option_name( $form_id, $payment_meta['price_id'], $payment->ID );
1586
						$donations['donations'][ $i ]['form']['price_name'] = $price_name;
1587
						$donations['donations'][ $i ]['form']['price_id']   = $price_id;
1588
						$donations['donations'][ $i ]['form']['price']      = give_format_decimal( array( 'amount' => give_get_price_option_amount( $form_id, $price_id ), 'currency' => give_get_option('currency' ), 'dp' => true ) );
1589
					}
1590
				}
1591
1592
				if( ! empty( $payment_meta ) ) {
1593
					// Add custom meta to API
1594
					foreach ( $payment_meta as $meta_key => $meta_value ) {
1595
1596
						$exceptions = array(
1597
							'_give_payment_form_title',
1598
							'form_title',
1599
							'_give_payment_form_id',
1600
							'form_id',
1601
							'_give_payment_price_id',
1602
							'price_id',
1603
							'user_info',
1604
							'_give_payment_purchase_key',
1605
							'key',
1606
							'email',
1607
							'date',
1608
							'currency',
1609
							'_give_payment_total',
1610
							'_give_payment_date'
1611
						);
1612
1613
						// Don't clutter up results with dupes
1614
						if ( ! is_string( $meta_value ) || in_array( $meta_key, $exceptions ) ) {
1615
							continue;
1616
						}
1617
1618
						// Meta key can contain price value like _give_fee_amount, so convert them to standard format.
1619
						if( give_is_amount_sanitized( $meta_value ) ) {
1620
							$meta_value = give_format_decimal( array( 'amount' => $meta_value, 'currency' => give_get_option('currency' ), 'dp' => true ) );
1621
						}
1622
1623
						$donations['donations'][ $i ]['payment_meta'][ $meta_key ] = $meta_value;
1624
1625
					}
1626
				}
1627
1628
				$i ++;
1629
			}// End foreach().
1630
		}// End if().
1631
1632
		return apply_filters( 'give_api_donations_endpoint', $donations );
1633
	}
1634
1635
	/**
1636
	 * Retrieve the output format.
1637
	 *
1638
	 * Determines whether results should be displayed in XML or JSON.
1639
	 *
1640
	 * @since  1.1
1641
	 * @access public
1642
	 *
1643
	 * @return mixed
1644
	 */
1645
	public function get_output_format() {
1646
		global $wp_query;
1647
1648
		$format = isset( $wp_query->query_vars['format'] ) ? $wp_query->query_vars['format'] : 'json';
1649
1650
		return apply_filters( 'give_api_output_format', $format );
1651
	}
1652
1653
1654
	/**
1655
	 * Log each API request, if enabled.
1656
	 *
1657
	 * @access private
1658
	 * @since  1.1
1659
	 *
1660
	 * @global WP_Query     $wp_query
1661
	 *
1662
	 * @param array         $data
1663
	 *
1664
	 * @return void
1665
	 */
1666
	private function log_request( $data = array() ) {
1667
		if ( ! $this->log_requests ) {
1668
			return;
1669
		}
1670
1671
		/**
1672
		 * @var WP_Query $wp_query
1673
		 */
1674
		global $wp_query;
1675
1676
		$query = array(
1677
			'give-api'    => $wp_query->query_vars['give-api'],
1678
			'key'         => isset( $wp_query->query_vars['key'] ) ? $wp_query->query_vars['key'] : null,
1679
			'token'       => isset( $wp_query->query_vars['token'] ) ? $wp_query->query_vars['token'] : null,
1680
			'query'       => isset( $wp_query->query_vars['query'] ) ? $wp_query->query_vars['query'] : null,
1681
			'type'        => isset( $wp_query->query_vars['type'] ) ? $wp_query->query_vars['type'] : null,
1682
			'form'        => isset( $wp_query->query_vars['form'] ) ? $wp_query->query_vars['form'] : null,
1683
			'donor'       => isset( $wp_query->query_vars['donor'] ) ? $wp_query->query_vars['donor'] : null,
1684
			'date'        => isset( $wp_query->query_vars['date'] ) ? $wp_query->query_vars['date'] : null,
1685
			'startdate'   => isset( $wp_query->query_vars['startdate'] ) ? $wp_query->query_vars['startdate'] : null,
1686
			'enddate'     => isset( $wp_query->query_vars['enddate'] ) ? $wp_query->query_vars['enddate'] : null,
1687
			'id'          => isset( $wp_query->query_vars['id'] ) ? $wp_query->query_vars['id'] : null,
1688
			'purchasekey' => isset( $wp_query->query_vars['purchasekey'] ) ? $wp_query->query_vars['purchasekey'] : null,
1689
			'email'       => isset( $wp_query->query_vars['email'] ) ? $wp_query->query_vars['email'] : null,
1690
		);
1691
1692
		$log_data = array(
1693
			'log_type'     => 'api_request',
1694
			'post_excerpt' => http_build_query( $query ),
1695
			'post_content' => ! empty( $data['error'] ) ? $data['error'] : '',
1696
		);
1697
1698
		$log_meta = array(
1699
			'api_query'  => http_build_query( $query ),
1700
			'request_ip' => give_get_ip(),
1701
			'user'       => $this->user_id,
1702
			'key'        => isset( $wp_query->query_vars['key'] ) ? $wp_query->query_vars['key'] : null,
1703
			'token'      => isset( $wp_query->query_vars['token'] ) ? $wp_query->query_vars['token'] : null,
1704
			'time'       => $data['request_speed'],
1705
			'version'    => $this->get_queried_version(),
1706
		);
1707
1708
		Give()->logs->insert_log( $log_data, $log_meta );
1709
	}
1710
1711
1712
	/**
1713
	 * Retrieve the output data.
1714
	 *
1715
	 * @access public
1716
	 * @since  1.1
1717
	 * @return array
1718
	 */
1719
	public function get_output() {
1720
		return $this->data;
1721
	}
1722
1723
	/**
1724
	 * Output Query in either JSON/XML.
1725
	 * The query data is outputted as JSON by default.
1726
	 *
1727
	 * @since 1.1
1728
	 * @global WP_Query $wp_query
1729
	 *
1730
	 * @param int       $status_code
1731
	 */
1732
	public function output( $status_code = 200 ) {
1733
1734
		$format = $this->get_output_format();
1735
1736
		status_header( $status_code );
1737
1738
		/**
1739
		 * Fires before outputting the API.
1740
		 *
1741
		 * @since 1.1
1742
		 *
1743
		 * @param array    $data   Response data to return.
1744
		 * @param Give_API $this   The Give_API object.
1745
		 * @param string   $format Output format, XML or JSON. Default is JSON.
1746
		 */
1747
		do_action( 'give_api_output_before', $this->data, $this, $format );
1748
1749
		switch ( $format ) :
1750
1751
			case 'xml' :
1752
1753
				require_once GIVE_PLUGIN_DIR . 'includes/libraries/array2xml.php';
1754
				$xml = Array2XML::createXML( 'give', $this->data );
1755
				echo $xml->saveXML();
1756
1757
				break;
1758
1759
			case 'json' :
1760
1761
				header( 'Content-Type: application/json' );
1762
				if ( ! empty( $this->pretty_print ) ) {
1763
					echo json_encode( $this->data, $this->pretty_print );
1764
				} else {
1765
					echo json_encode( $this->data );
1766
				}
1767
1768
				break;
1769
1770
			default :
1771
1772
				/**
1773
				 * Fires by the API while outputting other formats.
1774
				 *
1775
				 * @since 1.1
1776
				 *
1777
				 * @param array    $data Response data to return.
1778
				 * @param Give_API $this The Give_API object.
1779
				 */
1780
				do_action( "give_api_output_{$format}", $this->data, $this );
1781
1782
				break;
1783
1784
		endswitch;
1785
1786
		/**
1787
		 * Fires after outputting the API.
1788
		 *
1789
		 * @since 1.1
1790
		 *
1791
		 * @param array    $data   Response data to return.
1792
		 * @param Give_API $this   The Give_API object.
1793
		 * @param string   $format Output format, XML or JSON. Default is JSON.
1794
		 */
1795
		do_action( 'give_api_output_after', $this->data, $this, $format );
1796
1797
		give_die();
1798
	}
1799
1800
	/**
1801
	 * Modify User Profile
1802
	 *
1803
	 * Modifies the output of profile.php to add key generation/revocation.
1804
	 *
1805
	 * @access public
1806
	 * @since  1.1
1807
	 *
1808
	 * @param object $user Current user info
1809
	 *
1810
	 * @return void
1811
	 */
1812
	function user_key_field( $user ) {
0 ignored issues
show
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1813
1814
		if ( ( give_get_option( 'api_allow_user_keys', false ) || current_user_can( 'manage_give_settings' ) ) && current_user_can( 'edit_user', $user->ID ) ) {
1815
1816
			$user = get_userdata( $user->ID );
1817
			?>
1818
			<table class="form-table">
1819
				<tbody>
1820
				<tr>
1821
					<th>
1822
						<?php _e( 'Give API Keys', 'give' ); ?>
1823
					</th>
1824
					<td>
1825
						<?php
1826
						$public_key = $this->get_user_public_key( $user->ID );
1827
						$secret_key = $this->get_user_secret_key( $user->ID );
1828
						?>
1829
						<?php if ( empty( $user->give_user_public_key ) ) { ?>
1830
							<input name="give_set_api_key" type="checkbox" id="give_set_api_key" />
1831
							<span class="description"><?php _e( 'Generate API Key', 'give' ); ?></span>
1832
						<?php } else { ?>
1833
							<strong style="display:inline-block; width: 125px;"><?php _e( 'Public key:', 'give' ); ?>
1834
								&nbsp;</strong>
1835
							<input type="text" disabled="disabled" class="regular-text" id="publickey" value="<?php echo esc_attr( $public_key ); ?>" />
1836
							<br />
1837
							<strong style="display:inline-block; width: 125px;"><?php _e( 'Secret key:', 'give' ); ?>
1838
								&nbsp;</strong>
1839
							<input type="text" disabled="disabled" class="regular-text" id="privatekey" value="<?php echo esc_attr( $secret_key ); ?>" />
1840
							<br />
1841
							<strong style="display:inline-block; width: 125px;"><?php _e( 'Token:', 'give' ); ?>
1842
								&nbsp;</strong>
1843
							<input type="text" disabled="disabled" class="regular-text" id="token" value="<?php echo esc_attr( $this->get_token( $user->ID ) ); ?>" />
1844
							<br />
1845
							<input name="give_revoke_api_key" type="checkbox" id="give_revoke_api_key" />
1846
							<span class="description"><label for="give_revoke_api_key"><?php _e( 'Revoke API Keys', 'give' ); ?></label></span>
1847
						<?php } ?>
1848
					</td>
1849
				</tr>
1850
				</tbody>
1851
			</table>
1852
		<?php }// End if().
1853
	}
1854
1855
	/**
1856
	 * Process an API key generation/revocation
1857
	 *
1858
	 * @access public
1859
	 * @since  1.1
1860
	 *
1861
	 * @param array $args
1862
	 *
1863
	 * @return void
1864
	 */
1865
	public function process_api_key( $args ) {
1866
1867 View Code Duplication
		if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'give-api-nonce' ) ) {
0 ignored issues
show
This code seems to be duplicated across your project.

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.

Loading history...
1868
			wp_die( __( 'We\'re unable to recognize your session. Please refresh the screen to try again; otherwise contact your website administrator for assistance.', 'give' ), __( 'Error', 'give' ), array(
1869
				'response' => 403,
1870
			) );
1871
		}
1872
1873 View Code Duplication
		if ( empty( $args['user_id'] ) ) {
0 ignored issues
show
This code seems to be duplicated across your project.

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.

Loading history...
1874
			wp_die( __( 'User ID Required.', 'give' ), __( 'Error', 'give' ), array(
1875
				'response' => 401,
1876
			) );
1877
		}
1878
1879
		if ( is_numeric( $args['user_id'] ) ) {
1880
			$user_id = isset( $args['user_id'] ) ? absint( $args['user_id'] ) : get_current_user_id();
1881
		} else {
1882
			$userdata = get_user_by( 'login', $args['user_id'] );
1883
			$user_id  = $userdata->ID;
1884
		}
1885
		$process = isset( $args['give_api_process'] ) ? strtolower( $args['give_api_process'] ) : false;
1886
1887
		if ( $user_id == get_current_user_id() && ! give_get_option( 'allow_user_api_keys' ) && ! current_user_can( 'manage_give_settings' ) ) {
1888
			wp_die( sprintf( /* translators: %s: process */
1889
				__( 'You do not have permission to %s API keys for this user.', 'give' ), $process ), __( 'Error', 'give' ), array(
1890
				'response' => 403,
1891
			) );
1892 View Code Duplication
		} elseif ( ! current_user_can( 'manage_give_settings' ) ) {
0 ignored issues
show
This code seems to be duplicated across your project.

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.

Loading history...
1893
			wp_die( sprintf( /* translators: %s: process */
1894
				__( 'You do not have permission to %s API keys for this user.', 'give' ), $process ), __( 'Error', 'give' ), array(
1895
				'response' => 403,
1896
			) );
1897
		}
1898
1899
		switch ( $process ) {
1900
			case 'generate':
1901
				if ( $this->generate_api_key( $user_id ) ) {
1902
					Give_Cache::delete( Give_Cache::get_key( 'give_total_api_keys' ) );
1903
					wp_redirect( add_query_arg( 'give-messages[]', 'api-key-generated', 'edit.php?post_type=give_forms&page=give-tools&tab=api' ) );
1904
					exit();
1905
				} else {
1906
					wp_redirect( add_query_arg( 'give-messages[]', 'api-key-failed', 'edit.php?post_type=give_forms&page=give-tools&tab=api' ) );
1907
					exit();
1908
				}
1909
				break;
0 ignored issues
show
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
1910 View Code Duplication
			case 'regenerate':
0 ignored issues
show
This code seems to be duplicated across your project.

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.

Loading history...
1911
				$this->generate_api_key( $user_id, true );
1912
				Give_Cache::delete( Give_Cache::get_key( 'give_total_api_keys' ) );
1913
				wp_redirect( add_query_arg( 'give-messages[]', 'api-key-regenerated', 'edit.php?post_type=give_forms&page=give-tools&tab=api' ) );
1914
				exit();
1915
				break;
0 ignored issues
show
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
1916 View Code Duplication
			case 'revoke':
0 ignored issues
show
This code seems to be duplicated across your project.

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.

Loading history...
1917
				$this->revoke_api_key( $user_id );
1918
				Give_Cache::delete( Give_Cache::get_key( 'give_total_api_keys' ) );
1919
				wp_redirect( add_query_arg( 'give-messages[]', 'api-key-revoked', 'edit.php?post_type=give_forms&page=give-tools&tab=api' ) );
1920
				exit();
1921
				break;
0 ignored issues
show
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
1922
			default;
1923
				break;
1924
		}
1925
	}
1926
1927
	/**
1928
	 * Generate new API keys for a user
1929
	 *
1930
	 * @param int     $user_id    User ID the key is being generated for.
1931
	 * @param boolean $regenerate Regenerate the key for the user.
1932
	 *
1933
	 * @access public
1934
	 * @since  1.1
1935
	 *
1936
	 * @return boolean True if (re)generated successfully, false otherwise.
1937
	 */
1938
	public function generate_api_key( $user_id = 0, $regenerate = false ) {
1939
1940
		// Bail out, if user doesn't exists.
1941
		if ( empty( $user_id ) ) {
1942
			return false;
1943
		}
1944
1945
		$user = get_userdata( $user_id );
1946
1947
		// Bail Out, if user object doesn't exists.
1948
		if ( ! $user ) {
1949
			return false;
1950
		}
1951
1952
		$new_public_key = '';
1953
		$new_secret_key = '';
1954
1955
		if( ! empty( $_POST['from'] ) && 'profile' === $_POST['from'] ) {
1956
			// For User Profile Page.
1957
			if( ! empty( $_POST['give_set_api_key'] ) ) {
1958
				// Generate API Key from User Profile page.
1959
				$new_public_key = $this->generate_public_key( $user->user_email );
1960
				$new_secret_key = $this->generate_private_key( $user->ID );
1961
			} elseif ( ! empty( $_POST['give_revoke_api_key'] ) ) {
1962
				// Revoke API Key from User Profile page.
1963
				$this->revoke_api_key( $user->ID );
1964
			} else {
1965
				return false;
1966
			}
1967
		} else {
1968
			// For Tools > API page.
1969
			$public_key = $this->get_user_public_key( $user_id );
1970
1971
			if ( empty( $public_key ) && ! $regenerate ) {
1972
				// Generating API for first time.
1973
				$new_public_key = $this->generate_public_key( $user->user_email );
1974
				$new_secret_key = $this->generate_private_key( $user->ID );
1975
			} elseif ( $public_key && $regenerate ) {
1976
				// API Key already exists and Regenerating API Key.
1977
				$this->revoke_api_key( $user->ID );
1978
				$new_public_key = $this->generate_public_key( $user->user_email );
1979
				$new_secret_key = $this->generate_private_key( $user->ID );
1980
			} elseif ( ! empty( $public_key ) && ! $regenerate ) {
1981
				// Doing nothing, when API Key exists but still try to generate again instead of regenerating.
1982
				return false;
1983
			} else {
1984
				// Revoke API Key.
1985
				$this->revoke_api_key( $user->ID );
1986
			}
1987
		}
1988
1989
		update_user_meta( $user_id, $new_public_key, 'give_user_public_key' );
1990
		update_user_meta( $user_id, $new_secret_key, 'give_user_secret_key' );
1991
1992
		return true;
1993
	}
1994
1995
	/**
1996
	 * Revoke a users API keys
1997
	 *
1998
	 * @access public
1999
	 * @since  1.1
2000
	 *
2001
	 * @param int $user_id User ID of user to revoke key for
2002
	 *
2003
	 * @return bool
2004
	 */
2005
	public function revoke_api_key( $user_id = 0 ) {
2006
2007
		if ( empty( $user_id ) ) {
2008
			return false;
2009
		}
2010
2011
		$user = get_userdata( $user_id );
2012
2013
		if ( ! $user ) {
2014
			return false;
2015
		}
2016
2017
		$public_key = $this->get_user_public_key( $user_id );
2018
		$secret_key = $this->get_user_secret_key( $user_id );
2019
		if ( ! empty( $public_key ) ) {
2020
			Give_Cache::delete( Give_Cache::get_key( md5( 'give_api_user_' . $public_key ) ) );
2021
			Give_Cache::delete( Give_Cache::get_key( md5( 'give_api_user_public_key' . $user_id ) ) );
2022
			Give_Cache::delete( Give_Cache::get_key( md5( 'give_api_user_secret_key' . $user_id ) ) );
2023
			delete_user_meta( $user_id, $public_key );
2024
			delete_user_meta( $user_id, $secret_key );
2025
		} else {
2026
			return false;
2027
		}
2028
2029
		return true;
2030
	}
2031
2032
	public function get_version() {
2033
		return self::VERSION;
2034
	}
2035
2036
	/**
2037
	 * Generate the public key for a user
2038
	 *
2039
	 * @access private
2040
	 * @since  1.1
2041
	 *
2042
	 * @param string $user_email
2043
	 *
2044
	 * @return string
2045
	 */
2046
	private function generate_public_key( $user_email = '' ) {
2047
		$auth_key = defined( 'AUTH_KEY' ) ? AUTH_KEY : '';
2048
		$public   = hash( 'md5', $user_email . $auth_key . date( 'U' ) );
2049
2050
		return $public;
2051
	}
2052
2053
	/**
2054
	 * Generate the secret key for a user
2055
	 *
2056
	 * @access private
2057
	 * @since  1.1
2058
	 *
2059
	 * @param int $user_id
2060
	 *
2061
	 * @return string
2062
	 */
2063
	private function generate_private_key( $user_id = 0 ) {
2064
		$auth_key = defined( 'AUTH_KEY' ) ? AUTH_KEY : '';
2065
		$secret   = hash( 'md5', $user_id . $auth_key . date( 'U' ) );
2066
2067
		return $secret;
2068
	}
2069
2070
	/**
2071
	 * Retrieve the user's token
2072
	 *
2073
	 * @access private
2074
	 * @since  1.1
2075
	 *
2076
	 * @param int $user_id
2077
	 *
2078
	 * @return string
2079
	 */
2080
	public function get_token( $user_id = 0 ) {
2081
		return hash( 'md5', $this->get_user_secret_key( $user_id ) . $this->get_user_public_key( $user_id ) );
2082
	}
2083
2084
	/**
2085
	 * Generate the default donation stats returned by the 'stats' endpoint
2086
	 *
2087
	 * @access private
2088
	 * @since  1.1
2089
	 * @return array default sales statistics
2090
	 */
2091
	private function get_default_sales_stats() {
2092
2093
		// Default sales return
2094
		$donations                               = array();
2095
		$donations['donations']['today']         = $this->stats->get_sales( 0, 'today' );
2096
		$donations['donations']['current_month'] = $this->stats->get_sales( 0, 'this_month' );
2097
		$donations['donations']['last_month']    = $this->stats->get_sales( 0, 'last_month' );
2098
		$donations['donations']['totals']        = give_get_total_donations();
2099
2100
		return $donations;
2101
	}
2102
2103
	/**
2104
	 * Generate the default earnings stats returned by the 'stats' endpoint
2105
	 *
2106
	 * @access private
2107
	 * @return array default earnings statistics
2108
	 * @since  1.1
2109
	 */
2110
	private function get_default_earnings_stats() {
2111
		$currency = give_get_option( 'currency' );
2112
2113
		// Default earnings return
2114
		$earnings                              = array();
2115
		$earnings['earnings']['today']         = give_format_decimal( array(
2116
			'amount'   => $this->stats->get_earnings( 0, 'today' ),
2117
			'currency' => $currency,
2118
		) );
2119
		$earnings['earnings']['current_month'] = give_format_decimal( array(
2120
			'amount'   => $this->stats->get_earnings( 0, 'this_month' ),
2121
			'currency' => $currency,
2122
		) );
2123
		$earnings['earnings']['last_month']    = give_format_decimal( array(
2124
			'amount'   => $this->stats->get_earnings( 0, 'last_month' ),
2125
			'currency' => $currency,
2126
		) );
2127
		$earnings['earnings']['totals']        = give_format_decimal( array(
2128
			'amount'   => give_get_total_earnings(),
2129
			'currency' => $currency,
2130
		) );
2131
2132
		return $earnings;
2133
	}
2134
2135
	/**
2136
	 * API Key Backwards Compatibility
2137
	 *
2138
	 * A Backwards Compatibility call for the change of meta_key/value for users API Keys.
2139
	 *
2140
	 * @since  1.3.6
2141
	 *
2142
	 * @param  string $check     Whether to check the cache or not
2143
	 * @param  int    $object_id The User ID being passed
2144
	 * @param  string $meta_key  The user meta key
2145
	 * @param  bool   $single    If it should return a single value or array
2146
	 *
2147
	 * @return string            The API key/secret for the user supplied
2148
	 */
2149
	public function api_key_backwards_compat( $check, $object_id, $meta_key, $single ) {
2150
2151
		if ( $meta_key !== 'give_user_public_key' && $meta_key !== 'give_user_secret_key' ) {
2152
			return $check;
2153
		}
2154
2155
		$return = $check;
2156
2157
		switch ( $meta_key ) {
2158
			case 'give_user_public_key':
2159
				$return = Give()->api->get_user_public_key( $object_id );
2160
				break;
2161
			case 'give_user_secret_key':
2162
				$return = Give()->api->get_user_secret_key( $object_id );
2163
				break;
2164
		}
2165
2166
		if ( ! $single ) {
2167
			$return = array( $return );
2168
		}
2169
2170
		return $return;
2171
2172
	}
2173
}
2174