Passed
Push — master ( 6374db...9c638d )
by Brian
05:27
created

GetPaid_Paypal_Gateway_IPN_Handler::verify_ipn()   A

Complexity

Conditions 6
Paths 16

Size

Total Lines 33
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 18
c 2
b 1
f 0
dl 0
loc 33
rs 9.0444
cc 6
nc 16
nop 0
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' );
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' );
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'] );
67
		wpinv_error_log( 'IPN Type:' . $posted['txn_type'] );
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' );
72
			wp_die( 'Processed', 200 );
73
		}
74
75
		wpinv_error_log( 'Aborting, Unsupported IPN type:' . $posted['txn_type'] );
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' );
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() );
95
				return $invoice;
96
			}
97
98
		}
99
100
		wpinv_error_log( 'Could not retrieve the associated invoice.' );
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' );
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 $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

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' );
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'] );
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.' );
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.' );
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".' );
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' );
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' );
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.' );
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' );
0 ignored issues
show
Bug introduced by
Are you sure the usage of wpinv_error_log('Abortin...et_id() . ' not found') 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 ( date( 'Ynd', $subscription->get_date_created() ) == date( 'Ynd', strtotime( $posted['payment_date'] ) ) ) {
0 ignored issues
show
Bug introduced by
$subscription->get_date_created() of type string is incompatible with the type integer expected by parameter $timestamp of date(). ( Ignorable by Annotation )

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

348
		if ( date( 'Ynd', /** @scrutinizer ignore-type */ $subscription->get_date_created() ) == date( 'Ynd', strtotime( $posted['payment_date'] ) ) ) {
Loading history...
349
			$invoice->set_transaction_id( sanitize_text_field( $posted['txn_id'] ) );
350
			$invoice->save();
351
			return;
352
		}
353
354
		wpinv_error_log( 'Processing subscription renewal payment for the invoice ' . $invoice->get_id() );
355
356
		// Abort if the payment is already recorded.
357
		if ( wpinv_get_id_by_transaction_id( $posted['txn_id'] ) ) {
358
			return wpinv_error_log( 'Aborting, Transaction ' . $posted['txn_id'] .' has already been processed' );
359
		}
360
361
		$args = array(
362
			'transaction_id' => $posted['txn_id'],
363
			'gateway'        => $this->id,
364
		);
365
366
		$invoice = wpinv_get_invoice( $subscription->add_payment( $args ) );
367
368
		if ( empty( $invoice ) ) {
369
			return;
370
		}
371
372
		$invoice->add_note( wp_sprintf( __( 'PayPal Transaction ID: %s', 'invoicing' ) , $posted['txn_id'] ), false, false, true );
373
		$invoice->add_note( wp_sprintf( __( 'PayPal Subscription ID: %s', 'invoicing' ) , $posted['subscr_id'] ), false, false, true );
374
375
		$subscription->renew();
376
		wpinv_error_log( 'Subscription renewed.' );
377
378
	}
379
380
	/**
381
	 * Handles subscription cancelations.
382
	 *
383
	 * @param WPInv_Invoice $invoice  Invoice object.
384
	 */
385
	protected function ipn_txn_subscr_cancel( $invoice ) {
386
387
		// Make sure the invoice has a subscription.
388
		$subscription = wpinv_get_subscription( $invoice );
389
390
		if ( empty( $subscription ) ) {
391
			return wpinv_error_log( 'Aborting, Subscription for the invoice ' . $invoice->get_id() . ' not found' );
0 ignored issues
show
Bug introduced by
Are you sure the usage of wpinv_error_log('Abortin...et_id() . ' not found') 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...
392
		}
393
394
		wpinv_error_log( 'Processing subscription cancellation for the invoice ' . $invoice->get_id() );
395
		$subscription->cancel();
396
		wpinv_error_log( 'Subscription cancelled.' );
397
398
	}
399
400
	/**
401
	 * Handles subscription completions.
402
	 *
403
	 * @param WPInv_Invoice $invoice  Invoice object.
404
	 * @param array    $posted Posted data.
405
	 */
406
	protected function ipn_txn_subscr_eot( $invoice ) {
407
408
		// Make sure the invoice has a subscription.
409
		$subscription = wpinv_get_subscription( $invoice );
410
411
		if ( empty( $subscription ) ) {
412
			return wpinv_error_log( 'Aborting, Subscription for the invoice ' . $invoice->get_id() . ' not found' );
0 ignored issues
show
Bug introduced by
Are you sure the usage of wpinv_error_log('Abortin...et_id() . ' not found') 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...
413
		}
414
415
		wpinv_error_log( 'Processing subscription end of life for the invoice ' . $invoice->get_id() );
416
		$subscription->complete();
417
		wpinv_error_log( 'Subscription completed.' );
418
419
	}
420
421
	/**
422
	 * Handles subscription fails.
423
	 *
424
	 * @param WPInv_Invoice $invoice  Invoice object.
425
	 * @param array    $posted Posted data.
426
	 */
427
	protected function ipn_txn_subscr_failed( $invoice ) {
428
429
		// Make sure the invoice has a subscription.
430
		$subscription = wpinv_get_subscription( $invoice );
431
432
		if ( empty( $subscription ) ) {
433
			return wpinv_error_log( 'Aborting, Subscription for the invoice ' . $invoice->get_id() . ' not found' );
0 ignored issues
show
Bug introduced by
Are you sure the usage of wpinv_error_log('Abortin...et_id() . ' not found') 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...
434
		}
435
436
		wpinv_error_log( 'Processing subscription payment failure for the invoice ' . $invoice->get_id() );
437
		$subscription->failing();
438
		wpinv_error_log( 'Subscription marked as failing.' );
439
440
	}
441
442
}
443