Passed
Push — master ( c24352...fabb71 )
by Brian
14:05 queued 08:38
created

GetPaid_Paypal_Gateway::delete_line_items()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 2
rs 10
cc 1
nc 1
nop 0
1
<?php
2
/**
3
 * Paypal payment gateway
4
 *
5
 */
6
7
defined( 'ABSPATH' ) || exit;
8
9
/**
10
 * Paypal Payment Gateway class.
11
 *
12
 */
13
class GetPaid_Paypal_Gateway extends GetPaid_Payment_Gateway {
14
15
    /**
16
	 * Payment method id.
17
	 *
18
	 * @var string
19
	 */
20
    public $id = 'paypal';
21
22
    /**
23
	 * An array of features that this gateway supports.
24
	 *
25
	 * @var array
26
	 */
27
    protected $supports = array( 'subscription' );
28
29
    /**
30
	 * Payment method order.
31
	 *
32
	 * @var int
33
	 */
34
    public $order = 1;
35
36
    /**
37
	 * Stores line items to send to PayPal.
38
	 *
39
	 * @var array
40
	 */
41
    protected $line_items = array();
42
43
    /**
44
	 * Endpoint for requests from PayPal.
45
	 *
46
	 * @var string
47
	 */
48
	protected $notify_url;
49
50
	/**
51
	 * Endpoint for requests to PayPal.
52
	 *
53
	 * @var string
54
	 */
55
    protected $endpoint;
56
    
57
    /**
58
	 * Currencies this gateway is allowed for.
59
	 *
60
	 * @var array
61
	 */
62
	public $currencies = array( 'AUD', 'BRL', 'CAD', 'MXN', 'NZD', 'HKD', 'SGD', 'USD', 'EUR', 'JPY', 'TRY', 'NOK', 'CZK', 'DKK', 'HUF', 'ILS', 'MYR', 'PHP', 'PLN', 'SEK', 'CHF', 'TWD', 'THB', 'GBP', 'RMB', 'RUB', 'INR' );
63
64
    /**
65
	 * URL to view a transaction.
66
	 *
67
	 * @var string
68
	 */
69
    public $view_transaction_url = 'https://www.{sandbox}paypal.com/activity/payment/%s';
70
71
    /**
72
	 * URL to view a subscription.
73
	 *
74
	 * @var string
75
	 */
76
	public $view_subscription_url = 'https://www.{sandbox}paypal.com/cgi-bin/webscr?cmd=_profile-recurring-payments&encrypted_profile_id=%s';
77
78
    /**
79
	 * Class constructor.
80
	 */
81
	public function __construct() {
82
83
        $this->title             = __( 'PayPal Standard', 'invoicing' );
84
        $this->method_title      = __( 'PayPal Standard', 'invoicing' );
85
        $this->order_button_text = __( 'Proceed to PayPal', 'invoicing' );
0 ignored issues
show
Bug Best Practice introduced by
The property order_button_text does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
86
        $this->notify_url        = wpinv_get_ipn_url( 'paypal' );
87
88
        add_filter( 'getpaid_paypal_args', array( $this, 'process_subscription' ), 10, 2 );
89
        add_filter( 'wpinv_gateway_description', array( $this, 'sandbox_notice' ), 10, 2 );
90
91
        parent::__construct();
92
    }
93
94
    /**
95
	 * Process Payment.
96
	 *
97
	 *
98
	 * @param WPInv_Invoice $invoice Invoice.
99
	 * @param array $submission_data Posted checkout fields.
100
	 * @param GetPaid_Payment_Form_Submission $submission Checkout submission.
101
	 * @return array
102
	 */
103
	public function process_payment( $invoice, $submission_data, $submission ) {
104
105
        // Get redirect url.
106
        $paypal_redirect = $this->get_request_url( $invoice );
107
108
        // Add a note about the request url.
109
        $invoice->add_note(
110
            sprintf(
111
                __( 'Redirecting to PayPal: %s', 'invoicing' ),
112
                esc_url( $paypal_redirect )
113
            ),
114
            false,
115
            false,
116
            true
117
        );
118
119
        // Redirect to PayPal
120
        wp_redirect( $paypal_redirect );
121
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
122
123
    }
124
125
    /**
126
	 * Get the PayPal request URL for an invoice.
127
	 *
128
	 * @param  WPInv_Invoice $invoice Invoice object.
129
	 * @return string
130
	 */
131
	public function get_request_url( $invoice ) {
132
133
        // Endpoint for this request
134
		$this->endpoint    = $this->is_sandbox( $invoice ) ? 'https://www.sandbox.paypal.com/cgi-bin/webscr?test_ipn=1&' : 'https://www.paypal.com/cgi-bin/webscr?';
135
136
        // Retrieve paypal args.
137
        $paypal_args       = map_deep( $this->get_paypal_args( $invoice ), 'urlencode' );
138
139
        if ( $invoice->is_recurring() ) {
140
            $paypal_args['bn'] = 'GetPaid_Subscribe_WPS_US';
141
        } else {
142
            $paypal_args['bn'] = 'GetPaid_ShoppingCart_WPS_US';
143
        }
144
145
        return add_query_arg( $paypal_args, $this->endpoint );
146
147
	}
148
149
    /**
150
	 * Get PayPal Args for passing to PP.
151
	 *
152
	 * @param  WPInv_Invoice $invoice Invoice object.
153
	 * @return array
154
	 */
155
	protected function get_paypal_args( $invoice ) {
156
157
        // Whether or not to send the line items as one item.
158
		$force_one_line_item = apply_filters( 'getpaid_paypal_force_one_line_item', false, $invoice );
159
160
		if ( $invoice->is_recurring() || ( wpinv_use_taxes() && wpinv_prices_include_tax() ) ) {
161
			$force_one_line_item = true;
162
		}
163
164
		$paypal_args = apply_filters(
165
			'getpaid_paypal_args',
166
			array_merge(
167
				$this->get_transaction_args( $invoice ),
168
				$this->get_line_item_args( $invoice, $force_one_line_item )
169
			),
170
			$invoice
171
		);
172
173
		return $this->fix_request_length( $invoice, $paypal_args );
174
    }
175
176
    /**
177
	 * Get transaction args for paypal request.
178
	 *
179
	 * @param WPInv_Invoice $invoice Invoice object.
180
	 * @return array
181
	 */
182
	protected function get_transaction_args( $invoice ) {
183
184
		return array(
185
            'cmd'           => '_cart',
186
            'business'      => wpinv_get_option( 'paypal_email', false ),
187
            'no_shipping'   => '1',
188
            'shipping'      => '0',
189
            'no_note'       => '1',
190
            'charset'       => 'utf-8',
191
            'rm'            => is_ssl() ? 2 : 1,
192
            'upload'        => 1,
193
            'currency_code' => $invoice->get_currency(), // https://developer.paypal.com/docs/nvp-soap-api/currency-codes/#paypal
194
            'return'        => esc_url_raw( $this->get_return_url( $invoice ) ),
195
            'cancel_return' => esc_url_raw( $invoice->get_checkout_payment_url() ),
196
            'notify_url'    => getpaid_limit_length( $this->notify_url, 255 ),
197
            'invoice'       => getpaid_limit_length( $invoice->get_number(), 127 ),
198
            'custom'        => $invoice->get_id(),
199
            'first_name'    => getpaid_limit_length( $invoice->get_first_name(), 32 ),
200
            'last_name'     => getpaid_limit_length( $invoice->get_last_name(), 64 ),
201
            'country'       => getpaid_limit_length( $invoice->get_country(), 2 ),
202
            'email'         => getpaid_limit_length( $invoice->get_email(), 127 ),
203
            'cbt'           => get_bloginfo( 'name' )
204
        );
205
206
    }
207
208
    /**
209
	 * Get line item args for paypal request.
210
	 *
211
	 * @param  WPInv_Invoice $invoice Invoice object.
212
	 * @param  bool     $force_one_line_item Create only one item for this invoice.
213
	 * @return array
214
	 */
215
	protected function get_line_item_args( $invoice, $force_one_line_item = false ) {
216
217
        // Maybe send invoice as a single item.
218
		if ( $force_one_line_item ) {
219
            return $this->get_line_item_args_single_item( $invoice );
220
        }
221
222
        // Send each line item individually.
223
        $line_item_args = array();
224
225
        // Prepare line items.
226
        $this->prepare_line_items( $invoice );
227
228
        // Add taxes to the cart
229
        if ( wpinv_use_taxes() && $invoice->is_taxable() ) {
230
            $line_item_args['tax_cart'] = wpinv_sanitize_amount( (float) $invoice->get_total_tax(), 2 );
231
        }
232
233
        // Add discount.
234
        if ( $invoice->get_total_discount() > 0 ) {
235
            $line_item_args['discount_amount_cart'] = wpinv_sanitize_amount( (float) $invoice->get_total_discount(), 2 );
236
        }
237
238
		return array_merge( $line_item_args, $this->get_line_items() );
239
240
    }
241
242
    /**
243
	 * Get line item args for paypal request as a single line item.
244
	 *
245
	 * @param  WPInv_Invoice $invoice Invoice object.
246
	 * @return array
247
	 */
248
	protected function get_line_item_args_single_item( $invoice ) {
249
		$this->delete_line_items();
250
251
        $item_name = sprintf( __( 'Invoice #%s', 'invoicing' ), $invoice->get_number() );
252
		$this->add_line_item( $item_name, 1, wpinv_sanitize_amount( (float) $invoice->get_total(), 2 ), $invoice->get_id() );
0 ignored issues
show
Bug introduced by
It seems like wpinv_sanitize_amount((d...nvoice->get_total(), 2) can also be of type string; however, parameter $amount of GetPaid_Paypal_Gateway::add_line_item() does only seem to accept double, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

252
		$this->add_line_item( $item_name, 1, /** @scrutinizer ignore-type */ wpinv_sanitize_amount( (float) $invoice->get_total(), 2 ), $invoice->get_id() );
Loading history...
253
254
		return $this->get_line_items();
255
    }
256
257
    /**
258
	 * Return all line items.
259
	 */
260
	protected function get_line_items() {
261
		return $this->line_items;
262
	}
263
264
    /**
265
	 * Remove all line items.
266
	 */
267
	protected function delete_line_items() {
268
		$this->line_items = array();
269
    }
270
271
    /**
272
	 * Prepare line items to send to paypal.
273
	 *
274
	 * @param  WPInv_Invoice $invoice Invoice object.
275
	 */
276
	protected function prepare_line_items( $invoice ) {
277
		$this->delete_line_items();
278
279
		// Items.
280
		foreach ( $invoice->get_items() as $item ) {
281
			$amount   = $invoice->get_template() == 'amount' ? $item->get_price() : $item->get_sub_total();
282
			$quantity = $invoice->get_template() == 'amount' ? 1 : $item->get_quantity();
283
			$this->add_line_item( $item->get_raw_name(), $quantity, $amount, $item->get_id() );
284
        }
285
286
        // Fees.
287
		foreach ( $invoice->get_fees() as $fee => $data ) {
288
            $this->add_line_item( $fee, 1, $data['amount'] );
289
        }
290
291
    }
292
293
    /**
294
	 * Add PayPal Line Item.
295
	 *
296
	 * @param  string $item_name Item name.
297
	 * @param  int    $quantity Item quantity.
298
	 * @param  float  $amount Amount.
299
	 * @param  string $item_number Item number.
300
	 */
301
	protected function add_line_item( $item_name, $quantity = 1, $amount = 0.0, $item_number = '' ) {
302
		$index = ( count( $this->line_items ) / 4 ) + 1;
303
304
		$item = apply_filters(
305
			'getpaid_paypal_line_item',
306
			array(
307
				'item_name'   => html_entity_decode( getpaid_limit_length( $item_name ? wp_strip_all_tags( $item_name ) : __( 'Item', 'invoicing' ), 127 ), ENT_NOQUOTES, 'UTF-8' ),
308
				'quantity'    => (int) $quantity,
309
				'amount'      => wpinv_sanitize_amount( (float) $amount, 2 ),
310
				'item_number' => $item_number,
311
			),
312
			$item_name,
313
			$quantity,
314
			$amount,
315
			$item_number
316
		);
317
318
		$this->line_items[ 'item_name_' . $index ]   = getpaid_limit_length( $item['item_name'], 127 );
319
        $this->line_items[ 'quantity_' . $index ]    = $item['quantity'];
320
        
321
        // The price or amount of the product, service, or contribution, not including shipping, handling, or tax.
322
		$this->line_items[ 'amount_' . $index ]      = $item['amount'];
323
		$this->line_items[ 'item_number_' . $index ] = getpaid_limit_length( $item['item_number'], 127 );
324
    }
325
326
    /**
327
	 * If the default request with line items is too long, generate a new one with only one line item.
328
	 *
329
	 * https://support.microsoft.com/en-us/help/208427/maximum-url-length-is-2-083-characters-in-internet-explorer.
330
	 *
331
	 * @param WPInv_Invoice $invoice Invoice to be sent to Paypal.
332
	 * @param array    $paypal_args Arguments sent to Paypal in the request.
333
	 * @return array
334
	 */
335
	protected function fix_request_length( $invoice, $paypal_args ) {
336
		$max_paypal_length = 2083;
337
		$query_candidate   = http_build_query( $paypal_args, '', '&' );
338
339
		if ( strlen( $this->endpoint . $query_candidate ) <= $max_paypal_length ) {
340
			return $paypal_args;
341
		}
342
343
		return apply_filters(
344
			'getpaid_paypal_args',
345
			array_merge(
346
				$this->get_transaction_args( $invoice ),
347
				$this->get_line_item_args( $invoice, true )
348
			),
349
			$invoice
350
		);
351
352
    }
353
    
354
    /**
355
	 * Processes recurring invoices.
356
	 *
357
	 * @param  array $paypal_args PayPal args.
358
	 * @param  WPInv_Invoice    $invoice Invoice object.
359
	 */
360
	public function process_subscription( $paypal_args, $invoice ) {
361
362
        // Make sure this is a subscription.
363
        if ( ! $invoice->is_recurring() || ! $subscription = wpinv_get_subscription( $invoice ) ) {
364
            return $paypal_args;
365
        }
366
367
        // It's a subscription
368
        $paypal_args['cmd'] = '_xclick-subscriptions';
369
370
        // Subscription name.
371
        $paypal_args['item_name'] = sprintf( __( 'Invoice #%s', 'invoicing' ), $invoice->get_number() );
372
373
        // Get subscription args.
374
        $period                 = strtoupper( substr( $subscription->period, 0, 1) );
375
        $interval               = (int) $subscription->frequency;
0 ignored issues
show
Bug Best Practice introduced by
The property frequency does not exist on WPInv_Subscription. Since you implemented __get, consider adding a @property annotation.
Loading history...
376
        $bill_times             = (int) $subscription->bill_times;
377
        $initial_amount         = (float) wpinv_sanitize_amount( $invoice->get_initial_total(), 2 );
378
        $recurring_amount       = (float) wpinv_sanitize_amount( $invoice->get_recurring_total(), 2 );
379
        $subscription_item      = $invoice->get_recurring( true );
380
381
        if ( $subscription_item->has_free_trial() ) {
382
383
            $paypal_args['a1'] = 0 == $initial_amount ? 0 : $initial_amount;
384
385
			// Trial period length.
386
			$paypal_args['p1'] = $subscription_item->get_trial_interval();
387
388
			// Trial period.
389
			$paypal_args['t1'] = $subscription_item->get_trial_period();
390
391
        } else if ( $initial_amount != $recurring_amount ) {
392
393
            // No trial period, but initial amount includes a sign-up fee and/or other items, so charge it as a separate period.
394
395
            if ( 1 == $bill_times ) {
396
                $param_number = 3;
397
            } else {
398
                $param_number = 1;
399
            }
400
401
            $paypal_args[ 'a' . $param_number ] = $initial_amount ? 0 : $initial_amount;
402
403
            // Sign Up interval
404
            $paypal_args[ 'p' . $param_number ] = $interval;
405
406
            // Sign Up unit of duration
407
            $paypal_args[ 't' . $param_number ] = $period;
408
409
        }
410
411
        // We have a recurring payment
412
		if ( ! isset( $param_number ) || 1 == $param_number ) {
413
414
			// Subscription price
415
			$paypal_args['a3'] = $recurring_amount;
416
417
			// Subscription duration
418
			$paypal_args['p3'] = $interval;
419
420
			// Subscription period
421
			$paypal_args['t3'] = $period;
422
423
        }
424
        
425
        // Recurring payments
426
		if ( 1 == $bill_times || ( $initial_amount != $recurring_amount && ! $subscription_item->has_free_trial() && 2 == $bill_times ) ) {
427
428
			// Non-recurring payments
429
			$paypal_args['src'] = 0;
430
431
		} else {
432
433
			$paypal_args['src'] = 1;
434
435
			if ( $bill_times > 0 ) {
436
437
				// An initial period is being used to charge a sign-up fee
438
				if ( $initial_amount != $recurring_amount && ! $subscription_item->has_free_trial() ) {
439
					$bill_times--;
440
				}
441
442
                // Make sure it's not over the max of 52
443
                $paypal_args['srt'] = ( $bill_times <= 52 ? absint( $bill_times ) : 52 );
444
445
			}
446
        }
447
        
448
        // Force return URL so that order description & instructions display
449
        $paypal_args['rm'] = 2;
450
        
451
        // Get rid of redudant items.
452
        foreach ( array( 'item_name_1', 'quantity_1', 'amount_1', 'item_number_1' ) as $arg ) {
453
454
            if ( isset( $paypal_args[ $arg ] ) ) {
455
                unset( $paypal_args[ $arg ] );
456
            }
457
458
        }
459
460
        return apply_filters(
461
			'getpaid_paypal_subscription_args',
462
			$paypal_args,
463
			$invoice
464
        );
465
466
    }
467
468
    /**
469
	 * Processes ipns and marks payments as complete.
470
	 *
471
	 * @return void
472
	 */
473
	public function verify_ipn() {
474
475
        // Validate the IPN.
476
        if ( empty( $_POST ) || ! $this->validate_ipn() ) {
477
		    wp_die( 'PayPal IPN Request Failure', 'PayPal IPN', array( 'response' => 500 ) );
478
		}
479
480
        // Process the IPN.
481
        $posted  = wp_unslash( $_POST );
482
        $invoice = wpinv_get_invoice( $posted['custom'] );
483
484
        if ( $invoice && $this->id == $invoice->get_gateway() ) {
485
486
			$posted['payment_status'] = strtolower( $posted['payment_status'] );
487
488
            wpinv_error_log( 'Found invoice #' . $invoice->get_number() );
489
            wpinv_error_log( 'Payment status:' . $posted['payment_status'] );
490
491
			if ( method_exists( $this, 'ipn_txn_' . $posted['txn_type'] ) ) {
492
				call_user_func( array( $this, 'ipn_txn_' . $posted['txn_type'] ), $invoice, $posted );
0 ignored issues
show
Security Code Execution introduced by
array($this, 'ipn_txn_' . $posted['txn_type']) can contain request data and is used in code execution context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_POST, and Data is passed through wp_unslash(), and wp_unslash($_POST) is assigned to $posted
    in includes/gateways/class-getpaid-paypal-gateway.php on line 481
  2. strtolower($posted['payment_status']) is assigned to $posted
    in includes/gateways/class-getpaid-paypal-gateway.php on line 486

General Strategies to prevent injection

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

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

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

$sanitized = (integer) $tainted;
Loading history...
493
            } else {
494
                wpinv_error_log( 'Aborting, Invalid type:' . $posted['txn_type'] );
495
            }
496
497
        }
498
499
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
500
501
    }
502
503
    /**
504
	 * Check PayPal IPN validity.
505
	 */
506
	public function validate_ipn() {
507
508
		wpinv_error_log( 'Validating PayPal IPN response' );
509
510
		// Get received values from post data.
511
		$validate_ipn        = wp_unslash( $_POST );
512
		$validate_ipn['cmd'] = '_notify-validate';
513
514
		// Send back post vars to paypal.
515
		$params = array(
516
			'body'        => $validate_ipn,
517
			'timeout'     => 60,
518
			'httpversion' => '1.1',
519
			'compress'    => false,
520
			'decompress'  => false,
521
			'user-agent'  => 'GetPaid/' . WPINV_VERSION,
522
		);
523
524
		// Post back to get a response.
525
		$response = wp_safe_remote_post( $this->is_sandbox() ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr', $params );
526
527
        // Check to see if the request was valid.
528
		if ( ! is_wp_error( $response ) && $response['response']['code'] >= 200 && $response['response']['code'] < 300 && strstr( $response['body'], 'VERIFIED' ) ) {
529
            wpinv_error_log( $response['body'], 'Received valid response from PayPal IPN' );
530
			return true;
531
        }
532
533
        if ( is_wp_error( $response ) ) {
534
            wpinv_error_log( $response->get_error_message(), 'Received invalid response from PayPal IPN' );
535
        } else {
536
            wpinv_error_log( $response['body'], 'Received invalid response from PayPal IPN' );
537
        }
538
539
        return false;
540
541
    }
542
543
    /**
544
	 * Check currency from IPN matches the invoice.
545
	 *
546
	 * @param WPInv_Invoice $invoice          Invoice object.
547
	 * @param string   $currency currency to validate.
548
	 */
549
	protected function validate_ipn_currency( $invoice, $currency ) {
550
		if ( strtolower( $invoice->get_currency() ) !== strtolower( $currency ) ) {
551
            wpinv_record_gateway_error( 'IPN Error', "Currencies do not match: {$currency} instead of {$invoice->get_currency()}" );
552
553
			/* translators: %s: currency code. */
554
			$invoice->update_status( 'wpi-processing', sprintf( __( 'Validation error: PayPal currencies do not match (code %s).', 'invoicing' ), $currency ) );
555
			exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
556
		}
557
	}
558
559
	/**
560
	 * Check payment amount from IPN matches the invoice.
561
	 *
562
	 * @param WPInv_Invoice $invoice          Invoice object.
563
	 * @param float   $amount amount to validate.
564
	 */
565
	protected function validate_ipn_amount( $invoice, $amount ) {
566
		if ( number_format( $invoice->get_total(), 2, '.', '' ) !== number_format( $amount, 2, '.', '' ) ) {
567
            wpinv_record_gateway_error( 'IPN Error', "Amounts do not match: {$amount} instead of {$invoice->get_total()}" );
568
569
			/* translators: %s: Amount. */
570
			$invoice->update_status( 'wpi-processing', sprintf( __( 'Validation error: PayPal amounts do not match (gross %s).', 'invoicing' ), $amount ) );
571
			exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
572
		}
573
	}
574
575
	/**
576
	 * Verify receiver email from PayPal.
577
	 *
578
	 * @param WPInv_Invoice $invoice          Invoice object.
579
	 * @param string   $receiver_email Email to validate.
580
	 */
581
	protected function validate_ipn_receiver_email( $invoice, $receiver_email ) {
582
        $paypal_email = wpinv_get_option( 'paypal_email' );
583
584
		if ( strcasecmp( trim( $receiver_email ), trim( $paypal_email ) ) !== 0 ) {
0 ignored issues
show
Bug introduced by
It seems like $paypal_email can also be of type false; however, parameter $str of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

584
		if ( strcasecmp( trim( $receiver_email ), trim( /** @scrutinizer ignore-type */ $paypal_email ) ) !== 0 ) {
Loading history...
585
            wpinv_record_gateway_error( 'IPN Error', "IPN Response is for another account: {$receiver_email}. Your email is {$paypal_email}" );
586
587
			/* translators: %s: email address . */
588
			$invoice->update_status( 'wpi-processing', sprintf( __( 'Validation error: PayPal IPN response from a different email address (%s).', 'invoicing' ), $receiver_email ) );
589
			exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
590
        }
591
592
	}
593
594
    /**
595
	 * Handles one time payments.
596
	 *
597
	 * @param WPInv_Invoice $invoice  Invoice object.
598
	 * @param array    $posted Posted data.
599
	 */
600
	protected function ipn_txn_web_accept( $invoice, $posted ) {
601
602
        // Collect payment details
603
        $payment_status = strtolower( $posted['payment_status'] );
604
        $business_email = isset( $posted['business'] ) && is_email( $posted['business'] ) ? trim( $posted['business'] ) : trim( $posted['receiver_email'] );
605
606
        $this->validate_ipn_receiver_email( $invoice, $business_email );
607
        $this->validate_ipn_currency( $invoice, $posted['mc_currency'] );
608
609
        // Update the transaction id.
610
        if ( ! empty( $posted['txn_id'] ) ) {
611
            $invoice->set_transaction_id( wpinv_clean( $posted['txn_id'] ) );
612
            $invoice->save();
613
        }
614
615
        // Process a refund.
616
        if ( $payment_status == 'refunded' || $payment_status == 'reversed' ) {
617
618
            update_post_meta( $invoice->get_id(), 'refunded_remotely', 1 );
619
620
            if ( ! $invoice->is_refunded() ) {
621
                $invoice->update_status( 'wpi-refunded', $posted['reason_code'] );
622
            }
623
624
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
625
        }
626
627
        // Process payments.
628
        if ( $payment_status == 'completed' ) {
629
630
            if ( $invoice->is_paid() && 'wpi_processing' != $invoice->get_status() ) {
631
                wpinv_error_log( 'Aborting, Invoice #' . $invoice->get_number() . ' is already complete.' );
632
                exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
633
            }
634
635
            $this->validate_ipn_amount( $invoice, $posted['mc_gross'] );
636
637
            if ( 'completed' === $payment_status || 'pending' === $payment_status ) {
638
639
                $note = '';
640
641
                if ( ! empty( $posted['mc_fee'] ) ) {
642
                    $note = sprintf( __( 'PayPal Transaction Fee %s', 'invoicing' ), wpinv_clean( $posted['mc_fee'] ) );
643
                }
644
645
                $invoice->mark_paid( ( ! empty( $posted['txn_id'] ) ? wpinv_clean( $posted['txn_id'] ) : '' ), $note );
646
647
            } else {
648
649
                /* translators: %s: pending reason. */
650
                $invoice->update_status( 'wpi_processing', sprintf( __( 'Payment pending (%s).', 'invoicing' ), $posted['pending_reason'] ) );
651
652
            }
653
654
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
655
656
        }
657
658
        // Process failures.
659
660
        /* translators: %s: payment status. */
661
		$invoice->update_status( 'wpi-failed', sprintf( __( 'Payment %s via IPN.', 'invoicing' ), wpinv_clean( $posted['payment_status'] ) ) );
662
663
	}
664
665
	/**
666
	 * Handles one time payments.
667
	 *
668
	 * @param WPInv_Invoice $invoice  Invoice object.
669
	 * @param array    $posted Posted data.
670
	 */
671
	protected function ipn_txn_cart( $invoice, $posted ) {
672
		$this->ipn_txn_web_accept( $invoice, $posted );
673
    }
674
675
    /**
676
	 * Handles subscription sign ups.
677
	 *
678
	 * @param WPInv_Invoice $invoice  Invoice object.
679
	 * @param array    $posted Posted data.
680
	 */
681
	protected function ipn_txn_subscr_signup( $invoice, $posted ) {
682
683
        // Make sure the invoice has a subscription.
684
        $subscription = wpinv_get_subscription( $invoice );
685
686
        if ( empty( $subscription ) ) {
687
            wpinv_error_log( 'Aborting, Subscription for the invoice ' . $invoice->get_id() . ' not found' );
688
        }
689
690
        // Update the subscription ids.
691
        $subscription->update(
692
            array(
693
                'profile_id' => sanitize_text_field( $posted['subscr_id'] ),
694
            )
695
        );
696
697
        // Set the transaction id.
698
        if ( ! empty( $posted['txn_id'] ) ) {
699
            $invoice->set_transaction_id( $posted['txn_id'] );
700
        }
701
702
        // Update the payment status.
703
        $invoice->mark_paid();
704
705
        $invoice->add_note( sprintf( __( 'PayPal Subscription ID: %s', 'invoicing' ) , $posted['subscr_id'] ), false, false, true );
706
707
        // Use the action id as the subscription id.
708
        $duration = strtotime( $subscription->expiration ) - strtotime( $subscription->created );
709
        $subscription->update( 
710
            array(
711
                'status'     => 'trialling' == $subscription->status ? 'trialling' : 'active',
712
                'created'    => current_time( 'mysql' ),
713
                'expiration' => date( 'Y-m-d H:i:s', ( current_time( 'timestamp' ) + $duration ) ),
714
            )
715
        );
716
717
    }
718
719
    /**
720
	 * Handles subscription renewals.
721
	 *
722
	 * @param WPInv_Invoice $invoice  Invoice object.
723
	 * @param array    $posted Posted data.
724
	 */
725
	protected function ipn_txn_subscr_payment( $invoice, $posted ) {
726
727
        // Make sure the invoice has a subscription.
728
        $subscription = wpinv_get_subscription( $invoice );
729
730
        if ( empty( $subscription ) ) {
731
            wpinv_error_log( 'Aborting, Subscription for the invoice ' . $invoice->get_id() . ' not found' );
732
        }
733
734
        $this->validate_ipn_currency( $invoice, $posted['mc_currency'] );
735
736
        // Abort if the payment is already recorded.
737
        if ( wpinv_get_id_by_transaction_id( $posted['txn_id'] ) ) {
738
            return;
739
        }
740
741
        // Abort if this is the first payment.
742
        if ( date( 'Ynd', $subscription->created ) == date( 'Ynd', strtotime( $posted['payment_date'] ) ) ) {
743
            $invoice->set_transaction_id( $posted['txn_id'] );
744
            $invoice->save();
745
            return;
746
        }
747
748
        $args = array(
749
            'amount'         => $posted['mc_gross'],
750
            'transaction_id' => $posted['txn_id'],
751
            'gateway'        => $this->id,
752
        );
753
754
        $invoice = wpinv_get_invoice( $subscription->add_payment( $args ) );
755
756
        if ( empty( $invoice ) ) {
757
            return;
758
        }
759
760
        $invoice->add_note( wp_sprintf( __( 'PayPal Transaction ID: %s', 'invoicing' ) , $posted['txn_id'] ), false, false, true );
761
        $invoice->add_note( wp_sprintf( __( 'PayPal Subscription ID: %s', 'invoicing' ) , $posted['subscr_id'] ), false, false, true );
762
763
        $subscription->renew();
764
765
    }
766
767
    /**
768
	 * Handles subscription cancelations.
769
	 *
770
	 * @param WPInv_Invoice $invoice  Invoice object.
771
	 * @param array    $posted Posted data.
772
	 */
773
	protected function ipn_txn_subscr_cancel( $invoice, $posted ) {
774
775
        if ( $subscription = wpinv_get_subscription( $invoice ) ) {
776
            $subscription->cancel();
777
        }
778
779
    }
780
781
    /**
782
	 * Handles subscription completions.
783
	 *
784
	 * @param WPInv_Invoice $invoice  Invoice object.
785
	 * @param array    $posted Posted data.
786
	 */
787
	protected function ipn_txn_subscr_eot( $invoice, $posted ) {
788
789
        if ( $subscription = wpinv_get_subscription( $invoice ) ) {
790
            $subscription->complete();
791
        }
792
793
    }
794
795
    /**
796
	 * Handles subscription fails.
797
	 *
798
	 * @param WPInv_Invoice $invoice  Invoice object.
799
	 * @param array    $posted Posted data.
800
	 */
801
	protected function ipn_txn_subscr_failed( $invoice, $posted ) {
802
803
        if ( $subscription = wpinv_get_subscription( $invoice ) ) {
804
            $subscription->failing();
805
        }
806
807
    }
808
809
    /**
810
     * Displays a notice on the checkout page if sandbox is enabled.
811
     */
812
    public function sandbox_notice( $description, $gateway ) {
813
        if ( 'paypal' == $gateway && wpinv_is_test_mode( 'paypal' ) ) {
814
            $description .= '<br>' . sprintf(
815
                __( 'SANDBOX ENABLED. You can use sandbox testing accounts only. See the %sPayPal Sandbox Testing Guide%s for more details.', 'invoicing' ),
816
                '<a href="https://developer.paypal.com/docs/classic/lifecycle/ug_sandbox/">',
817
                '</a>'
818
            );
819
        }
820
        return $description;
821
822
    }
823
824
}
825