Passed
Push — master ( c5e685...72070d )
by Brian
15:09
created

GetPaid_Paypal_Gateway_IPN_Handler::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 2
c 1
b 1
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 1
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'] = 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 ( $this->gateway->is_sandbox( $invoice ) ) {
70
			wpinv_error_log( $posted, 'Invoice was processed in sandbox hence logging the posted data' );
71
		}
72
73
		if ( method_exists( $this, 'ipn_txn_' . $posted['txn_type'] ) ) {
74
			call_user_func( array( $this, 'ipn_txn_' . $posted['txn_type'] ), $invoice, $posted );
75
			wpinv_error_log( 'Done processing IPN' );
76
			wp_die( 'Processed', 200 );
77
		}
78
79
		wpinv_error_log( 'Aborting, Unsupported IPN type:' . $posted['txn_type'] );
80
		wp_die( 'Unsupported IPN type', 200 );
81
82
	}
83
84
	/**
85
	 * Retrieves IPN Invoice.
86
	 *
87
	 * @param array $posted
88
	 * @return WPInv_Invoice
89
	 */
90
	protected function get_ipn_invoice( $posted ) {
91
92
		wpinv_error_log( 'Retrieving PayPal IPN Response Invoice' );
93
94
		if ( ! empty( $posted['custom'] ) ) {
95
			$invoice = new WPInv_Invoice( $posted['custom'] );
96
97
			if ( $invoice->exists() ) {
98
				wpinv_error_log( 'Found invoice #' . $invoice->get_number() );
99
				return $invoice;
100
			}
101
102
		}
103
104
		wpinv_error_log( 'Could not retrieve the associated invoice.' );
105
		wp_die( 'Could not retrieve the associated invoice.', 500 );
106
	}
107
108
	/**
109
	 * Check PayPal IPN validity.
110
	 */
111
	protected function validate_ipn() {
112
113
		wpinv_error_log( 'Validating PayPal IPN response' );
114
115
		// Retrieve the associated invoice.
116
		$posted  = wp_unslash( $_POST );
117
		$invoice = $this->get_ipn_invoice( $posted );
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( "Currencies 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
			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( $posted, 'Processing subscription signup' );
0 ignored issues
show
Bug introduced by
$posted of type array is incompatible with the type string expected by parameter $log of wpinv_error_log(). ( Ignorable by Annotation )

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

298
		wpinv_error_log( /** @scrutinizer ignore-type */ $posted, 'Processing subscription signup' );
Loading history...
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
		$this->validate_ipn_amount( $invoice, $posted['mc_gross'] );
312
313
		// Activate the subscription.
314
		$duration = strtotime( $subscription->get_expiration() ) - strtotime( $subscription->get_date_created() );
315
		$subscription->set_date_created( current_time( 'mysql' ) );
316
		$subscription->set_expiration( date( 'Y-m-d H:i:s', ( current_time( 'timestamp' ) + $duration ) ) );
317
		$subscription->set_profile_id( sanitize_text_field( $posted['subscr_id'] ) );
318
		$subscription->activate();
319
320
		// Set the transaction id.
321
		if ( ! empty( $posted['txn_id'] ) ) {
322
			$invoice->set_transaction_id( $posted['txn_id'] );
323
		}
324
325
		// Update the payment status.
326
		$invoice->mark_paid();
327
328
		$invoice->add_note( sprintf( __( 'PayPal Subscription ID: %s', 'invoicing' ) , $posted['subscr_id'] ), false, false, true );
329
330
		wpinv_error_log( 'Subscription started.' );
331
	}
332
333
	/**
334
	 * Handles subscription renewals.
335
	 *
336
	 * @param WPInv_Invoice $invoice  Invoice object.
337
	 * @param array    $posted Posted data.
338
	 */
339
	protected function ipn_txn_subscr_payment( $invoice, $posted ) {
340
341
		// Make sure the invoice has a subscription.
342
		$subscription = wpinv_get_subscription( $invoice );
343
344
		if ( empty( $subscription ) ) {
345
			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...
346
		}
347
348
		// Abort if this is the first payment.
349
		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

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