Passed
Push — master ( 825506...0612be )
by Brian
05:08
created

GetPaid_Paypal_Gateway_IPN_Handler   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 452
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 158
dl 0
loc 452
rs 4.08
c 2
b 1
f 0
wmc 59

15 Methods

Rating   Name   Duplication   Size   Complexity  
A ipn_txn_cart() 0 2 1
C ipn_txn_web_accept() 0 62 14
A validate_ipn_amount() 0 10 2
A validate_ipn_receiver_email() 0 13 2
A validate_ipn_currency() 0 11 2
A __construct() 0 3 1
A get_ipn_invoice() 0 16 3
A verify_ipn() 0 33 6
B validate_ipn() 0 41 7
A ipn_txn_subscr_signup() 0 34 5
A ipn_txn_subscr_failed() 0 12 2
B ipn_txn_subscr_payment() 0 44 8
A ipn_txn_subscr_cancel() 0 12 2
A ipn_txn_subscr_eot() 0 12 2
A ipn_txn_recurring_payment_suspended_due_to_max_failed_payment() 0 12 2

How to fix   Complexity   

Complex Class

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

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

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

1
<?php
2
/**
3
 * Paypal payment gateway IPN handler
4
 *
5
 */
6
7
defined( 'ABSPATH' ) || exit;
8
9
/**
10
 * Paypal Payment Gateway IPN handler class.
11
 *
12
 */
13
class GetPaid_Paypal_Gateway_IPN_Handler {
14
15
	/**
16
	 * Payment method id.
17
	 *
18
	 * @var string
19
	 */
20
	protected $id = 'paypal';
21
22
	/**
23
	 * Payment method object.
24
	 *
25
	 * @var GetPaid_Paypal_Gateway
26
	 */
27
	protected $gateway;
28
29
	/**
30
	 * Class constructor.
31
	 *
32
	 * @param GetPaid_Paypal_Gateway $gateway
33
	 */
34
	public function __construct( $gateway ) {
35
		$this->gateway = $gateway;
36
		$this->verify_ipn();
37
	}
38
39
	/**
40
	 * Processes ipns and marks payments as complete.
41
	 *
42
	 * @return void
43
	 */
44
	public function verify_ipn() {
45
46
		wpinv_error_log( 'GetPaid PayPal IPN Handler', false );
47
48
		// Validate the IPN.
49
		if ( empty( $_POST ) || ! $this->validate_ipn() ) {
50
			wp_die( 'PayPal IPN Request Failure', 500 );
51
		}
52
53
		// Process the IPN.
54
		$posted  = wp_unslash( $_POST );
55
		$invoice = $this->get_ipn_invoice( $posted );
56
57
		// Abort if it was not paid by our gateway.
58
		if ( $this->id != $invoice->get_gateway() ) {
59
			wpinv_error_log( 'Aborting, Invoice was not paid via PayPal', false );
60
			wp_die( 'Invoice not paid via PayPal', 500 );
61
		}
62
63
		$posted['payment_status'] = isset( $posted['payment_status'] ) ? sanitize_key( strtolower( $posted['payment_status'] ) ) : '';
64
		$posted['txn_type']       = sanitize_key( strtolower( $posted['txn_type'] ) );
65
66
		wpinv_error_log( 'Payment status:' . $posted['payment_status'], false );
67
		wpinv_error_log( 'IPN Type:' . $posted['txn_type'], false );
68
69
		if ( method_exists( $this, 'ipn_txn_' . $posted['txn_type'] ) ) {
70
			call_user_func( array( $this, 'ipn_txn_' . $posted['txn_type'] ), $invoice, $posted );
71
			wpinv_error_log( 'Done processing IPN', false );
72
			wp_die( 'Processed', 200 );
73
		}
74
75
		wpinv_error_log( 'Aborting, Unsupported IPN type:' . $posted['txn_type'], false );
76
		wp_die( 'Unsupported IPN type', 200 );
77
78
	}
79
80
	/**
81
	 * Retrieves IPN Invoice.
82
	 *
83
	 * @param array $posted
84
	 * @return WPInv_Invoice
85
	 */
86
	protected function get_ipn_invoice( $posted ) {
87
88
		wpinv_error_log( 'Retrieving PayPal IPN Response Invoice', false );
89
90
		if ( ! empty( $posted['custom'] ) ) {
91
			$invoice = new WPInv_Invoice( $posted['custom'] );
92
93
			if ( $invoice->exists() ) {
94
				wpinv_error_log( 'Found invoice #' . $invoice->get_number(), false );
95
				return $invoice;
96
			}
97
98
		}
99
100
		wpinv_error_log( 'Could not retrieve the associated invoice.', false );
101
		wp_die( 'Could not retrieve the associated invoice.', 500 );
102
	}
103
104
	/**
105
	 * Check PayPal IPN validity.
106
	 */
107
	protected function validate_ipn() {
108
109
		wpinv_error_log( 'Validating PayPal IPN response', false );
110
111
		// Retrieve the associated invoice.
112
		$posted  = wp_unslash( $_POST );
113
		$invoice = $this->get_ipn_invoice( $posted );
114
115
		if ( $this->gateway->is_sandbox( $invoice ) ) {
116
			wpinv_error_log( $posted, 'Invoice was processed in sandbox hence logging the posted data' );
117
		}
118
119
		// Validate the IPN.
120
		$posted['cmd'] = '_notify-validate';
121
122
		// Send back post vars to paypal.
123
		$params = array(
124
			'body'        => $posted,
125
			'timeout'     => 60,
126
			'httpversion' => '1.1',
127
			'compress'    => false,
128
			'decompress'  => false,
129
			'user-agent'  => 'GetPaid/' . WPINV_VERSION,
130
		);
131
132
		// Post back to get a response.
133
		$response = wp_safe_remote_post( $this->gateway->is_sandbox( $invoice ) ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr', $params );
134
135
		// Check to see if the request was valid.
136
		if ( ! is_wp_error( $response ) && $response['response']['code'] < 300 && strstr( $response['body'], 'VERIFIED' ) ) {
137
			wpinv_error_log( $response['body'], 'Received valid response from PayPal IPN' );
138
			return true;
139
		}
140
141
		if ( is_wp_error( $response ) ) {
142
			wpinv_error_log( $response->get_error_message(), 'Received invalid response from PayPal IPN' );
143
			return false;
144
		}
145
146
		wpinv_error_log( $response['body'], 'Received invalid response from PayPal IPN' );
147
		return false;
148
149
	}
150
151
	/**
152
	 * Check currency from IPN matches the invoice.
153
	 *
154
	 * @param WPInv_Invoice $invoice          Invoice object.
155
	 * @param string   $currency currency to validate.
156
	 */
157
	protected function validate_ipn_currency( $invoice, $currency ) {
158
159
		if ( strtolower( $invoice->get_currency() ) !== strtolower( $currency ) ) {
160
161
			/* translators: %s: currency code. */
162
			$invoice->update_status( 'wpi-processing', sprintf( __( 'Validation error: PayPal currencies do not match (code %s).', 'invoicing' ), $currency ) );
163
164
			wpinv_error_log( "Currencies do not match: {$currency} instead of {$invoice->get_currency()}", 'IPN Error', __FILE__, __LINE__, true );
165
		}
166
167
		wpinv_error_log( $currency, 'Validated IPN Currency' );
168
	}
169
170
	/**
171
	 * Check payment amount from IPN matches the invoice.
172
	 *
173
	 * @param WPInv_Invoice $invoice          Invoice object.
174
	 * @param float   $amount amount to validate.
175
	 */
176
	protected function validate_ipn_amount( $invoice, $amount ) {
177
		if ( number_format( $invoice->get_total(), 2, '.', '' ) !== number_format( $amount, 2, '.', '' ) ) {
178
179
			/* translators: %s: Amount. */
180
			$invoice->update_status( 'wpi-processing', sprintf( __( 'Validation error: PayPal amounts do not match (gross %s).', 'invoicing' ), $amount ) );
181
182
			wpinv_error_log( "Amounts do not match: {$amount} instead of {$invoice->get_total()}", 'IPN Error', __FILE__, __LINE__, true );
183
		}
184
185
		wpinv_error_log( $amount, 'Validated IPN Amount' );
186
	}
187
188
	/**
189
	 * Verify receiver email from PayPal.
190
	 *
191
	 * @param WPInv_Invoice $invoice          Invoice object.
192
	 * @param string   $receiver_email Email to validate.
193
	 */
194
	protected function validate_ipn_receiver_email( $invoice, $receiver_email ) {
195
		$paypal_email = wpinv_get_option( 'paypal_email' );
196
197
		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 $string 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

197
		if ( strcasecmp( trim( $receiver_email ), trim( /** @scrutinizer ignore-type */ $paypal_email ) ) !== 0 ) {
Loading history...
198
			wpinv_record_gateway_error( 'IPN Error', "IPN Response is for another account: {$receiver_email}. Your email is {$paypal_email}" );
199
200
			/* translators: %s: email address . */
201
			$invoice->update_status( 'wpi-processing', sprintf( __( 'Validation error: PayPal IPN response from a different email address (%s).', 'invoicing' ), $receiver_email ) );
202
203
			return wpinv_error_log( "IPN Response is for another account: {$receiver_email}. Your email is {$paypal_email}", 'IPN Error', __FILE__, __LINE__, true );
204
		}
205
206
		wpinv_error_log( 'Validated PayPal Email', false );
207
	}
208
209
	/**
210
	 * Handles one time payments.
211
	 *
212
	 * @param WPInv_Invoice $invoice  Invoice object.
213
	 * @param array    $posted Posted data.
214
	 */
215
	protected function ipn_txn_web_accept( $invoice, $posted ) {
216
217
		// Collect payment details
218
		$payment_status = strtolower( $posted['payment_status'] );
219
		$business_email = isset( $posted['business'] ) && is_email( $posted['business'] ) ? trim( $posted['business'] ) : trim( $posted['receiver_email'] );
220
221
		$this->validate_ipn_receiver_email( $invoice, $business_email );
222
		$this->validate_ipn_currency( $invoice, $posted['mc_currency'] );
223
224
		// Update the transaction id.
225
		if ( ! empty( $posted['txn_id'] ) ) {
226
			$invoice->set_transaction_id( wpinv_clean( $posted['txn_id'] ) );
227
			$invoice->save();
228
		}
229
230
		// Process a refund.
231
		if ( $payment_status == 'refunded' || $payment_status == 'reversed' ) {
232
233
			update_post_meta( $invoice->get_id(), 'refunded_remotely', 1 );
234
235
			if ( ! $invoice->is_refunded() ) {
236
				$invoice->update_status( 'wpi-refunded', $posted['reason_code'] );
237
			}
238
239
			return wpinv_error_log( $posted['reason_code'], false );
240
		}
241
242
		// Process payments.
243
		if ( $payment_status == 'completed' ) {
244
245
			if ( $invoice->is_paid() && 'wpi_processing' != $invoice->get_status() ) {
246
				return wpinv_error_log( 'Aborting, Invoice #' . $invoice->get_number() . ' is already paid.', false );
247
			}
248
249
			$this->validate_ipn_amount( $invoice, $posted['mc_gross'] );
250
251
			$note = '';
252
253
			if ( ! empty( $posted['mc_fee'] ) ) {
254
				$note = sprintf( __( 'PayPal Transaction Fee %.', 'invoicing' ), sanitize_text_field( $posted['mc_fee'] ) );
255
			}
256
257
			if ( ! empty( $posted['payer_status'] ) ) {
258
				$note = ' ' . sprintf( __( 'Buyer status %.', 'invoicing' ), sanitize_text_field( $posted['payer_status'] ) );
259
			}
260
261
			$invoice->mark_paid( ( ! empty( $posted['txn_id'] ) ? sanitize_text_field( $posted['txn_id'] ) : '' ), trim( $note ) );
262
			return wpinv_error_log( 'Invoice marked as paid.', false );
263
264
		}
265
266
		// Pending payments.
267
		if ( $payment_status == 'pending' ) {
268
269
			/* translators: %s: pending reason. */
270
			$invoice->update_status( 'wpi-onhold', sprintf( __( 'Payment pending (%s).', 'invoicing' ), $posted['pending_reason'] ) );
271
272
			return wpinv_error_log( 'Invoice marked as "payment held".', false );
273
		}
274
275
		/* translators: %s: payment status. */
276
		$invoice->update_status( 'wpi-failed', sprintf( __( 'Payment %s via IPN.', 'invoicing' ), sanitize_text_field( $posted['payment_status'] ) ) );
277
278
	}
279
280
	/**
281
	 * Handles one time payments.
282
	 *
283
	 * @param WPInv_Invoice $invoice  Invoice object.
284
	 * @param array    $posted Posted data.
285
	 */
286
	protected function ipn_txn_cart( $invoice, $posted ) {
287
		$this->ipn_txn_web_accept( $invoice, $posted );
288
	}
289
290
	/**
291
	 * Handles subscription sign ups.
292
	 *
293
	 * @param WPInv_Invoice $invoice  Invoice object.
294
	 * @param array    $posted Posted data.
295
	 */
296
	protected function ipn_txn_subscr_signup( $invoice, $posted ) {
297
298
		wpinv_error_log( 'Processing subscription signup', false );
299
300
		// Make sure the invoice has a subscription.
301
		$subscription = getpaid_get_invoice_subscription( $invoice );
302
303
		if ( empty( $subscription ) ) {
304
			return wpinv_error_log( 'Aborting, Subscription for the invoice ' . $invoice->get_id() . ' not found', false );
305
		}
306
307
		// Validate the IPN.
308
		$business_email = isset( $posted['business'] ) && is_email( $posted['business'] ) ? trim( $posted['business'] ) : trim( $posted['receiver_email'] );
309
		$this->validate_ipn_receiver_email( $invoice, $business_email );
310
		$this->validate_ipn_currency( $invoice, $posted['mc_currency'] );
311
312
		// Activate the subscription.
313
		$duration = strtotime( $subscription->get_expiration() ) - strtotime( $subscription->get_date_created() );
314
		$subscription->set_date_created( current_time( 'mysql' ) );
315
		$subscription->set_expiration( date( 'Y-m-d H:i:s', ( current_time( 'timestamp' ) + $duration ) ) );
316
		$subscription->set_profile_id( sanitize_text_field( $posted['subscr_id'] ) );
317
		$subscription->activate();
318
319
		// Set the transaction id.
320
		if ( ! empty( $posted['txn_id'] ) ) {
321
			$invoice->set_transaction_id( $posted['txn_id'] );
322
		}
323
324
		// Update the payment status.
325
		$invoice->mark_paid();
326
327
		$invoice->add_note( sprintf( __( 'PayPal Subscription ID: %s', 'invoicing' ) , $posted['subscr_id'] ), false, false, true );
328
329
		wpinv_error_log( 'Subscription started.', false );
330
	}
331
332
	/**
333
	 * Handles subscription renewals.
334
	 *
335
	 * @param WPInv_Invoice $invoice  Invoice object.
336
	 * @param array    $posted Posted data.
337
	 */
338
	protected function ipn_txn_subscr_payment( $invoice, $posted ) {
339
340
		// Make sure the invoice has a subscription.
341
		$subscription = wpinv_get_subscription( $invoice );
342
343
		if ( empty( $subscription ) ) {
344
			return wpinv_error_log( 'Aborting, Subscription for the invoice ' . $invoice->get_id() . ' not found', false );
0 ignored issues
show
Bug introduced by
Are you sure the usage of wpinv_error_log('Abortin... . ' not found', false) is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
345
		}
346
347
		// Abort if this is the first payment.
348
		if ( ( $invoice->is_paid() && date( 'Ynd', strtotime( $invoice->get_date_completed() ) ) == date( 'Ynd', strtotime( $posted['payment_date'] ) ) ) || date( 'Ynd', $subscription->get_time_created() ) == date( 'Ynd', strtotime( $posted['payment_date'] ) ) ) {
349
350
			if ( ! empty( $posted['txn_id'] ) ) {
351
				$invoice->set_transaction_id( sanitize_text_field( $posted['txn_id'] ) );
352
				$invoice->mark_paid();
353
				$invoice->save();
354
			}
355
356
			return;
357
		}
358
359
		wpinv_error_log( 'Processing subscription renewal payment for the invoice ' . $invoice->get_id(), false );
360
361
		// Abort if the payment is already recorded.
362
		if ( wpinv_get_id_by_transaction_id( $posted['txn_id'] ) ) {
363
			return wpinv_error_log( 'Aborting, Transaction ' . $posted['txn_id'] .' has already been processed', false );
364
		}
365
366
		$args = array(
367
			'transaction_id' => $posted['txn_id'],
368
			'gateway'        => $this->id,
369
		);
370
371
		$invoice = wpinv_get_invoice( $subscription->add_payment( $args ) );
372
373
		if ( empty( $invoice ) ) {
374
			return;
375
		}
376
377
		$invoice->add_note( wp_sprintf( __( 'PayPal Transaction ID: %s', 'invoicing' ) , $posted['txn_id'] ), false, false, true );
378
		$invoice->add_note( wp_sprintf( __( 'PayPal Subscription ID: %s', 'invoicing' ) , $posted['subscr_id'] ), false, false, true );
379
380
		$subscription->renew();
381
		wpinv_error_log( 'Subscription renewed.', false );
382
383
	}
384
385
	/**
386
	 * Handles subscription cancelations.
387
	 *
388
	 * @param WPInv_Invoice $invoice  Invoice object.
389
	 */
390
	protected function ipn_txn_subscr_cancel( $invoice ) {
391
392
		// Make sure the invoice has a subscription.
393
		$subscription = wpinv_get_subscription( $invoice );
394
395
		if ( empty( $subscription ) ) {
396
			return wpinv_error_log( 'Aborting, Subscription for the invoice ' . $invoice->get_id() . ' not found', false);
0 ignored issues
show
Bug introduced by
Are you sure the usage of wpinv_error_log('Abortin... . ' not found', false) is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
397
		}
398
399
		wpinv_error_log( 'Processing subscription cancellation for the invoice ' . $invoice->get_id(), false );
400
		$subscription->cancel();
401
		wpinv_error_log( 'Subscription cancelled.', false );
402
403
	}
404
405
	/**
406
	 * Handles subscription completions.
407
	 *
408
	 * @param WPInv_Invoice $invoice  Invoice object.
409
	 * @param array    $posted Posted data.
410
	 */
411
	protected function ipn_txn_subscr_eot( $invoice ) {
412
413
		// Make sure the invoice has a subscription.
414
		$subscription = wpinv_get_subscription( $invoice );
415
416
		if ( empty( $subscription ) ) {
417
			return wpinv_error_log( 'Aborting, Subscription for the invoice ' . $invoice->get_id() . ' not found', false );
0 ignored issues
show
Bug introduced by
Are you sure the usage of wpinv_error_log('Abortin... . ' not found', false) is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
418
		}
419
420
		wpinv_error_log( 'Processing subscription end of life for the invoice ' . $invoice->get_id(), false );
421
		$subscription->complete();
422
		wpinv_error_log( 'Subscription completed.', false );
423
424
	}
425
426
	/**
427
	 * Handles subscription fails.
428
	 *
429
	 * @param WPInv_Invoice $invoice  Invoice object.
430
	 * @param array    $posted Posted data.
431
	 */
432
	protected function ipn_txn_subscr_failed( $invoice ) {
433
434
		// Make sure the invoice has a subscription.
435
		$subscription = wpinv_get_subscription( $invoice );
436
437
		if ( empty( $subscription ) ) {
438
			return wpinv_error_log( 'Aborting, Subscription for the invoice ' . $invoice->get_id() . ' not found', false );
0 ignored issues
show
Bug introduced by
Are you sure the usage of wpinv_error_log('Abortin... . ' not found', false) is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
439
		}
440
441
		wpinv_error_log( 'Processing subscription payment failure for the invoice ' . $invoice->get_id(), false );
442
		$subscription->failing();
443
		wpinv_error_log( 'Subscription marked as failing.', false );
444
445
	}
446
447
	/**
448
	 * Handles subscription suspensions.
449
	 *
450
	 * @param WPInv_Invoice $invoice  Invoice object.
451
	 * @param array    $posted Posted data.
452
	 */
453
	protected function ipn_txn_recurring_payment_suspended_due_to_max_failed_payment( $invoice ) {
454
455
		// Make sure the invoice has a subscription.
456
		$subscription = wpinv_get_subscription( $invoice );
457
458
		if ( empty( $subscription ) ) {
459
			return wpinv_error_log( 'Aborting, Subscription for the invoice ' . $invoice->get_id() . ' not found', false );
0 ignored issues
show
Bug introduced by
Are you sure the usage of wpinv_error_log('Abortin... . ' not found', false) is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
460
		}
461
462
		wpinv_error_log( 'Processing subscription cancellation due to max failed payment for the invoice ' . $invoice->get_id(), false );
463
		$subscription->cancel();
464
		wpinv_error_log( 'Subscription cancelled.', false );
465
	}
466
467
}
468