Completed
Pull Request — master (#1405)
by
unknown
02:14
created

WC_Gateway_Stripe::verify_intent_after_checkout()   C

Complexity

Conditions 12
Paths 9

Size

Total Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
nc 9
nop 1
dl 0
loc 43
rs 6.9666
c 0
b 0
f 0

How to fix   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
if ( ! defined( 'ABSPATH' ) ) {
3
	exit;
4
}
5
6
/**
7
 * WC_Gateway_Stripe class.
8
 *
9
 * @extends WC_Payment_Gateway
10
 */
11
class WC_Gateway_Stripe extends WC_Stripe_Payment_Gateway {
12
	/**
13
	 * The delay between retries.
14
	 *
15
	 * @var int
16
	 */
17
	public $retry_interval;
18
19
	/**
20
	 * Should we capture Credit cards
21
	 *
22
	 * @var bool
23
	 */
24
	public $capture;
25
26
	/**
27
	 * Alternate credit card statement name
28
	 *
29
	 * @var bool
30
	 */
31
	public $statement_descriptor;
32
33
	/**
34
	 * Should we store the users credit cards?
35
	 *
36
	 * @var bool
37
	 */
38
	public $saved_cards;
39
40
	/**
41
	 * API access secret key
42
	 *
43
	 * @var string
44
	 */
45
	public $secret_key;
46
47
	/**
48
	 * Api access publishable key
49
	 *
50
	 * @var string
51
	 */
52
	public $publishable_key;
53
54
	/**
55
	 * Do we accept Payment Request?
56
	 *
57
	 * @var bool
58
	 */
59
	public $payment_request;
60
61
	/**
62
	 * Is test mode active?
63
	 *
64
	 * @var bool
65
	 */
66
	public $testmode;
67
68
	/**
69
	 * Inline CC form styling
70
	 *
71
	 * @var string
72
	 */
73
	public $inline_cc_form;
74
75
	/**
76
	 * Pre Orders Object
77
	 *
78
	 * @var object
79
	 */
80
	public $pre_orders;
81
82
	/**
83
	 * Constructor
84
	 */
85
	public function __construct() {
86
		$this->retry_interval = 1;
87
		$this->id             = 'stripe';
88
		$this->method_title   = __( 'Stripe', 'woocommerce-gateway-stripe' );
89
		/* translators: 1) link to Stripe register page 2) link to Stripe api keys page */
90
		$this->method_description = __( 'Stripe works by adding payment fields on the checkout and then sending the details to Stripe for verification.', 'woocommerce-gateway-stripe' );
91
		$this->has_fields         = true;
92
		$this->supports           = array(
93
			'products',
94
			'refunds',
95
			'tokenization',
96
			'add_payment_method',
97
			'subscriptions',
98
			'subscription_cancellation',
99
			'subscription_suspension',
100
			'subscription_reactivation',
101
			'subscription_amount_changes',
102
			'subscription_date_changes',
103
			'subscription_payment_method_change',
104
			'subscription_payment_method_change_customer',
105
			'subscription_payment_method_change_admin',
106
			'multiple_subscriptions',
107
			'pre-orders',
108
		);
109
110
		// Load the form fields.
111
		$this->init_form_fields();
112
113
		// Load the settings.
114
		$this->init_settings();
115
116
		// Get setting values.
117
		$this->title                = $this->get_option( 'title' );
118
		$this->description          = $this->get_option( 'description' );
119
		$this->enabled              = $this->get_option( 'enabled' );
120
		$this->testmode             = 'yes' === $this->get_option( 'testmode' );
121
		$this->inline_cc_form       = 'yes' === $this->get_option( 'inline_cc_form' );
0 ignored issues
show
Documentation Bug introduced by
The property $inline_cc_form was declared of type string, but 'yes' === $this->get_option('inline_cc_form') is of type boolean. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
122
		$this->capture              = 'yes' === $this->get_option( 'capture', 'yes' );
123
		$this->statement_descriptor = WC_Stripe_Helper::clean_statement_descriptor( $this->get_option( 'statement_descriptor' ) );
0 ignored issues
show
Documentation Bug introduced by
The property $statement_descriptor was declared of type boolean, but \WC_Stripe_Helper::clean...statement_descriptor')) is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
124
		$this->saved_cards          = 'yes' === $this->get_option( 'saved_cards' );
125
		$this->secret_key           = $this->testmode ? $this->get_option( 'test_secret_key' ) : $this->get_option( 'secret_key' );
126
		$this->publishable_key      = $this->testmode ? $this->get_option( 'test_publishable_key' ) : $this->get_option( 'publishable_key' );
127
		$this->payment_request      = 'yes' === $this->get_option( 'payment_request', 'yes' );
128
129
		WC_Stripe_API::set_secret_key( $this->secret_key );
130
131
		// Hooks.
132
		add_action( 'wp_enqueue_scripts', array( $this, 'payment_scripts' ) );
133
		add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) );
134
		add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
135
		add_action( 'woocommerce_admin_order_totals_after_total', array( $this, 'display_order_fee' ) );
136
		add_action( 'woocommerce_admin_order_totals_after_total', array( $this, 'display_order_payout' ), 20 );
137
		add_action( 'woocommerce_customer_save_address', array( $this, 'show_update_card_notice' ), 10, 2 );
138
		add_action( 'before_woocommerce_pay', array( $this, 'prepare_order_pay_page' ) );
139
		add_action( 'after_woocommerce_pay', array( $this, 'restore_order_pay_page' ), 99 );
140
		add_action( 'woocommerce_account_view-order_endpoint', array( $this, 'check_intent_status_on_order_page' ), 1 );
141
		add_filter( 'woocommerce_payment_successful_result', array( $this, 'modify_successful_payment_result' ), 99999, 2 );
142
		add_action( 'set_logged_in_cookie', array( $this, 'set_cookie_on_current_request' ) );
143
		add_filter( 'woocommerce_get_checkout_payment_url', array( $this, 'get_checkout_payment_url' ), 10, 2 );
144
145
		// Note: display error is in the parent class.
146
		add_action( 'admin_notices', array( $this, 'display_errors' ), 9999 );
147
148 View Code Duplication
		if ( WC_Stripe_Helper::is_pre_orders_exists() ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
149
			$this->pre_orders = new WC_Stripe_Pre_Orders_Compat();
150
151
			add_action( 'wc_pre_orders_process_pre_order_completion_payment_' . $this->id, array( $this->pre_orders, 'process_pre_order_release_payment' ) );
152
		}
153
	}
154
155
	/**
156
	 * Checks if gateway should be available to use.
157
	 *
158
	 * @since 4.0.2
159
	 */
160
	public function is_available() {
161
		if ( is_add_payment_method_page() && ! $this->saved_cards ) {
162
			return false;
163
		}
164
165
		return parent::is_available();
166
	}
167
168
	/**
169
	 * Adds a notice for customer when they update their billing address.
170
	 *
171
	 * @since 4.1.0
172
	 * @param int    $user_id      The ID of the current user.
173
	 * @param string $load_address The address to load.
174
	 */
175
	public function show_update_card_notice( $user_id, $load_address ) {
176
		if ( ! $this->saved_cards || ! WC_Stripe_Payment_Tokens::customer_has_saved_methods( $user_id ) || 'billing' !== $load_address ) {
177
			return;
178
		}
179
180
		/* translators: 1) Opening anchor tag 2) closing anchor tag */
181
		wc_add_notice( sprintf( __( 'If your billing address has been changed for saved payment methods, be sure to remove any %1$ssaved payment methods%2$s on file and re-add them.', 'woocommerce-gateway-stripe' ), '<a href="' . esc_url( wc_get_endpoint_url( 'payment-methods' ) ) . '" class="wc-stripe-update-card-notice" style="text-decoration:underline;">', '</a>' ), 'notice' );
182
	}
183
184
	/**
185
	 * Get_icon function.
186
	 *
187
	 * @since 1.0.0
188
	 * @version 4.0.0
189
	 * @return string
190
	 */
191
	public function get_icon() {
192
		$icons = $this->payment_icons();
193
194
		$icons_str = '';
195
196
		$icons_str .= isset( $icons['visa'] ) ? $icons['visa'] : '';
197
		$icons_str .= isset( $icons['amex'] ) ? $icons['amex'] : '';
198
		$icons_str .= isset( $icons['mastercard'] ) ? $icons['mastercard'] : '';
199
200
		if ( 'USD' === get_woocommerce_currency() ) {
201
			$icons_str .= isset( $icons['discover'] ) ? $icons['discover'] : '';
202
			$icons_str .= isset( $icons['jcb'] ) ? $icons['jcb'] : '';
203
			$icons_str .= isset( $icons['diners'] ) ? $icons['diners'] : '';
204
		}
205
206
		return apply_filters( 'woocommerce_gateway_icon', $icons_str, $this->id );
207
	}
208
209
	/**
210
	 * Initialise Gateway Settings Form Fields
211
	 */
212
	public function init_form_fields() {
213
		$this->form_fields = require( dirname( __FILE__ ) . '/admin/stripe-settings.php' );
214
	}
215
216
	/**
217
	 * Payment form on checkout page
218
	 */
219
	public function payment_fields() {
220
		global $wp;
221
		$user                 = wp_get_current_user();
222
		$display_tokenization = $this->supports( 'tokenization' ) && is_checkout() && $this->saved_cards;
223
		$total                = WC()->cart->total;
0 ignored issues
show
Unused Code introduced by
$total is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
224
		$user_email           = '';
225
		$description          = $this->get_description();
226
		$description          = ! empty( $description ) ? $description : '';
227
		$firstname            = '';
228
		$lastname             = '';
229
230
		// If paying from order, we need to get total from order not cart.
231
		if ( isset( $_GET['pay_for_order'] ) && ! empty( $_GET['key'] ) ) { // wpcs: csrf ok.
232
			$order      = wc_get_order( wc_clean( $wp->query_vars['order-pay'] ) ); // wpcs: csrf ok, sanitization ok.
233
			$total      = $order->get_total();
0 ignored issues
show
Unused Code introduced by
$total is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
234
			$user_email = $order->get_billing_email();
235
		} else {
236
			if ( $user->ID ) {
237
				$user_email = get_user_meta( $user->ID, 'billing_email', true );
238
				$user_email = $user_email ? $user_email : $user->user_email;
239
			}
240
		}
241
242
		if ( is_add_payment_method_page() ) {
243
			$firstname       = $user->user_firstname;
244
			$lastname        = $user->user_lastname;
245
		}
246
247
		ob_start();
248
249
		echo '<div
250
			id="stripe-payment-data"
251
			data-email="' . esc_attr( $user_email ) . '"
252
			data-full-name="' . esc_attr( $firstname . ' ' . $lastname ) . '"
253
			data-currency="' . esc_attr( strtolower( get_woocommerce_currency() ) ) . '"
254
		>';
255
256
		if ( $this->testmode ) {
257
			/* translators: link to Stripe testing page */
258
			$description .= ' ' . sprintf( __( 'TEST MODE ENABLED. In test mode, you can use the card number 4242424242424242 with any CVC and a valid expiration date or check the <a href="%s" target="_blank">Testing Stripe documentation</a> for more card numbers.', 'woocommerce-gateway-stripe' ), 'https://stripe.com/docs/testing' );
259
		}
260
261
		$description = trim( $description );
262
263
		echo apply_filters( 'wc_stripe_description', wpautop( wp_kses_post( $description ) ), $this->id ); // wpcs: xss ok.
264
265
		if ( $display_tokenization ) {
266
			$this->tokenization_script();
267
			$this->saved_payment_methods();
268
		}
269
270
		$this->elements_form();
271
272 View Code Duplication
		if ( apply_filters( 'wc_stripe_display_save_payment_method_checkbox', $display_tokenization ) && ! is_add_payment_method_page() && ! isset( $_GET['change_payment_method'] ) ) { // wpcs: csrf ok.
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
273
274
			$this->save_payment_method_checkbox();
275
		}
276
277
		do_action( 'wc_stripe_cards_payment_fields', $this->id );
278
279
		echo '</div>';
280
281
		ob_end_flush();
282
	}
283
284
	/**
285
	 * Renders the Stripe elements form.
286
	 *
287
	 * @since 4.0.0
288
	 * @version 4.0.0
289
	 */
290
	public function elements_form() {
291
		?>
292
		<fieldset id="wc-<?php echo esc_attr( $this->id ); ?>-cc-form" class="wc-credit-card-form wc-payment-form" style="background:transparent;">
293
			<?php do_action( 'woocommerce_credit_card_form_start', $this->id ); ?>
294
295
			<?php if ( $this->inline_cc_form ) { ?>
296
				<label for="card-element">
297
					<?php esc_html_e( 'Credit or debit card', 'woocommerce-gateway-stripe' ); ?>
298
				</label>
299
300
				<div id="stripe-card-element" class="wc-stripe-elements-field">
301
				<!-- a Stripe Element will be inserted here. -->
302
				</div>
303
			<?php } else { ?>
304
				<div class="form-row form-row-wide">
305
					<label for="stripe-card-element"><?php esc_html_e( 'Card Number', 'woocommerce-gateway-stripe' ); ?> <span class="required">*</span></label>
306
					<div class="stripe-card-group">
307
						<div id="stripe-card-element" class="wc-stripe-elements-field">
308
						<!-- a Stripe Element will be inserted here. -->
309
						</div>
310
311
						<i class="stripe-credit-card-brand stripe-card-brand" alt="Credit Card"></i>
312
					</div>
313
				</div>
314
315
				<div class="form-row form-row-first">
316
					<label for="stripe-exp-element"><?php esc_html_e( 'Expiry Date', 'woocommerce-gateway-stripe' ); ?> <span class="required">*</span></label>
317
318
					<div id="stripe-exp-element" class="wc-stripe-elements-field">
319
					<!-- a Stripe Element will be inserted here. -->
320
					</div>
321
				</div>
322
323
				<div class="form-row form-row-last">
324
					<label for="stripe-cvc-element"><?php esc_html_e( 'Card Code (CVC)', 'woocommerce-gateway-stripe' ); ?> <span class="required">*</span></label>
325
				<div id="stripe-cvc-element" class="wc-stripe-elements-field">
326
				<!-- a Stripe Element will be inserted here. -->
327
				</div>
328
				</div>
329
				<div class="clear"></div>
330
			<?php } ?>
331
332
			<!-- Used to display form errors -->
333
			<div class="stripe-source-errors" role="alert"></div>
334
			<br />
335
			<?php do_action( 'woocommerce_credit_card_form_end', $this->id ); ?>
336
			<div class="clear"></div>
337
		</fieldset>
338
		<?php
339
	}
340
341
	/**
342
	 * Load admin scripts.
343
	 *
344
	 * @since 3.1.0
345
	 * @version 3.1.0
346
	 */
347
	public function admin_scripts() {
348
		if ( 'woocommerce_page_wc-settings' !== get_current_screen()->id ) {
349
			return;
350
		}
351
352
		$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
353
354
		wp_enqueue_script( 'woocommerce_stripe_admin', plugins_url( 'assets/js/stripe-admin' . $suffix . '.js', WC_STRIPE_MAIN_FILE ), array(), WC_STRIPE_VERSION, true );
355
	}
356
357
	/**
358
	 * Payment_scripts function.
359
	 *
360
	 * Outputs scripts used for stripe payment
361
	 *
362
	 * @since 3.1.0
363
	 * @version 4.0.0
364
	 */
365
	public function payment_scripts() {
366
		global $wp;
367
		if (
368
			! is_product()
369
			&& ! is_cart()
370
			&& ! is_checkout()
371
			&& ! isset( $_GET['pay_for_order'] ) // wpcs: csrf ok.
372
			&& ! is_add_payment_method_page()
373
			&& ! isset( $_GET['change_payment_method'] ) // wpcs: csrf ok.
374
			&& ! ( ! empty( get_query_var( 'view-subscription' ) ) && is_callable( 'WCS_Early_Renewal_Manager::is_early_renewal_via_modal_enabled' ) && WCS_Early_Renewal_Manager::is_early_renewal_via_modal_enabled() )
375
			|| ( is_order_received_page() )
376
		) {
377
			return;
378
		}
379
380
		// If Stripe is not enabled bail.
381
		if ( 'no' === $this->enabled ) {
382
			return;
383
		}
384
385
		// If keys are not set bail.
386
		if ( ! $this->are_keys_set() ) {
387
			WC_Stripe_Logger::log( 'Keys are not set correctly.' );
388
			return;
389
		}
390
391
		// If no SSL bail.
392
		if ( ! $this->testmode && ! is_ssl() ) {
393
			WC_Stripe_Logger::log( 'Stripe live mode requires SSL.' );
394
			return;
395
		}
396
397
		$current_theme = wp_get_theme();
0 ignored issues
show
Unused Code introduced by
$current_theme is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
398
399
		$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
400
401
		wp_register_style( 'stripe_styles', plugins_url( 'assets/css/stripe-styles.css', WC_STRIPE_MAIN_FILE ), array(), WC_STRIPE_VERSION );
402
		wp_enqueue_style( 'stripe_styles' );
403
404
		wp_register_script( 'stripe', 'https://js.stripe.com/v3/', '', '3.0', true );
405
		wp_register_script( 'woocommerce_stripe', plugins_url( 'assets/js/stripe' . $suffix . '.js', WC_STRIPE_MAIN_FILE ), array( 'jquery-payment', 'stripe' ), WC_STRIPE_VERSION, true );
406
407
		$stripe_params = array(
408
			'key'                  => $this->publishable_key,
409
			'i18n_terms'           => __( 'Please accept the terms and conditions first', 'woocommerce-gateway-stripe' ),
410
			'i18n_required_fields' => __( 'Please fill in required checkout fields first', 'woocommerce-gateway-stripe' ),
411
		);
412
413
		// If we're on the pay page we need to pass stripe.js the address of the order.
414
		if ( isset( $_GET['pay_for_order'] ) && 'true' === $_GET['pay_for_order'] ) { // wpcs: csrf ok.
415
			$order_id = wc_clean( $wp->query_vars['order-pay'] ); // wpcs: csrf ok, sanitization ok, xss ok.
416
			$order    = wc_get_order( $order_id );
417
418
			if ( is_a( $order, 'WC_Order' ) ) {
419
				$stripe_params['billing_first_name'] = $order->get_billing_first_name();
420
				$stripe_params['billing_last_name']  = $order->get_billing_last_name();
421
				$stripe_params['billing_address_1']  = $order->get_billing_address_1();
422
				$stripe_params['billing_address_2']  = $order->get_billing_address_2();
423
				$stripe_params['billing_state']      = $order->get_billing_state();
424
				$stripe_params['billing_city']       = $order->get_billing_city();
425
				$stripe_params['billing_postcode']   = $order->get_billing_postcode();
426
				$stripe_params['billing_country']    = $order->get_billing_country();
427
			}
428
		}
429
430
		$sepa_elements_options = apply_filters(
431
			'wc_stripe_sepa_elements_options',
432
			array(
433
				'supportedCountries' => array( 'SEPA' ),
434
				'placeholderCountry' => WC()->countries->get_base_country(),
435
				'style'              => array( 'base' => array( 'fontSize' => '15px' ) ),
436
			)
437
		);
438
439
		$stripe_params['no_prepaid_card_msg']       = __( 'Sorry, we\'re not accepting prepaid cards at this time. Your credit card has not been charged. Please try with alternative payment method.', 'woocommerce-gateway-stripe' );
440
		$stripe_params['no_sepa_owner_msg']         = __( 'Please enter your IBAN account name.', 'woocommerce-gateway-stripe' );
441
		$stripe_params['no_sepa_iban_msg']          = __( 'Please enter your IBAN account number.', 'woocommerce-gateway-stripe' );
442
		$stripe_params['payment_intent_error']      = __( 'We couldn\'t initiate the payment. Please try again.', 'woocommerce-gateway-stripe' );
443
		$stripe_params['sepa_mandate_notification'] = apply_filters( 'wc_stripe_sepa_mandate_notification', 'email' );
444
		$stripe_params['allow_prepaid_card']        = apply_filters( 'wc_stripe_allow_prepaid_card', true ) ? 'yes' : 'no';
445
		$stripe_params['inline_cc_form']            = $this->inline_cc_form ? 'yes' : 'no';
446
		$stripe_params['is_checkout']               = ( is_checkout() && empty( $_GET['pay_for_order'] ) ) ? 'yes' : 'no'; // wpcs: csrf ok.
447
		$stripe_params['return_url']                = $this->get_stripe_return_url();
448
		$stripe_params['ajaxurl']                   = WC_AJAX::get_endpoint( '%%endpoint%%' );
449
		$stripe_params['stripe_nonce']              = wp_create_nonce( '_wc_stripe_nonce' );
450
		$stripe_params['statement_descriptor']      = $this->statement_descriptor;
451
		$stripe_params['elements_options']          = apply_filters( 'wc_stripe_elements_options', array() );
452
		$stripe_params['sepa_elements_options']     = $sepa_elements_options;
453
		$stripe_params['invalid_owner_name']        = __( 'Billing First Name and Last Name are required.', 'woocommerce-gateway-stripe' );
454
		$stripe_params['is_change_payment_page']    = isset( $_GET['change_payment_method'] ) ? 'yes' : 'no'; // wpcs: csrf ok.
455
		$stripe_params['is_add_payment_page']       = is_wc_endpoint_url( 'add-payment-method' ) ? 'yes' : 'no';
456
		$stripe_params['is_pay_for_order_page']     = is_wc_endpoint_url( 'order-pay' ) ? 'yes' : 'no';
457
		$stripe_params['elements_styling']          = apply_filters( 'wc_stripe_elements_styling', false );
458
		$stripe_params['elements_classes']          = apply_filters( 'wc_stripe_elements_classes', false );
459
460
		// Merge localized messages to be use in JS.
461
		$stripe_params = array_merge( $stripe_params, WC_Stripe_Helper::get_localized_messages() );
462
463
		wp_localize_script( 'woocommerce_stripe', 'wc_stripe_params', apply_filters( 'wc_stripe_params', $stripe_params ) );
464
465
		$this->tokenization_script();
466
		wp_enqueue_script( 'woocommerce_stripe' );
467
	}
468
469
	/**
470
	 * Checks if a source object represents a prepaid credit card and
471
	 * throws an exception if it is one, but that is not allowed.
472
	 *
473
	 * @since 4.2.0
474
	 * @param object $prepared_source The object with source details.
475
	 * @throws WC_Stripe_Exception An exception if the card is prepaid, but prepaid cards are not allowed.
476
	 */
477
	public function maybe_disallow_prepaid_card( $prepared_source ) {
478
		// Check if we don't allow prepaid credit cards.
479
		if ( apply_filters( 'wc_stripe_allow_prepaid_card', true ) || ! $this->is_prepaid_card( $prepared_source->source_object ) ) {
480
			return;
481
		}
482
483
		$localized_message = __( 'Sorry, we\'re not accepting prepaid cards at this time. Your credit card has not been charged. Please try with alternative payment method.', 'woocommerce-gateway-stripe' );
484
		throw new WC_Stripe_Exception( print_r( $prepared_source->source_object, true ), $localized_message );
485
	}
486
487
	/**
488
	 * Checks whether a source exists.
489
	 *
490
	 * @since 4.2.0
491
	 * @param  object $prepared_source The source that should be verified.
492
	 * @throws WC_Stripe_Exception     An exception if the source ID is missing.
493
	 */
494
	public function check_source( $prepared_source ) {
495 View Code Duplication
		if ( empty( $prepared_source->source ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
496
			$localized_message = __( 'Payment processing failed. Please retry.', 'woocommerce-gateway-stripe' );
497
			throw new WC_Stripe_Exception( print_r( $prepared_source, true ), $localized_message );
498
		}
499
	}
500
501
	/**
502
	 * Customer param wrong? The user may have been deleted on stripe's end. Remove customer_id. Can be retried without.
503
	 *
504
	 * @since 4.2.0
505
	 * @param object   $error The error that was returned from Stripe's API.
506
	 * @param WC_Order $order The order those payment is being processed.
507
	 * @return bool           A flag that indicates that the customer does not exist and should be removed.
508
	 */
509
	public function maybe_remove_non_existent_customer( $error, $order ) {
510
		if ( ! $this->is_no_such_customer_error( $error ) ) {
511
			return false;
512
		}
513
514
		delete_user_option( $order->get_customer_id(), '_stripe_customer_id' );
515
		$order->delete_meta_data( '_stripe_customer_id' );
516
		$order->save();
517
518
		return true;
519
	}
520
521
	/**
522
	 * Completes an order without a positive value.
523
	 *
524
	 * @since 4.2.0
525
	 * @param WC_Order $order             The order to complete.
526
	 * @param WC_Order $prepared_source   Payment source and customer data.
527
	 * @param boolean  $force_save_source Whether the payment source must be saved, like when dealing with a Subscription setup.
528
	 * @return array                      Redirection data for `process_payment`.
529
	 */
530
	public function complete_free_order( $order, $prepared_source, $force_save_source ) {
531
		if ( $force_save_source ) {
532
			$intent_secret = $this->setup_intent( $order, $prepared_source );
533
534
			if ( ! empty( $intent_secret ) ) {
535
				// `get_return_url()` must be called immediately before returning a value.
536
				return array(
537
					'result'              => 'success',
538
					'redirect'            => $this->get_return_url( $order ),
539
					'setup_intent_secret' => $intent_secret,
540
				);
541
			}
542
		}
543
544
		// Remove cart.
545
		WC()->cart->empty_cart();
546
547
		$order->payment_complete();
548
549
		// Return thank you page redirect.
550
		return array(
551
			'result'   => 'success',
552
			'redirect' => $this->get_return_url( $order ),
553
		);
554
	}
555
556
	/**
557
	 * Process the payment
558
	 *
559
	 * @since 1.0.0
560
	 * @since 4.1.0 Add 4th parameter to track previous error.
561
	 * @param int  $order_id Reference.
562
	 * @param bool $retry Should we retry on fail.
563
	 * @param bool $force_save_source Force save the payment source.
564
	 * @param mix  $previous_error Any error message from previous request.
565
	 * @param bool $use_order_source Whether to use the source, which should already be attached to the order.
566
	 *
567
	 * @throws Exception If payment will not be accepted.
568
	 * @return array|void
569
	 */
570
	public function process_payment( $order_id, $retry = true, $force_save_source = false, $previous_error = false, $use_order_source = false ) {
571
		try {
572
			$order = wc_get_order( $order_id );
573
574
			// ToDo: `process_pre_order` saves the source to the order for a later payment.
575
			// This might not work well with PaymentIntents.
576
			if ( $this->maybe_process_pre_orders( $order_id ) ) {
577
				return $this->pre_orders->process_pre_order( $order_id );
578
			}
579
580
			// Check whether there is an existing intent.
581
			$intent = $this->get_intent_from_order( $order );
582
			if ( isset( $intent->object ) && 'setup_intent' === $intent->object ) {
583
				$intent = false; // This function can only deal with *payment* intents
584
			}
585
586
			$stripe_customer_id = null;
587
			if ( $intent && ! empty( $intent->customer ) ) {
588
				$stripe_customer_id = $intent->customer;
589
			}
590
591
			// For some payments the source should already be present in the order.
592
			if ( $use_order_source ) {
593
				$prepared_source = $this->prepare_order_source( $order );
594
			} else {
595
				$prepared_source = $this->prepare_source( get_current_user_id(), $force_save_source, $stripe_customer_id );
596
			}
597
598
			$this->maybe_disallow_prepaid_card( $prepared_source );
599
			$this->check_source( $prepared_source );
600
			$this->save_source_to_order( $order, $prepared_source );
601
602
			if ( 0 >= $order->get_total() ) {
603
				return $this->complete_free_order( $order, $prepared_source, $force_save_source );
604
			}
605
606
			// This will throw exception if not valid.
607
			$this->validate_minimum_order_amount( $order );
608
609
			WC_Stripe_Logger::log( "Info: Begin processing payment for order $order_id for the amount of {$order->get_total()}" );
610
611
			if ( $intent ) {
612
				$intent = $this->update_existing_intent( $intent, $order, $prepared_source );
613
			} else {
614
				$intent = $this->create_intent( $order, $prepared_source );
615
			}
616
617
			// Confirm the intent after locking the order to make sure webhooks will not interfere.
618
			if ( empty( $intent->error ) ) {
619
				$this->lock_order_payment( $order, $intent );
620
				$intent = $this->confirm_intent( $intent, $order, $prepared_source );
621
			}
622
623
			if ( ! empty( $intent->error ) ) {
624
				$this->maybe_remove_non_existent_customer( $intent->error, $order );
625
626
				// We want to retry.
627
				if ( $this->is_retryable_error( $intent->error ) ) {
628
					return $this->retry_after_error( $intent, $order, $retry, $force_save_source, $previous_error, $use_order_source );
629
				}
630
631
				$this->unlock_order_payment( $order );
632
				$this->throw_localized_message( $intent, $order );
633
			}
634
635
			if ( ! empty( $intent ) ) {
636
				// Use the last charge within the intent to proceed.
637
				$response = end( $intent->charges->data );
638
639
				// If the intent requires a 3DS flow, redirect to it.
640
				if ( 'requires_action' === $intent->status ) {
641
					$this->unlock_order_payment( $order );
642
643
					if ( is_wc_endpoint_url( 'order-pay' ) ) {
644
						$redirect_url = add_query_arg( 'wc-stripe-confirmation', 1, $order->get_checkout_payment_url( false ) );
645
646
						return array(
647
							'result'   => 'success',
648
							'redirect' => $redirect_url,
649
						);
650
					} else {
651
						/**
652
						 * This URL contains only a hash, which will be sent to `checkout.js` where it will be set like this:
653
						 * `window.location = result.redirect`
654
						 * Once this redirect is sent to JS, the `onHashChange` function will execute `handleCardPayment`.
655
						 */
656
657
						return array(
658
							'result'                => 'success',
659
							'redirect'              => $this->get_return_url( $order ),
660
							'payment_intent_secret' => $intent->client_secret,
661
						);
662
					}
663
				}
664
			}
665
666
			// Process valid response.
667
			$this->process_response( $response, $order );
0 ignored issues
show
Bug introduced by
The variable $response does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
668
669
			// Remove cart.
670
			if ( isset( WC()->cart ) ) {
671
				WC()->cart->empty_cart();
672
			}
673
674
			// Unlock the order.
675
			$this->unlock_order_payment( $order );
676
677
			// Return thank you page redirect.
678
			return array(
679
				'result'   => 'success',
680
				'redirect' => $this->get_return_url( $order ),
681
			);
682
683
		} catch ( WC_Stripe_Exception $e ) {
684
			wc_add_notice( $e->getLocalizedMessage(), 'error' );
685
			WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
686
687
			do_action( 'wc_gateway_stripe_process_payment_error', $e, $order );
688
689
			/* translators: error message */
690
			$order->update_status( 'failed' );
691
692
			return array(
693
				'result'   => 'fail',
694
				'redirect' => '',
695
			);
696
		}
697
	}
698
699
	/**
700
	 * Displays the Stripe fee
701
	 *
702
	 * @since 4.1.0
703
	 *
704
	 * @param int $order_id The ID of the order.
705
	 */
706 View Code Duplication
	public function display_order_fee( $order_id ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
707
		if ( apply_filters( 'wc_stripe_hide_display_order_fee', false, $order_id ) ) {
708
			return;
709
		}
710
711
		$order = wc_get_order( $order_id );
712
713
		$fee      = WC_Stripe_Helper::get_stripe_fee( $order );
714
		$currency = WC_Stripe_Helper::get_stripe_currency( $order );
715
716
		if ( ! $fee || ! $currency ) {
717
			return;
718
		}
719
720
		?>
721
722
		<tr>
723
			<td class="label stripe-fee">
724
				<?php echo wc_help_tip( __( 'This represents the fee Stripe collects for the transaction.', 'woocommerce-gateway-stripe' ) ); // wpcs: xss ok. ?>
725
				<?php esc_html_e( 'Stripe Fee:', 'woocommerce-gateway-stripe' ); ?>
726
			</td>
727
			<td width="1%"></td>
728
			<td class="total">
729
				-&nbsp;<?php echo wc_price( $fee, array( 'currency' => $currency ) ); // wpcs: xss ok. ?>
730
			</td>
731
		</tr>
732
733
		<?php
734
	}
735
736
	/**
737
	 * Displays the net total of the transaction without the charges of Stripe.
738
	 *
739
	 * @since 4.1.0
740
	 *
741
	 * @param int $order_id The ID of the order.
742
	 */
743 View Code Duplication
	public function display_order_payout( $order_id ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
744
		if ( apply_filters( 'wc_stripe_hide_display_order_payout', false, $order_id ) ) {
745
			return;
746
		}
747
748
		$order = wc_get_order( $order_id );
749
750
		$net      = WC_Stripe_Helper::get_stripe_net( $order );
751
		$currency = WC_Stripe_Helper::get_stripe_currency( $order );
752
753
		if ( ! $net || ! $currency ) {
754
			return;
755
		}
756
757
		?>
758
759
		<tr>
760
			<td class="label stripe-payout">
761
				<?php echo wc_help_tip( __( 'This represents the net total that will be credited to your Stripe bank account. This may be in the currency that is set in your Stripe account.', 'woocommerce-gateway-stripe' ) ); // wpcs: xss ok. ?>
762
				<?php esc_html_e( 'Stripe Payout:', 'woocommerce-gateway-stripe' ); ?>
763
			</td>
764
			<td width="1%"></td>
765
			<td class="total">
766
				<?php echo wc_price( $net, array( 'currency' => $currency ) ); // wpcs: xss ok. ?>
767
			</td>
768
		</tr>
769
770
		<?php
771
	}
772
773
	/**
774
	 * Generates a localized message for an error from a response.
775
	 *
776
	 * @since 4.3.2
777
	 *
778
	 * @param stdClass $response The response from the Stripe API.
779
	 *
780
	 * @return string The localized error message.
781
	 */
782
	public function get_localized_error_message_from_response( $response ) {
783
		$localized_messages = WC_Stripe_Helper::get_localized_messages();
784
785
		if ( 'card_error' === $response->error->type ) {
786
			$localized_message = isset( $localized_messages[ $response->error->code ] ) ? $localized_messages[ $response->error->code ] : $response->error->message;
787
		} else {
788
			$localized_message = isset( $localized_messages[ $response->error->type ] ) ? $localized_messages[ $response->error->type ] : $response->error->message;
789
		}
790
791
		return $localized_message;
792
	}
793
794
	/**
795
	 * Gets a localized message for an error from a response, adds it as a note to the order, and throws it.
796
	 *
797
	 * @since 4.2.0
798
	 * @param  stdClass $response  The response from the Stripe API.
799
	 * @param  WC_Order $order     The order to add a note to.
800
	 * @throws WC_Stripe_Exception An exception with the right message.
801
	 */
802
	public function throw_localized_message( $response, $order ) {
803
		$localized_message = $this->get_localized_error_message_from_response( $response );
804
805
		$order->add_order_note( $localized_message );
806
807
		throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
808
	}
809
810
	/**
811
	 * Retries the payment process once an error occured.
812
	 *
813
	 * @since 4.2.0
814
	 * @param object   $response          The response from the Stripe API.
815
	 * @param WC_Order $order             An order that is being paid for.
816
	 * @param bool     $retry             A flag that indicates whether another retry should be attempted.
817
	 * @param bool     $force_save_source Force save the payment source.
818
	 * @param mixed    $previous_error    Any error message from previous request.
819
	 * @param bool     $use_order_source  Whether to use the source, which should already be attached to the order.
820
	 * @throws WC_Stripe_Exception        If the payment is not accepted.
821
	 * @return array|void
822
	 */
823
	public function retry_after_error( $response, $order, $retry, $force_save_source, $previous_error, $use_order_source ) {
824
		if ( ! $retry ) {
825
			$localized_message = __( 'Sorry, we are unable to process your payment at this time. Please retry later.', 'woocommerce-gateway-stripe' );
826
			$order->add_order_note( $localized_message );
827
			throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.
828
		}
829
830
		// Don't do anymore retries after this.
831
		if ( 5 <= $this->retry_interval ) {
832
			return $this->process_payment( $order->get_id(), false, $force_save_source, $response->error, $previous_error );
833
		}
834
835
		sleep( $this->retry_interval );
836
		$this->retry_interval++;
837
838
		return $this->process_payment( $order->get_id(), true, $force_save_source, $response->error, $previous_error, $use_order_source );
839
	}
840
841
	/**
842
	 * Adds the necessary hooks to modify the "Pay for order" page in order to clean
843
	 * it up and prepare it for the Stripe PaymentIntents modal to confirm a payment.
844
	 *
845
	 * @since 4.2
846
	 */
847
	public function prepare_order_pay_page() {
848
		if ( ! is_wc_endpoint_url( 'order-pay' ) || ! isset( $_GET['wc-stripe-confirmation'] ) ) { // wpcs: csrf ok.
849
			return;
850
		}
851
852
		try {
853
			$this->prepare_intent_for_order_pay_page();
854
		} catch ( WC_Stripe_Exception $e ) {
855
			// Just show the full order pay page if there was a problem preparing the Payment Intent
856
			return;
857
		}
858
859
		add_filter( 'woocommerce_checkout_show_terms', '__return_false' );
860
		add_filter( 'woocommerce_pay_order_button_html', '__return_false' );
861
		add_filter( 'woocommerce_available_payment_gateways', '__return_empty_array' );
862
		add_filter( 'woocommerce_no_available_payment_methods_message', array( $this, 'change_no_available_methods_message' ) );
863
		add_action( 'woocommerce_pay_order_after_submit', array( $this, 'render_intent_inputs' ) );
864
		add_action( 'after_woocommerce_pay', array( $this, 'render_intent_inputs' ), 101 );
865
	}
866
867
	/**
868
	 * Removes hooks modifying "Pay for order" page (i.e. so that later hooks can operate on available payment gateways).
869
	 */
870
	public function restore_order_pay_page() {
871
		remove_filter( 'woocommerce_checkout_show_terms', '__return_false' );
872
		remove_filter( 'woocommerce_pay_order_button_html', '__return_false' );
873
		remove_filter( 'woocommerce_available_payment_gateways', '__return_empty_array' );
874
		remove_filter( 'woocommerce_no_available_payment_methods_message', array( $this, 'change_no_available_methods_message' ) );
875
		remove_action( 'woocommerce_pay_order_after_submit', array( $this, 'render_intent_inputs' ) );
876
		remove_action( 'after_woocommerce_pay', array( $this, 'render_intent_inputs' ), 101 );
877
	}
878
879
	/**
880
	 * Changes the text of the "No available methods" message to one that indicates
881
	 * the need for a PaymentIntent to be confirmed.
882
	 *
883
	 * @since 4.2
884
	 * @return string the new message.
885
	 */
886
	public function change_no_available_methods_message() {
887
		return __( "Almost there!\n\nYour order has already been created, the only thing that still needs to be done is for you to authorize the payment with your bank.", 'woocommerce-gateway-stripe' );
888
	}
889
890
	/**
891
	 * Prepares the Payment Intent for it to be completed in the "Pay for Order" page.
892
	 *
893
	 * @param WC_Order|null $order Order object, or null to get the order from the "order-pay" URL parameter
894
	 *
895
	 * @throws WC_Stripe_Exception
896
	 * @since 4.3
897
	 */
898
	public function prepare_intent_for_order_pay_page( $order = null ) {
899 View Code Duplication
		if ( ! isset( $order ) || empty( $order ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
900
			$order = wc_get_order( absint( get_query_var( 'order-pay' ) ) );
901
		}
902
		$intent = $this->get_intent_from_order( $order );
903
904
		if ( ! $intent ) {
905
			throw new WC_Stripe_Exception( 'Intent not found', __( 'Intent not found for order #' . $order->get_id(), 'woocommerce-gateway-stripe' ) );
906
		}
907
908
		if ( 'requires_payment_method' === $intent->status && isset( $intent->last_payment_error )
909
		     && 'authentication_required' === $intent->last_payment_error->code ) {
910
			$level3_data = $this->get_level3_data_from_order( $order );
911
			$intent      = WC_Stripe_API::request_with_level3_data(
912
				array(
913
					'payment_method' => $intent->last_payment_error->source->id,
914
				),
915
				'payment_intents/' . $intent->id . '/confirm',
916
				$level3_data,
917
				$order
918
			);
919
920
			if ( isset( $intent->error ) ) {
921
				throw new WC_Stripe_Exception( print_r( $intent, true ), $intent->error->message );
922
			}
923
		}
924
925
		$this->order_pay_intent = $intent;
926
	}
927
928
	/**
929
	 * Renders hidden inputs on the "Pay for Order" page in order to let Stripe handle intents.
930
	 *
931
	 * @param WC_Order|null $order Order object, or null to get the order from the "order-pay" URL parameter
932
	 *
933
	 * @throws WC_Stripe_Exception
934
	 * @since 4.2
935
	 */
936
	public function render_intent_inputs( $order = null ) {
937 View Code Duplication
		if ( ! isset( $order ) || empty( $order ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
938
			$order = wc_get_order( absint( get_query_var( 'order-pay' ) ) );
939
		}
940
		if ( ! isset( $this->order_pay_intent ) ) {
941
			$this->prepare_intent_for_order_pay_page( $order );
942
		}
943
944
		$redirect_url = rawurlencode( $this->get_return_url( $order ) );
945
946
		if ( isset( $_GET['wc-stripe-redirect'] ) ) {
947
			$redirect_url = $_GET['wc-stripe-redirect'];
948
		}
949
950
		$verification_url = add_query_arg(
951
			array(
952
				'order'       => $order->get_id(),
953
				'nonce'       => wp_create_nonce( 'wc_stripe_confirm_pi' ),
954
				'redirect_to' => $redirect_url,
955
			),
956
			WC_AJAX::get_endpoint( 'wc_stripe_verify_intent' )
957
		);
958
959
		echo '<input type="hidden" id="stripe-intent-id" value="' . esc_attr( $this->order_pay_intent->client_secret ) . '" />';
960
		echo '<input type="hidden" id="stripe-intent-return" value="' . esc_attr( $verification_url ) . '" />';
961
	}
962
963
	/**
964
	 * Adds an error message wrapper to each saved method.
965
	 *
966
	 * @since 4.2.0
967
	 * @param WC_Payment_Token $token Payment Token.
968
	 * @return string                 Generated payment method HTML
969
	 */
970
	public function get_saved_payment_method_option_html( $token ) {
971
		$html          = parent::get_saved_payment_method_option_html( $token );
972
		$error_wrapper = '<div class="stripe-source-errors" role="alert"></div>';
973
974
		return preg_replace( '~</(\w+)>\s*$~', "$error_wrapper</$1>", $html );
975
	}
976
977
	/**
978
	 * Attempt to manually complete the payment process for orders, which are still pending
979
	 * before displaying the View Order page. This is useful in case webhooks have not been set up.
980
	 *
981
	 * @since 4.2.0
982
	 * @param int $order_id The ID that will be used for the thank you page.
983
	 */
984
	public function check_intent_status_on_order_page( $order_id ) {
985
		if ( empty( $order_id ) || absint( $order_id ) <= 0 ) {
986
			return;
987
		}
988
989
		$order = wc_get_order( absint( $order_id ) );
990
991
		if ( ! $order ) {
992
			return;
993
		}
994
995
		$this->verify_intent_after_checkout( $order );
996
	}
997
998
	/**
999
	 * Attached to `woocommerce_payment_successful_result` with a late priority,
1000
	 * this method will combine the "naturally" generated redirect URL from
1001
	 * WooCommerce and a payment/setup intent secret into a hash, which contains both
1002
	 * the secret, and a proper URL, which will confirm whether the intent succeeded.
1003
	 *
1004
	 * @since 4.2.0
1005
	 * @param array $result   The result from `process_payment`.
1006
	 * @param int   $order_id The ID of the order which is being paid for.
1007
	 * @return array
1008
	 */
1009
	public function modify_successful_payment_result( $result, $order_id ) {
1010
		if ( ! isset( $result['payment_intent_secret'] ) && ! isset( $result['setup_intent_secret'] ) ) {
1011
			// Only redirects with intents need to be modified.
1012
			return $result;
1013
		}
1014
1015
		// Put the final thank you page redirect into the verification URL.
1016
		$verification_url = add_query_arg(
1017
			array(
1018
				'order'       => $order_id,
1019
				'nonce'       => wp_create_nonce( 'wc_stripe_confirm_pi' ),
1020
				'redirect_to' => rawurlencode( $result['redirect'] ),
1021
			),
1022
			WC_AJAX::get_endpoint( 'wc_stripe_verify_intent' )
1023
		);
1024
1025
		if ( isset( $result['payment_intent_secret'] ) ) {
1026
			$redirect = sprintf( '#confirm-pi-%s:%s', $result['payment_intent_secret'], rawurlencode( $verification_url ) );
1027
		} else if ( isset( $result['setup_intent_secret'] ) ) {
1028
			$redirect = sprintf( '#confirm-si-%s:%s', $result['setup_intent_secret'], rawurlencode( $verification_url ) );
1029
		}
1030
1031
		return array(
1032
			'result'   => 'success',
1033
			'redirect' => $redirect,
0 ignored issues
show
Bug introduced by
The variable $redirect does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1034
		);
1035
	}
1036
1037
	/**
1038
	 * Proceed with current request using new login session (to ensure consistent nonce).
1039
	 */
1040
	public function set_cookie_on_current_request( $cookie ) {
1041
		$_COOKIE[ LOGGED_IN_COOKIE ] = $cookie;
1042
	}
1043
1044
	/**
1045
	 * Executed between the "Checkout" and "Thank you" pages, this
1046
	 * method updates orders based on the status of associated PaymentIntents.
1047
	 *
1048
	 * @since 4.2.0
1049
	 * @param WC_Order $order The order which is in a transitional state.
1050
	 */
1051
	public function verify_intent_after_checkout( $order ) {
1052
		$payment_method = $order->get_payment_method();
1053
		if ( $payment_method !== $this->id ) {
1054
			// If this is not the payment method, an intent would not be available.
1055
			return;
1056
		}
1057
1058
		$intent = $this->get_intent_from_order( $order );
1059
		if ( ! $intent ) {
1060
			// No intent, redirect to the order received page for further actions.
1061
			return;
1062
		}
1063
1064
		// A webhook might have modified or locked the order while the intent was retreived. This ensures we are reading the right status.
1065
		clean_post_cache( $order->get_id() );
1066
		$order = wc_get_order( $order->get_id() );
1067
1068
		if ( ! $order->has_status( array( 'pending', 'failed' ) ) ) {
1069
			// If payment has already been completed, this function is redundant.
1070
			return;
1071
		}
1072
1073
		if ( $this->lock_order_payment( $order, $intent ) ) {
1074
			return;
1075
		}
1076
1077
		if ( 'setup_intent' === $intent->object && 'succeeded' === $intent->status ) {
1078
			WC()->cart->empty_cart();
1079
			if ( WC_Stripe_Helper::is_pre_orders_exists() && WC_Pre_Orders_Order::order_contains_pre_order( $order ) ) {
1080
				WC_Pre_Orders_Order::mark_order_as_pre_ordered( $order );
1081
			} else {
1082
				$order->payment_complete();
1083
			}
1084
		} else if ( 'succeeded' === $intent->status || 'requires_capture' === $intent->status ) {
1085
			// Proceed with the payment completion.
1086
			$this->handle_intent_verification_success( $order, $intent );
1087
		} else if ( 'requires_payment_method' === $intent->status ) {
1088
			// `requires_payment_method` means that SCA got denied for the current payment method.
1089
			$this->handle_intent_verification_failure( $order, $intent );
1090
		}
1091
1092
		$this->unlock_order_payment( $order );
1093
	}
1094
1095
	/**
1096
	 * Called after an intent verification succeeds, this allows
1097
	 * specific APNs or children of this class to modify its behavior.
1098
	 *
1099
	 * @param WC_Order $order The order whose verification succeeded.
1100
	 * @param stdClass $intent The Payment Intent object.
1101
	 */
1102
	protected function handle_intent_verification_success( $order, $intent ) {
1103
		$this->process_response( end( $intent->charges->data ), $order );
1104
	}
1105
1106
	/**
1107
	 * Called after an intent verification fails, this allows
1108
	 * specific APNs or children of this class to modify its behavior.
1109
	 *
1110
	 * @param WC_Order $order The order whose verification failed.
1111
	 * @param stdClass $intent The Payment Intent object.
1112
	 */
1113
	protected function handle_intent_verification_failure( $order, $intent ) {
1114
		$this->failed_sca_auth( $order, $intent );
1115
	}
1116
1117
	/**
1118
	 * Checks if the payment intent associated with an order failed and records the event.
1119
	 *
1120
	 * @since 4.2.0
1121
	 * @param WC_Order $order  The order which should be checked.
1122
	 * @param object   $intent The intent, associated with the order.
1123
	 */
1124
	public function failed_sca_auth( $order, $intent ) {
1125
		// If the order has already failed, do not repeat the same message.
1126
		if ( $order->has_status( 'failed' ) ) {
1127
			return;
1128
		}
1129
1130
		// Load the right message and update the status.
1131
		$status_message = isset( $intent->last_payment_error )
1132
			/* translators: 1) The error message that was received from Stripe. */
1133
			? sprintf( __( 'Stripe SCA authentication failed. Reason: %s', 'woocommerce-gateway-stripe' ), $intent->last_payment_error->message )
1134
			: __( 'Stripe SCA authentication failed.', 'woocommerce-gateway-stripe' );
1135
		$order->update_status( 'failed', $status_message );
1136
	}
1137
1138
	/**
1139
	 * Preserves the "wc-stripe-confirmation" URL parameter so the user can complete the SCA authentication after logging in.
1140
	 *
1141
	 * @param string $pay_url Current computed checkout URL for the given order.
1142
	 * @param WC_Order $order Order object.
1143
	 *
1144
	 * @return string Checkout URL for the given order.
1145
	 */
1146
	public function get_checkout_payment_url( $pay_url, $order ) {
1147
		global $wp;
1148
		if ( isset( $_GET['wc-stripe-confirmation'] ) && isset( $wp->query_vars['order-pay'] ) && $wp->query_vars['order-pay'] == $order->get_id() ) {
1149
			$pay_url = add_query_arg( 'wc-stripe-confirmation', 1, $pay_url );
1150
		}
1151
		return $pay_url;
1152
	}
1153
1154
	/**
1155
	 * Checks whether new keys are being entered when saving options.
1156
	 */
1157
	public function process_admin_options() {
1158
		// Load all old values before the new settings get saved.
1159
		$old_publishable_key      = $this->get_option( 'publishable_key' );
1160
		$old_secret_key           = $this->get_option( 'secret_key' );
1161
		$old_test_publishable_key = $this->get_option( 'test_publishable_key' );
1162
		$old_test_secret_key      = $this->get_option( 'test_secret_key' );
1163
1164
		parent::process_admin_options();
1165
1166
		// Load all old values after the new settings have been saved.
1167
		$new_publishable_key      = $this->get_option( 'publishable_key' );
1168
		$new_secret_key           = $this->get_option( 'secret_key' );
1169
		$new_test_publishable_key = $this->get_option( 'test_publishable_key' );
1170
		$new_test_secret_key      = $this->get_option( 'test_secret_key' );
1171
1172
		// Checks whether a value has transitioned from a non-empty value to a new one.
1173
		$has_changed = function( $old_value, $new_value ) {
1174
			return ! empty( $old_value ) && ( $old_value !== $new_value );
1175
		};
1176
1177
		// Look for updates.
1178
		if (
1179
			$has_changed( $old_publishable_key, $new_publishable_key )
1180
			|| $has_changed( $old_secret_key, $new_secret_key )
1181
			|| $has_changed( $old_test_publishable_key, $new_test_publishable_key )
1182
			|| $has_changed( $old_test_secret_key, $new_test_secret_key )
1183
		) {
1184
			update_option( 'wc_stripe_show_changed_keys_notice', 'yes' );
1185
		}
1186
	}
1187
1188 View Code Duplication
	public function validate_publishable_key_field( $key, $value ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1189
		$value = $this->validate_text_field( $key, $value );
1190
		if ( ! empty( $value ) && ! preg_match( '/^pk_live_/', $value ) ) {
1191
			throw new Exception( __( 'The "Live Publishable Key" should start with "pk_live", enter the correct key.', 'woocommerce-gateway-stripe' ) );
1192
		}
1193
		return $value;
1194
	}
1195
1196 View Code Duplication
	public function validate_secret_key_field( $key, $value ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1197
		$value = $this->validate_text_field( $key, $value );
1198
		if ( ! empty( $value ) && ! preg_match( '/^[rs]k_live_/', $value ) ) {
1199
			throw new Exception( __( 'The "Live Secret Key" should start with "sk_live" or "rk_live", enter the correct key.', 'woocommerce-gateway-stripe' ) );
1200
		}
1201
		return $value;
1202
	}
1203
1204 View Code Duplication
	public function validate_test_publishable_key_field( $key, $value ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1205
		$value = $this->validate_text_field( $key, $value );
1206
		if ( ! empty( $value ) && ! preg_match( '/^pk_test_/', $value ) ) {
1207
			throw new Exception( __( 'The "Test Publishable Key" should start with "pk_test", enter the correct key.', 'woocommerce-gateway-stripe' ) );
1208
		}
1209
		return $value;
1210
	}
1211
1212 View Code Duplication
	public function validate_test_secret_key_field( $key, $value ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1213
		$value = $this->validate_text_field( $key, $value );
1214
		if ( ! empty( $value ) && ! preg_match( '/^[rs]k_test_/', $value ) ) {
1215
			throw new Exception( __( 'The "Test Secret Key" should start with "sk_test" or "rk_test", enter the correct key.', 'woocommerce-gateway-stripe' ) );
1216
		}
1217
		return $value;
1218
	}
1219
}
1220