Passed
Branch master (50908e)
by Stiofan
07:01
created

wpinv_process_paypal_web_accept_and_cart()   F

Complexity

Conditions 39
Paths 7418

Size

Total Lines 138
Code Lines 92

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 39
eloc 92
c 2
b 0
f 0
nc 7418
nop 2
dl 0
loc 138
rs 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
// Exit if accessed directly
3
if ( ! defined( 'ABSPATH' ) ) exit;
4
5
add_action( 'wpinv_paypal_cc_form', '__return_false' );
6
add_filter( 'wpinv_paypal_support_subscription', '__return_true' );
7
8
function wpinv_process_paypal_payment( $purchase_data ) {
9
    if( ! wp_verify_nonce( $purchase_data['gateway_nonce'], 'wpi-gateway' ) ) {
10
        wp_die( __( 'Nonce verification has failed', 'invoicing' ), __( 'Error', 'invoicing' ), array( 'response' => 403 ) );
11
    }
12
13
    // Collect payment data
14
    $payment_data = array(
15
        'price'         => $purchase_data['price'],
16
        'date'          => $purchase_data['date'],
17
        'user_email'    => $purchase_data['user_email'],
18
        'invoice_key'   => $purchase_data['invoice_key'],
19
        'currency'      => wpinv_get_currency(),
20
        'items'         => $purchase_data['items'],
21
        'user_info'     => $purchase_data['user_info'],
22
        'cart_details'  => $purchase_data['cart_details'],
23
        'gateway'       => 'paypal',
24
        'status'        => 'wpi-pending'
25
    );
26
27
    // Record the pending payment
28
    $invoice = wpinv_get_invoice( $purchase_data['invoice_id'] );
29
30
    // Check payment
31
    if ( ! $invoice ) {
32
        // Record the error
33
        wpinv_record_gateway_error( __( 'Payment Error', 'invoicing' ), sprintf( __( 'Payment creation failed before sending buyer to PayPal. Payment data: %s', 'invoicing' ), json_encode( $payment_data ) ), $invoice );
34
        // Problems? send back
35
        wpinv_send_back_to_checkout( '?payment-mode=' . $purchase_data['post_data']['wpi-gateway'] );
36
    } else {
37
        // Only send to PayPal if the pending payment is created successfully
38
        $listener_url = wpinv_get_ipn_url( 'paypal' );
39
40
        // Get the success url
41
        $return_url = add_query_arg( array(
42
                'payment-confirm' => 'paypal',
43
                'invoice-id' => $invoice->ID
44
            ), get_permalink( wpinv_get_option( 'success_page', false ) ) );
0 ignored issues
show
Bug introduced by
It seems like wpinv_get_option('success_page', false) can also be of type false; however, parameter $post of get_permalink() does only seem to accept WP_Post|integer, 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

44
            ), get_permalink( /** @scrutinizer ignore-type */ wpinv_get_option( 'success_page', false ) ) );
Loading history...
45
46
        // Get the PayPal redirect uri
47
        $paypal_redirect = trailingslashit( wpinv_get_paypal_redirect() ) . '?';
48
49
        // Setup PayPal arguments
50
        $paypal_args = array(
51
            'business'      => wpinv_get_option( 'paypal_email', false ),
52
            'email'         => $invoice->get_email(),
53
            'first_name'    => $invoice->get_first_name(),
54
            'last_name'     => $invoice->get_last_name(),
55
            'invoice'       => $invoice->get_key(),
56
            'no_shipping'   => '1',
57
            'shipping'      => '0',
58
            'no_note'       => '1',
59
            'currency_code' => wpinv_get_currency(),
60
            'charset'       => get_bloginfo( 'charset' ),
61
            'custom'        => $invoice->ID,
62
            'rm'            => '2',
63
            'return'        => $return_url,
64
            'cancel_return' => wpinv_get_failed_transaction_uri( '?invoice-id=' . $invoice->ID ),
65
            'notify_url'    => $listener_url,
66
            'cbt'           => get_bloginfo( 'name' ),
67
            'bn'            => 'WPInvoicing_SP',
68
            'lc'            => 'US', // this will force paypal site to english
69
            'landing_page'  => apply_filters( 'wpinv_paypal_standard_landing_page', 'billing', $invoice ), // 'login' or 'billing'. login - PayPal account login, billing - Non-PayPal account.
70
        );
71
72
        $paypal_args['address1'] = $invoice->get_address();
73
        $paypal_args['city']     = $invoice->user_info['city'];
74
        $paypal_args['state']    = $invoice->user_info['state'];
75
        $paypal_args['country']  = $invoice->user_info['country'];
76
        $paypal_args['zip']      = $invoice->user_info['zip'];
77
78
        $paypal_extra_args = array(
79
            'cmd'    => '_cart',
80
            'upload' => '1'
81
        );
82
83
        $paypal_args = array_merge( $paypal_extra_args, $paypal_args );
84
85
        // Add cart items
86
        $i = 1;
87
        if( is_array( $purchase_data['cart_details'] ) && ! empty( $purchase_data['cart_details'] ) ) {
88
            foreach ( $purchase_data['cart_details'] as $item ) {
89
                $item['quantity'] = $item['quantity'] > 0 ? $item['quantity'] : 1;
90
                $item_amount = wpinv_sanitize_amount( $item['subtotal'] / $item['quantity'], 2 );
91
92
                if ( $item_amount <= 0 ) {
93
                    $item_amount = 0;
94
                }
95
96
                $paypal_args['item_number_' . $i ]      = $item['id'];
97
                $paypal_args['item_name_' . $i ]        = stripslashes_deep( html_entity_decode( wpinv_get_cart_item_name( $item ), ENT_COMPAT, 'UTF-8' ) );
98
                $paypal_args['quantity_' . $i ]         = $item['quantity'];
99
                $paypal_args['amount_' . $i ]           = $item_amount;
100
                $paypal_args['discount_amount_' . $i ]  = wpinv_sanitize_amount( $item['discount'], 2 );
101
102
                $i++;
103
            }
104
        }
105
106
        // Add taxes to the cart
107
        if ( wpinv_use_taxes() ) {
108
            $paypal_args['tax_cart'] = wpinv_sanitize_amount( (float)$invoice->get_tax(), 2 );
109
        }
110
111
        $paypal_args = apply_filters( 'wpinv_paypal_args', $paypal_args, $purchase_data, $invoice );
112
113
        // Build query
114
        $paypal_redirect .= http_build_query( $paypal_args );
115
116
        // Fix for some sites that encode the entities
117
        $paypal_redirect = str_replace( '&amp;', '&', $paypal_redirect );
118
119
        // Get rid of cart contents
120
        wpinv_empty_cart();
121
        
122
        // Redirect to PayPal
123
        wp_redirect( $paypal_redirect );
124
        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...
125
    }
126
}
127
add_action( 'wpinv_gateway_paypal', 'wpinv_process_paypal_payment' );
128
129
function wpinv_get_paypal_recurring_args( $paypal_args, $purchase_data, $invoice ) {
130
    if ( $invoice->is_recurring() && $item_id = $invoice->get_recurring() ) {
131
        $item   = new WPInv_Item( $item_id );
132
        
133
        if ( empty( $item ) ) {
134
            return $paypal_args;
135
        }
136
137
        $period             = $item->get_recurring_period();
138
        $interval           = $item->get_recurring_interval();
139
        $bill_times         = (int)$item->get_recurring_limit();
140
        
141
        $initial_amount     = wpinv_sanitize_amount( $invoice->get_total(), 2 );
142
        $recurring_amount   = wpinv_sanitize_amount( $invoice->get_recurring_details( 'total' ), 2 );
143
        
144
        $paypal_args['cmd'] = '_xclick-subscriptions';
145
        $paypal_args['sra'] = '1';
146
        $paypal_args['src'] = '1';
147
        
148
        // Set item description
149
        $item_name                  = sprintf( '[%s] %s', $invoice->get_number(), wpinv_get_cart_item_name( array( 'id' => $item->ID ) ) );
150
        $paypal_args['item_name']   = stripslashes_deep( html_entity_decode( $item_name, ENT_COMPAT, 'UTF-8' ) );
151
        
152
        if ( $invoice->is_free_trial() && $item->has_free_trial() ) {
153
            $paypal_args['a1']  = $initial_amount;
154
            $paypal_args['p1']  = $item->get_trial_interval();
155
            $paypal_args['t1']  = $item->get_trial_period();
156
            
157
            // Set the recurring amount
158
            $paypal_args['a3']  = $recurring_amount;
159
        } else if ( $initial_amount != $recurring_amount && $bill_times != 1 ) {
160
            $paypal_args['a1']  = $initial_amount;
161
            $paypal_args['p1']  = $interval;
162
            $paypal_args['t1']  = $period;
163
            
164
            // Set the recurring amount
165
            $paypal_args['a3']  = $recurring_amount;
166
            
167
            if ( $bill_times > 1 ) {
168
                $bill_times--;
169
            }
170
        } else {
171
            $paypal_args['a3']  = $initial_amount;
172
        }
173
        
174
        $paypal_args['p3']  = $interval;
175
        $paypal_args['t3']  = $period;
176
        
177
        if ( $bill_times > 1 ) {
178
            // Make sure it's not over the max of 52
179
            $paypal_args['srt'] = ( $bill_times <= 52 ? absint( $bill_times ) : 52 );
180
        }
181
                
182
        // Remove cart items
183
        $i = 1;
184
        if( is_array( $purchase_data['cart_details'] ) && ! empty( $purchase_data['cart_details'] ) ) {
185
            foreach ( $purchase_data['cart_details'] as $item ) {                
186
                if ( isset( $paypal_args['item_number_' . $i] ) ) {
187
                    unset( $paypal_args['item_number_' . $i] );
188
                }
189
                if ( isset( $paypal_args['item_name_' . $i] ) ) {
190
                    unset( $paypal_args['item_name_' . $i] );
191
                }
192
                if ( isset( $paypal_args['quantity_' . $i] ) ) {
193
                    unset( $paypal_args['quantity_' . $i] );
194
                }
195
                if ( isset( $paypal_args['amount_' . $i] ) ) {
196
                    unset( $paypal_args['amount_' . $i] );
197
                }
198
                if ( isset( $paypal_args['discount_amount_' . $i] ) ) {
199
                    unset( $paypal_args['discount_amount_' . $i] );
200
                }
201
202
                $i++;
203
            }
204
        }
205
        
206
        if ( isset( $paypal_args['tax_cart'] ) ) {
207
            unset( $paypal_args['tax_cart'] );
208
        }
209
                
210
        if ( isset( $paypal_args['upload'] ) ) {
211
            unset( $paypal_args['upload'] );
212
        }
213
        
214
        $paypal_args = apply_filters( 'wpinv_paypal_recurring_args', $paypal_args, $purchase_data, $invoice );
215
    }
216
    
217
    return $paypal_args;
218
}
219
add_filter( 'wpinv_paypal_args', 'wpinv_get_paypal_recurring_args', 10, 3 );
220
221
function wpinv_process_paypal_ipn() {
222
	// Check the request method is POST
223
	if ( isset( $_SERVER['REQUEST_METHOD'] ) && $_SERVER['REQUEST_METHOD'] != 'POST' ) {
224
		return;
225
	}
226
227
	// Set initial post data to empty string
228
	$post_data = '';
229
230
	// Fallback just in case post_max_size is lower than needed
231
	if ( ini_get( 'allow_url_fopen' ) ) {
232
		$post_data = file_get_contents( 'php://input' );
233
	} else {
234
		// If allow_url_fopen is not enabled, then make sure that post_max_size is large enough
235
		ini_set( 'post_max_size', '12M' );
236
	}
237
	// Start the encoded data collection with notification command
238
	$encoded_data = 'cmd=_notify-validate';
239
240
	// Get current arg separator
241
	$arg_separator = wpinv_get_php_arg_separator_output();
242
243
	// Verify there is a post_data
244
	if ( $post_data || strlen( $post_data ) > 0 ) {
245
		// Append the data
246
		$encoded_data .= $arg_separator.$post_data;
247
	} else {
248
		// Check if POST is empty
249
		if ( empty( $_POST ) ) {
250
			// Nothing to do
251
			return;
252
		} else {
253
			// Loop through each POST
254
			foreach ( $_POST as $key => $value ) {
255
				// Encode the value and append the data
256
				$encoded_data .= $arg_separator."$key=" . urlencode( $value );
257
			}
258
		}
259
	}
260
261
	// Convert collected post data to an array
262
	parse_str( $encoded_data, $encoded_data_array );
0 ignored issues
show
Security Variable Injection introduced by
$encoded_data can contain request data and is used in variable name context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_POST, and $_POST is assigned to $value
    in includes/gateways/paypal.php on line 254
  2. Data is passed through urlencode(), and $arg_separator . $key.'=' . urlencode($value) is assigned to $encoded_data
    in includes/gateways/paypal.php on line 256

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...
263
264
	foreach ( $encoded_data_array as $key => $value ) {
265
		if ( false !== strpos( $key, 'amp;' ) ) {
266
			$new_key = str_replace( '&amp;', '&', $key );
267
			$new_key = str_replace( 'amp;', '&' , $new_key );
268
269
			unset( $encoded_data_array[ $key ] );
270
			$encoded_data_array[ $new_key ] = $value;
271
		}
272
	}
273
274
	// Get the PayPal redirect uri
275
	$paypal_redirect = wpinv_get_paypal_redirect( true );
0 ignored issues
show
Unused Code introduced by
The assignment to $paypal_redirect is dead and can be removed.
Loading history...
276
277
	if ( !wpinv_get_option( 'disable_paypal_verification', false ) ) {
278
		// Validate the IPN
279
280
		$remote_post_vars      = array(
281
			'method'           => 'POST',
282
			'timeout'          => 45,
283
			'redirection'      => 5,
284
			'httpversion'      => '1.1',
285
			'blocking'         => true,
286
			'headers'          => array(
287
				'host'         => 'www.paypal.com',
288
				'connection'   => 'close',
289
				'content-type' => 'application/x-www-form-urlencoded',
290
				'post'         => '/cgi-bin/webscr HTTP/1.1',
291
292
			),
293
			'sslverify'        => false,
294
			'body'             => $encoded_data_array
295
		);
296
297
		// Get response
298
		$api_response = wp_remote_post( wpinv_get_paypal_redirect(), $remote_post_vars );
299
300
		if ( is_wp_error( $api_response ) ) {
301
			wpinv_record_gateway_error( __( 'IPN Error', 'invoicing' ), sprintf( __( 'Invalid IPN verification response. IPN data: %s', 'invoicing' ), json_encode( $api_response ) ) );
302
			return; // Something went wrong
303
		}
304
305
		if ( $api_response['body'] !== 'VERIFIED' && wpinv_get_option( 'disable_paypal_verification', false ) ) {
306
			wpinv_record_gateway_error( __( 'IPN Error', 'invoicing' ), sprintf( __( 'Invalid IPN verification response. IPN data: %s', 'invoicing' ), json_encode( $api_response ) ) );
307
			return; // Response not okay
308
		}
309
	}
310
311
	// Check if $post_data_array has been populated
312
	if ( !is_array( $encoded_data_array ) && !empty( $encoded_data_array ) )
313
		return;
314
315
	$defaults = array(
316
		'txn_type'       => '',
317
		'payment_status' => ''
318
	);
319
320
	$encoded_data_array = wp_parse_args( $encoded_data_array, $defaults );
0 ignored issues
show
Security Variable Injection introduced by
$encoded_data_array can contain request data and is used in variable name context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_POST, and $_POST is assigned to $value
    in includes/gateways/paypal.php on line 254
  2. Data is passed through urlencode(), and $arg_separator . $key.'=' . urlencode($value) is assigned to $encoded_data
    in includes/gateways/paypal.php on line 256
  3. Assigned via parse_str()
    in includes/gateways/paypal.php on line 262

Used in variable context

  1. wp_parse_args() is called
    in includes/gateways/paypal.php on line 320
  2. Enters via parameter $args
    in wordpress/wp-includes/functions.php on line 4294
  3. wp_parse_str() is called
    in wordpress/wp-includes/functions.php on line 4300
  4. Enters via parameter $string
    in wordpress/wp-includes/formatting.php on line 4865
  5. parse_str() is called
    in wordpress/wp-includes/formatting.php on line 4866

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...
321
322
	$invoice_id = isset( $encoded_data_array['custom'] ) ? absint( $encoded_data_array['custom'] ) : 0;
323
    
324
	wpinv_error_log( $encoded_data_array['txn_type'], 'PayPal txn_type', __FILE__, __LINE__ );
325
	wpinv_error_log( $encoded_data_array, 'PayPal IPN response', __FILE__, __LINE__ );
326
327
	if ( has_action( 'wpinv_paypal_' . $encoded_data_array['txn_type'] ) ) {
328
		// Allow PayPal IPN types to be processed separately
329
		do_action( 'wpinv_paypal_' . $encoded_data_array['txn_type'], $encoded_data_array, $invoice_id );
330
	} else {
331
		// Fallback to web accept just in case the txn_type isn't present
332
		do_action( 'wpinv_paypal_web_accept', $encoded_data_array, $invoice_id );
333
	}
334
	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...
335
}
336
add_action( 'wpinv_verify_paypal_ipn', 'wpinv_process_paypal_ipn' );
337
338
function wpinv_process_paypal_web_accept_and_cart( $data, $invoice_id ) {
339
	if ( $data['txn_type'] != 'web_accept' && $data['txn_type'] != 'cart' && $data['payment_status'] != 'Refunded' ) {
340
		return;
341
	}
342
343
	if( empty( $invoice_id ) ) {
344
		return;
345
	}
346
347
	// Collect payment details
348
	$purchase_key   = isset( $data['invoice'] ) ? $data['invoice'] : $data['item_number'];
349
	$paypal_amount  = $data['mc_gross'];
350
	$payment_status = strtolower( $data['payment_status'] );
351
	$currency_code  = strtolower( $data['mc_currency'] );
352
	$business_email = isset( $data['business'] ) && is_email( $data['business'] ) ? trim( $data['business'] ) : trim( $data['receiver_email'] );
353
	$payment_meta   = wpinv_get_invoice_meta( $invoice_id );
354
355
	if ( wpinv_get_payment_gateway( $invoice_id ) != 'paypal' ) {
356
		return; // this isn't a PayPal standard IPN
357
	}
358
359
	// Verify payment recipient
360
	if ( strcasecmp( $business_email, trim( wpinv_get_option( 'paypal_email', false ) ) ) != 0 ) {
0 ignored issues
show
Bug introduced by
It seems like wpinv_get_option('paypal_email', false) 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

360
	if ( strcasecmp( $business_email, trim( /** @scrutinizer ignore-type */ wpinv_get_option( 'paypal_email', false ) ) ) != 0 ) {
Loading history...
361
		wpinv_record_gateway_error( __( 'IPN Error', 'invoicing' ), sprintf( __( 'Invalid business email in IPN response. IPN data: %s', 'invoicing' ), json_encode( $data ) ), $invoice_id );
362
		wpinv_update_payment_status( $invoice_id, 'wpi-failed' );
363
		wpinv_insert_payment_note( $invoice_id, __( 'Payment failed due to invalid PayPal business email.', 'invoicing' ), '', '', true );
364
		return;
365
	}
366
367
	// Verify payment currency
368
	if ( $currency_code != strtolower( $payment_meta['currency'] ) ) {
369
		wpinv_record_gateway_error( __( 'IPN Error', 'invoicing' ), sprintf( __( 'Invalid currency in IPN response. IPN data: %s', 'invoicing' ), json_encode( $data ) ), $invoice_id );
370
		wpinv_update_payment_status( $invoice_id, 'wpi-failed' );
371
		wpinv_insert_payment_note( $invoice_id, __( 'Payment failed due to invalid currency in PayPal IPN.', 'invoicing' ), '', '', true );
372
		return;
373
	}
374
375
	if ( !wpinv_get_payment_user_email( $invoice_id ) ) {
376
		// This runs when a Buy Now purchase was made. It bypasses checkout so no personal info is collected until PayPal
377
		// No email associated with purchase, so store from PayPal
378
		wpinv_update_invoice_meta( $invoice_id, '_wpinv_email', $data['payer_email'] );
379
380
		// Setup and store the customer's details
381
		$user_info = array(
382
			'user_id'    => '-1',
383
			'email'      => sanitize_text_field( $data['payer_email'] ),
384
			'first_name' => sanitize_text_field( $data['first_name'] ),
385
			'last_name'  => sanitize_text_field( $data['last_name'] ),
386
			'discount'   => '',
387
		);
388
		$user_info['address'] = ! empty( $data['address_street']       ) ? sanitize_text_field( $data['address_street'] )       : false;
389
		$user_info['city']    = ! empty( $data['address_city']         ) ? sanitize_text_field( $data['address_city'] )         : false;
390
		$user_info['state']   = ! empty( $data['address_state']        ) ? sanitize_text_field( $data['address_state'] )        : false;
391
		$user_info['country'] = ! empty( $data['address_country_code'] ) ? sanitize_text_field( $data['address_country_code'] ) : false;
392
		$user_info['zip']     = ! empty( $data['address_zip']          ) ? sanitize_text_field( $data['address_zip'] )          : false;
393
394
		$payment_meta['user_info'] = $user_info;
395
		wpinv_update_invoice_meta( $invoice_id, '_wpinv_payment_meta', $payment_meta );
396
	}
397
398
	if ( $payment_status == 'refunded' || $payment_status == 'reversed' ) {
399
		// Process a refund
400
		wpinv_process_paypal_refund( $data, $invoice_id );
401
	} else {
402
		if ( get_post_status( $invoice_id ) == 'publish' ) {
403
			return; // Only paid payments once
404
		}
405
406
		// Retrieve the total purchase amount (before PayPal)
407
		$payment_amount = wpinv_payment_total( $invoice_id );
408
409
		if ( number_format( (float) $paypal_amount, 2 ) < number_format( (float) $payment_amount, 2 ) ) {
410
			// The prices don't match
411
			wpinv_record_gateway_error( __( 'IPN Error', 'invoicing' ), sprintf( __( 'Invalid payment amount in IPN response. IPN data: %s', 'invoicing' ), json_encode( $data ) ), $invoice_id );
412
			wpinv_update_payment_status( $invoice_id, 'wpi-failed' );
413
			wpinv_insert_payment_note( $invoice_id, __( 'Payment failed due to invalid amount in PayPal IPN.', 'invoicing' ), '', '', true );
414
			return;
415
		}
416
		if ( $purchase_key != wpinv_get_payment_key( $invoice_id ) ) {
417
			// Purchase keys don't match
418
			wpinv_record_gateway_error( __( 'IPN Error', 'invoicing' ), sprintf( __( 'Invalid purchase key in IPN response. IPN data: %s', 'invoicing' ), json_encode( $data ) ), $invoice_id );
419
			wpinv_update_payment_status( $invoice_id, 'wpi-failed' );
420
			wpinv_insert_payment_note( $invoice_id, __( 'Payment failed due to invalid purchase key in PayPal IPN.', 'invoicing' ), '', '', true );
421
			return;
422
		}
423
424
		if ( 'complete' == $payment_status || 'completed' == $payment_status || 'processed' == $payment_status || wpinv_is_test_mode( 'paypal' ) ) {
425
			wpinv_insert_payment_note( $invoice_id, sprintf( __( 'PayPal Transaction ID: %s', 'invoicing' ) , $data['txn_id'] ), '', '', true );
426
			wpinv_set_payment_transaction_id( $invoice_id, $data['txn_id'] );
427
			wpinv_update_payment_status( $invoice_id, 'publish' );
428
		} else if ( 'pending' == $payment_status && isset( $data['pending_reason'] ) ) {
429
			// Look for possible pending reasons, such as an echeck
430
			$note = '';
431
432
			switch( strtolower( $data['pending_reason'] ) ) {
433
				case 'echeck' :
434
					$note = __( 'Payment made via eCheck and will clear automatically in 5-8 days', 'invoicing' );
435
					break;
436
				
437
                case 'address' :
438
					$note = __( 'Payment requires a confirmed customer address and must be accepted manually through PayPal', 'invoicing' );
439
					break;
440
				
441
                case 'intl' :
442
					$note = __( 'Payment must be accepted manually through PayPal due to international account regulations', 'invoicing' );
443
					break;
444
				
445
                case 'multi-currency' :
446
					$note = __( 'Payment received in non-shop currency and must be accepted manually through PayPal', 'invoicing' );
447
					break;
448
				
449
                case 'paymentreview' :
450
                case 'regulatory_review' :
451
					$note = __( 'Payment is being reviewed by PayPal staff as high-risk or in possible violation of government regulations', 'invoicing' );
452
					break;
453
				
454
                case 'unilateral' :
455
					$note = __( 'Payment was sent to non-confirmed or non-registered email address.', 'invoicing' );
456
					break;
457
				
458
                case 'upgrade' :
459
					$note = __( 'PayPal account must be upgraded before this payment can be accepted', 'invoicing' );
460
					break;
461
				
462
                case 'verify' :
463
					$note = __( 'PayPal account is not verified. Verify account in order to accept this payment', 'invoicing' );
464
					break;
465
466
				case 'other' :
467
					$note = __( 'Payment is pending for unknown reasons. Contact PayPal support for assistance', 'invoicing' );
468
					break;
469
			}
470
471
			if ( ! empty( $note ) ) {
472
				wpinv_insert_payment_note( $invoice_id, $note, '', '', true );
473
			}
474
		} else {
475
			wpinv_insert_payment_note( $invoice_id, wp_sprintf( __( 'PayPal IPN has been received with invalid payment status: %s', 'invoicing' ), $payment_status ), '', '', true );
476
		}
477
	}
478
}
479
add_action( 'wpinv_paypal_web_accept', 'wpinv_process_paypal_web_accept_and_cart', 10, 2 );
480
481
// Process PayPal subscription sign ups
482
add_action( 'wpinv_paypal_subscr_signup', 'wpinv_process_paypal_subscr_signup' );
483
484
// Process PayPal subscription payments
485
add_action( 'wpinv_paypal_subscr_payment', 'wpinv_process_paypal_subscr_payment' );
486
487
// Process PayPal subscription cancellations
488
add_action( 'wpinv_paypal_subscr_cancel', 'wpinv_process_paypal_subscr_cancel' );
489
490
// Process PayPal subscription end of term notices
491
add_action( 'wpinv_paypal_subscr_eot', 'wpinv_process_paypal_subscr_eot' );
492
493
// Process PayPal payment failed
494
add_action( 'wpinv_paypal_subscr_failed', 'wpinv_process_paypal_subscr_failed' );
495
496
497
/**
498
 * Process the subscription started IPN.
499
 */
500
function wpinv_process_paypal_subscr_signup( $ipn_data ) {
501
    $parent_invoice_id = absint( $ipn_data['custom'] );
502
    if( empty( $parent_invoice_id ) ) {
503
        return;
504
    }
505
506
    $invoice = wpinv_get_invoice( $parent_invoice_id );
507
    if ( empty( $invoice ) ) {
508
        return;
509
    }
510
511
    if ( $invoice->is_free_trial() && !empty( $ipn_data['invoice'] ) ) {
512
        wpinv_insert_payment_note( $parent_invoice_id, sprintf( __( 'PayPal Invoice ID: %s', 'invoicing' ) , $ipn_data['invoice'] ), '', '', true);
513
        if ( !empty( $ipn_data['txn_id'] ) ) {
514
            wpinv_set_payment_transaction_id( $parent_invoice_id, $ipn_data['txn_id'] );
515
        }
516
    }
517
518
    $subscription = wpinv_get_paypal_subscription( $ipn_data );
519
    if ( false === $subscription ) {
520
        return;
521
    }
522
    
523
    wpinv_update_payment_status( $parent_invoice_id, 'publish' );
524
    sleep(1);
525
    wpinv_insert_payment_note( $parent_invoice_id, sprintf( __( 'PayPal Subscription ID: %s', 'invoicing' ) , $ipn_data['subscr_id'] ), '', '', true );
526
    update_post_meta($parent_invoice_id,'_wpinv_subscr_profile_id', $ipn_data['subscr_id']);
527
528
    $status     = 'trialling' == $subscription->status ? 'trialling' : 'active';
529
    $diff_days  = absint( ( ( strtotime( $subscription->expiration ) - strtotime( $subscription->created ) ) / DAY_IN_SECONDS ) );
530
    $created    = date_i18n( 'Y-m-d H:i:s' );
531
    $expiration = date_i18n( 'Y-m-d 23:59:59', ( strtotime( $created ) + ( $diff_days * DAY_IN_SECONDS ) ) );
532
533
    // Retrieve pending subscription from database and update it's status to active and set proper profile ID
534
    $subscription->update( array( 'profile_id' => $ipn_data['subscr_id'], 'status' => $status, 'created' => $created, 'expiration' => $expiration ) );
535
}
536
537
/**
538
 * Process the subscription payment received IPN.
539
 */
540
function wpinv_process_paypal_subscr_payment( $ipn_data ) {
541
    $parent_invoice_id = absint( $ipn_data['custom'] );
542
543
    $parent_invoice = wpinv_get_invoice( $parent_invoice_id );
544
    if ( empty( $parent_invoice ) ) {
545
        return;
546
    }
547
548
    $subscription = wpinv_get_paypal_subscription( $ipn_data );
549
    if ( false === $subscription ) {
550
        return;
551
    }
552
553
    $transaction_id = wpinv_get_payment_transaction_id( $parent_invoice_id );
554
    $times_billed   = $subscription->get_times_billed();
555
    $signup_date    = strtotime( $subscription->created );
556
    $today          = date( 'Ynd', $signup_date ) == date( 'Ynd', strtotime( $ipn_data['payment_date'] ) );
557
558
    // Look to see if payment is same day as signup and we have set the transaction ID on the parent payment yet.
559
    if ( (empty($times_billed) || $today) && ( !$transaction_id || $transaction_id == $parent_invoice_id ) ) {
560
        wpinv_update_payment_status( $parent_invoice_id, 'publish' );
561
        sleep(1);
562
        
563
        // This is the very first payment
564
        wpinv_set_payment_transaction_id( $parent_invoice_id, $ipn_data['txn_id'] );
565
        wpinv_insert_payment_note( $parent_invoice_id, sprintf( __( 'PayPal Transaction ID: %s', 'invoicing' ) , $ipn_data['txn_id'] ), '', '', true );
566
        return;
567
    }
568
569
    if ( wpinv_get_id_by_transaction_id( $ipn_data['txn_id'] ) ) {
570
        return; // Payment already recorded
571
    }
572
573
    $currency_code = strtolower( $ipn_data['mc_currency'] );
574
575
    // verify details
576
    if ( $currency_code != strtolower( wpinv_get_currency() ) ) {
577
        // the currency code is invalid
578
        wpinv_record_gateway_error( __( 'IPN Error', 'invoicing' ), sprintf( __( 'Invalid currency in IPN response. IPN data: ', 'invoicing' ), json_encode( $ipn_data ) ) );
579
        return;
580
    }
581
582
    $args = array(
583
        'amount'         => $ipn_data['mc_gross'],
584
        'transaction_id' => $ipn_data['txn_id'],
585
        'gateway'        => 'paypal'
586
    );
587
    
588
    $invoice_id = $subscription->add_payment( $args );
589
590
    if ( $invoice_id > 0 ) {
591
        wpinv_insert_payment_note( $invoice_id, wp_sprintf( __( 'PayPal Transaction ID: %s', 'invoicing' ) , $ipn_data['txn_id'] ), '', '', true );
592
        wpinv_insert_payment_note( $invoice_id, wp_sprintf( __( 'PayPal Subscription ID: %s', 'invoicing' ) , $ipn_data['subscr_id'] ), '', '', true );
593
594
        $subscription->renew();
595
    }
596
}
597
598
/**
599
 * Process the subscription canceled IPN.
600
 */
601
function wpinv_process_paypal_subscr_cancel( $ipn_data ) {
602
    $subscription = wpinv_get_paypal_subscription( $ipn_data );
603
604
    if( false === $subscription ) {
605
        return;
606
    }
607
608
    $subscription->cancel();
609
}
610
611
/**
612
 * Process the subscription expired IPN.
613
 */
614
function wpinv_process_paypal_subscr_eot( $ipn_data ) {
615
    $subscription = wpinv_get_paypal_subscription( $ipn_data );
616
617
    if( false === $subscription ) {
618
        return;
619
    }
620
621
    $subscription->complete();
622
}
623
624
/**
625
 * Process the subscription payment failed IPN.
626
 */
627
function wpinv_process_paypal_subscr_failed( $ipn_data ) {
628
    $subscription = wpinv_get_paypal_subscription( $ipn_data );
629
630
    if( false === $subscription ) {
631
        return;
632
    }
633
634
    $subscription->failing();
635
636
    do_action( 'wpinv_recurring_payment_failed', $subscription );
637
}
638
639
/**
640
 * Retrieve the subscription this IPN notice is for.
641
 */
642
function wpinv_get_paypal_subscription( $ipn_data = array() ) {
643
    $parent_invoice_id = absint( $ipn_data['custom'] );
644
645
    if( empty( $parent_invoice_id ) ) {
646
        return false;
647
    }
648
649
    $invoice = wpinv_get_invoice( $parent_invoice_id );
650
    if ( empty( $invoice ) ) {
651
        return false;
652
    }
653
654
    $subscription = new WPInv_Subscription( $ipn_data['subscr_id'], true );
655
656
    if ( ! ( ! empty( $subscription ) && $subscription->id > 0 ) ) {
657
        $subscription = wpinv_get_subscription( $parent_invoice_id );
658
659
        if ( ! empty( $subscription ) && $subscription->id > 0 ) {
660
            $subscription->update( array( 'profile_id' => sanitize_text_field( $ipn_data['subscr_id'] ) ) );
661
        } else {
662
            return false;
663
        }
664
    }
665
666
    return $subscription;
667
668
}
669
670
function wpinv_process_paypal_refund( $data, $invoice_id = 0 ) {
671
	// Collect payment details
672
673
	if( empty( $invoice_id ) ) {
674
		return;
675
	}
676
677
	if ( get_post_status( $invoice_id ) == 'wpi-refunded' ) {
678
		return; // Only refund payments once
679
	}
680
681
	$payment_amount = wpinv_payment_total( $invoice_id );
682
	$refund_amount  = $data['mc_gross'] * -1;
683
684
	do_action( 'wpinv_paypal_refund_request', $data, $invoice_id );
685
686
	if ( number_format( (float) $refund_amount, 2 ) < number_format( (float) $payment_amount, 2 ) ) {
687
		wpinv_insert_payment_note( $invoice_id, wp_sprintf( __( 'PayPal partial refund of %s processed for transaction #%s for reason: %s', 'invoicing' ), (float)$refund_amount . ' '. $data['mc_currency'], $data['parent_txn_id'], $data['reason_code'] ), '', '', true );
688
689
		do_action( 'wpinv_paypal_invoice_partially_refunded', $data, $invoice_id, $refund_amount );
690
691
		return; // This is a partial refund
692
	}
693
694
	wpinv_insert_payment_note( $invoice_id, sprintf( __( 'PayPal Payment #%s Refunded for reason: %s', 'invoicing' ), $data['parent_txn_id'], $data['reason_code'] ), '', '', true );
695
	wpinv_insert_payment_note( $invoice_id, sprintf( __( 'PayPal Refund Transaction ID: %s', 'invoicing' ), $data['txn_id'] ), '', '', true );
696
	wpinv_update_payment_status( $invoice_id, 'wpi-refunded' );
697
698
	do_action( 'wpinv_paypal_invoice_fully_refunded', $data, $invoice_id );
699
}
700
701
function wpinv_get_paypal_redirect( $ssl_check = false ) {
702
    if ( is_ssl() || ! $ssl_check ) {
703
        $protocol = 'https://';
704
    } else {
705
        $protocol = 'http://';
706
    }
707
708
    // Check the current payment mode
709
    if ( wpinv_is_test_mode( 'paypal' ) ) {
710
        // Test mode
711
        $paypal_uri = $protocol . 'www.sandbox.paypal.com/cgi-bin/webscr';
712
    } else {
713
        // Live mode
714
        $paypal_uri = $protocol . 'www.paypal.com/cgi-bin/webscr';
715
    }
716
717
    return apply_filters( 'wpinv_paypal_uri', $paypal_uri );
718
}
719
720
function wpinv_paypal_success_page_content( $content ) {
721
    global $wpi_invoice;
722
    
723
    $session = wpinv_get_checkout_session();
724
725
    if ( empty( $_GET['invoice-id'] ) && empty( $session['invoice_key'] )  ) {
726
        return $content;
727
    }
728
729
    $invoice_id = !empty( $_GET['invoice-id'] ) ? absint( $_GET['invoice-id'] ) : wpinv_get_invoice_id_by_key( $session['invoice_key'] );
730
731
    if ( empty(  $invoice_id ) ) {
732
        return $content;
733
    }
734
735
    $wpi_invoice = wpinv_get_invoice( $invoice_id );
736
    
737
    if ( !empty( $wpi_invoice ) && 'wpi-pending' == $wpi_invoice->status ) {
738
        // Payment is still pending so show processing indicator to fix the Race Condition, issue #
739
        ob_start();
740
        wpinv_get_template_part( 'wpinv-payment-processing' );
741
        $content = ob_get_clean();
742
    }
743
744
    return $content;
745
}
746
add_filter( 'wpinv_payment_confirm_paypal', 'wpinv_paypal_success_page_content' );
747
748
function wpinv_paypal_get_transaction_id( $invoice_id ) {
749
    $transaction_id = '';
750
    $notes = wpinv_get_invoice_notes( $invoice_id );
751
752
    foreach ( $notes as $note ) {
753
        if ( preg_match( '/^PayPal Transaction ID: ([^\s]+)/', $note->comment_content, $match ) ) {
754
            $transaction_id = $match[1];
755
            continue;
756
        }
757
    }
758
759
    return apply_filters( 'wpinv_paypal_set_transaction_id', $transaction_id, $invoice_id );
760
}
761
add_filter( 'wpinv_payment_get_transaction_id-paypal', 'wpinv_paypal_get_transaction_id', 10, 1 );
762
763
function wpinv_paypal_link_transaction_id( $transaction_id, $invoice_id, $invoice ) {
764
    if ( $transaction_id == $invoice_id ) {
765
        $transaction_link = $transaction_id;
766
    } else {
767
        if ( ! empty( $invoice ) && ! empty( $invoice->mode ) ) {
768
            $mode = $invoice->mode;
769
        } else {
770
            $mode = wpinv_is_test_mode( 'paypal' ) ? 'test' : 'live';
771
        }
772
773
        $sandbox = $mode == 'test' ? '.sandbox' : '';
774
        $transaction_url = 'https://www' . $sandbox . '.paypal.com/webscr?cmd=_history-details-from-hub&id=' . $transaction_id;
775
776
        $transaction_link = '<a href="' . esc_url( $transaction_url ) . '" target="_blank">' . $transaction_id . '</a>';
777
    }
778
779
    return apply_filters( 'wpinv_paypal_link_payment_details_transaction_id', $transaction_link, $transaction_id, $invoice );
780
}
781
add_filter( 'wpinv_payment_details_transaction_id-paypal', 'wpinv_paypal_link_transaction_id', 10, 3 );
782
783
function wpinv_paypal_profile_id_link( $profile_id, $subscription ) {
784
    $link = $profile_id;
785
786
    if ( ! empty( $profile_id ) && ! empty( $subscription ) && ( $invoice_id = $subscription->get_original_payment_id() ) ) {
787
        $invoice = wpinv_get_invoice( $invoice_id );
788
789
        if ( ! empty( $invoice ) && ! empty( $invoice->mode ) ) {
790
            $mode = $invoice->mode;
791
        } else {
792
            $mode = wpinv_is_test_mode( 'paypal' ) ? 'test' : 'live';
793
        }
794
795
        $sandbox = $mode == 'test' ? '.sandbox' : '';
796
        $url = 'https://www' . $sandbox . '.paypal.com/cgi-bin/webscr?cmd=_profile-recurring-payments&encrypted_profile_id=' . $profile_id;
797
798
        $link = '<a href="' . esc_url( $url ) . '" target="_blank">' . $profile_id . '</a>';
799
    }
800
    
801
    return apply_filters( 'wpinv_paypal_profile_id_link', $link, $profile_id, $subscription );
802
}
803
add_filter( 'wpinv_subscription_profile_link_paypal', 'wpinv_paypal_profile_id_link', 10, 2 );
804
805
function wpinv_paypal_transaction_id_link( $transaction_id, $subscription ) {
806
    if ( ! empty( $transaction_id ) && ! empty( $subscription ) && ( $invoice_id = $subscription->get_original_payment_id() ) ) {
807
        $invoice = wpinv_get_invoice( $invoice_id );
808
809
        if ( ! empty( $invoice ) ) {
810
            return wpinv_paypal_link_transaction_id( $transaction_id, $invoice_id, $invoice );
811
        }        
812
    }
813
    
814
    return $transaction_id;
815
}
816
add_filter( 'wpinv_subscription_transaction_link_paypal', 'wpinv_paypal_transaction_id_link', 10, 2 );
817
818
function wpinv_is_paypal_valid_for_use() {
819
    return in_array( wpinv_get_currency(), apply_filters( 'wpinv_paypal_supported_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' ) ) );
820
}
821
822
function wpinv_check_paypal_currency_support( $gateway_list ) {
823
    if ( isset( $gateway_list['paypal'] ) && ! wpinv_is_paypal_valid_for_use() ) {
824
        unset( $gateway_list['paypal'] );
825
    }
826
    return $gateway_list;
827
}
828
add_filter( 'wpinv_enabled_payment_gateways', 'wpinv_check_paypal_currency_support', 10, 1 );
829
830
function wpinv_gateway_paypal_button_label( $label ) {
831
    return __( 'Proceed to PayPal', 'invoicing' );
832
}
833
add_filter( 'wpinv_gateway_paypal_button_label', 'wpinv_gateway_paypal_button_label', 10, 1 );