Completed
Pull Request — master (#1267)
by
unknown
02:06
created

WC_Gateway_Stripe::admin_scripts()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 5
nop 0
dl 0
loc 26
rs 8.8817
c 0
b 0
f 0
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_filter( 'woocommerce_available_payment_gateways', array( $this, 'prepare_order_pay_page' ) );
139
		add_action( 'woocommerce_account_view-order_endpoint', array( $this, 'check_intent_status_on_order_page' ), 1 );
140
		add_filter( 'woocommerce_payment_successful_result', array( $this, 'modify_successful_payment_result' ), 99999, 2 );
141
		add_action( 'set_logged_in_cookie', array( $this, 'set_cookie_on_current_request' ) );
142
		add_filter( 'woocommerce_get_checkout_payment_url', array( $this, 'get_checkout_payment_url' ), 10, 2 );
143
144
		// Note: display error is in the parent class.
145
		add_action( 'admin_notices', array( $this, 'display_errors' ), 9999 );
146
147 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...
148
			$this->pre_orders = new WC_Stripe_Pre_Orders_Compat();
149
150
			add_action( 'wc_pre_orders_process_pre_order_completion_payment_' . $this->id, array( $this->pre_orders, 'process_pre_order_release_payment' ) );
151
		}
152
	}
153
154
	/**
155
	 * Checks if gateway should be available to use.
156
	 *
157
	 * @since 4.0.2
158
	 */
159
	public function is_available() {
160
		if ( is_add_payment_method_page() && ! $this->saved_cards ) {
161
			return false;
162
		}
163
164
		return parent::is_available();
165
	}
166
167
	/**
168
	 * Adds a notice for customer when they update their billing address.
169
	 *
170
	 * @since 4.1.0
171
	 * @param int    $user_id      The ID of the current user.
172
	 * @param string $load_address The address to load.
173
	 */
174
	public function show_update_card_notice( $user_id, $load_address ) {
175
		if ( ! $this->saved_cards || ! WC_Stripe_Payment_Tokens::customer_has_saved_methods( $user_id ) || 'billing' !== $load_address ) {
176
			return;
177
		}
178
179
		/* translators: 1) Opening anchor tag 2) closing anchor tag */
180
		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' );
181
	}
182
183
	/**
184
	 * Get_icon function.
185
	 *
186
	 * @since 1.0.0
187
	 * @version 4.0.0
188
	 * @return string
189
	 */
190
	public function get_icon() {
191
		$icons = $this->payment_icons();
192
193
		$icons_str = '';
194
195
		$icons_str .= isset( $icons['visa'] ) ? $icons['visa'] : '';
196
		$icons_str .= isset( $icons['amex'] ) ? $icons['amex'] : '';
197
		$icons_str .= isset( $icons['mastercard'] ) ? $icons['mastercard'] : '';
198
199
		if ( 'USD' === get_woocommerce_currency() ) {
200
			$icons_str .= isset( $icons['discover'] ) ? $icons['discover'] : '';
201
			$icons_str .= isset( $icons['jcb'] ) ? $icons['jcb'] : '';
202
			$icons_str .= isset( $icons['diners'] ) ? $icons['diners'] : '';
203
		}
204
205
		return apply_filters( 'woocommerce_gateway_icon', $icons_str, $this->id );
206
	}
207
208
	/**
209
	 * Initialise Gateway Settings Form Fields
210
	 */
211
	public function init_form_fields() {
212
		$this->form_fields = require( dirname( __FILE__ ) . '/admin/stripe-settings.php' );
213
	}
214
215
	/**
216
	 * Payment form on checkout page
217
	 */
218
	public function payment_fields() {
219
		global $wp;
220
		$user                 = wp_get_current_user();
221
		$display_tokenization = $this->supports( 'tokenization' ) && is_checkout() && $this->saved_cards;
222
		$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...
223
		$user_email           = '';
224
		$description          = $this->get_description();
225
		$description          = ! empty( $description ) ? $description : '';
226
		$firstname            = '';
227
		$lastname             = '';
228
229
		// If paying from order, we need to get total from order not cart.
230
		if ( isset( $_GET['pay_for_order'] ) && ! empty( $_GET['key'] ) ) { // wpcs: csrf ok.
231
			$order      = wc_get_order( wc_clean( $wp->query_vars['order-pay'] ) ); // wpcs: csrf ok, sanitization ok.
232
			$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...
233
			$user_email = $order->get_billing_email();
234
		} else {
235
			if ( $user->ID ) {
236
				$user_email = get_user_meta( $user->ID, 'billing_email', true );
237
				$user_email = $user_email ? $user_email : $user->user_email;
238
			}
239
		}
240
241
		if ( is_add_payment_method_page() ) {
242
			$firstname       = $user->user_firstname;
243
			$lastname        = $user->user_lastname;
244
		}
245
246
		ob_start();
247
248
		echo '<div
249
			id="stripe-payment-data"
250
			data-email="' . esc_attr( $user_email ) . '"
251
			data-full-name="' . esc_attr( $firstname . ' ' . $lastname ) . '"
252
			data-currency="' . esc_attr( strtolower( get_woocommerce_currency() ) ) . '"
253
		>';
254
255
		if ( $this->testmode ) {
256
			/* translators: link to Stripe testing page */
257
			$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' );
258
		}
259
260
		$description = trim( $description );
261
262
		echo apply_filters( 'wc_stripe_description', wpautop( wp_kses_post( $description ) ), $this->id ); // wpcs: xss ok.
263
264
		if ( $display_tokenization ) {
265
			$this->tokenization_script();
266
			$this->saved_payment_methods();
267
		}
268
269
		$this->elements_form();
270
271 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...
272
273
			$this->save_payment_method_checkbox();
274
		}
275
276
		do_action( 'wc_stripe_cards_payment_fields', $this->id );
277
278
		echo '</div>';
279
280
		ob_end_flush();
281
	}
282
283
	/**
284
	 * Renders the Stripe elements form.
285
	 *
286
	 * @since 4.0.0
287
	 * @version 4.0.0
288
	 */
289
	public function elements_form() {
290
		?>
291
		<fieldset id="wc-<?php echo esc_attr( $this->id ); ?>-cc-form" class="wc-credit-card-form wc-payment-form" style="background:transparent;">
292
			<?php do_action( 'woocommerce_credit_card_form_start', $this->id ); ?>
293
294
			<?php if ( $this->inline_cc_form ) { ?>
295
				<label for="card-element">
296
					<?php esc_html_e( 'Credit or debit card', 'woocommerce-gateway-stripe' ); ?>
297
				</label>
298
299
				<div id="stripe-card-element" class="wc-stripe-elements-field">
300
				<!-- a Stripe Element will be inserted here. -->
301
				</div>
302
			<?php } else { ?>
303
				<div class="form-row form-row-wide">
304
					<label for="stripe-card-element"><?php esc_html_e( 'Card Number', 'woocommerce-gateway-stripe' ); ?> <span class="required">*</span></label>
305
					<div class="stripe-card-group">
306
						<div id="stripe-card-element" class="wc-stripe-elements-field">
307
						<!-- a Stripe Element will be inserted here. -->
308
						</div>
309
310
						<i class="stripe-credit-card-brand stripe-card-brand" alt="Credit Card"></i>
311
					</div>
312
				</div>
313
314
				<div class="form-row form-row-first">
315
					<label for="stripe-exp-element"><?php esc_html_e( 'Expiry Date', 'woocommerce-gateway-stripe' ); ?> <span class="required">*</span></label>
316
317
					<div id="stripe-exp-element" class="wc-stripe-elements-field">
318
					<!-- a Stripe Element will be inserted here. -->
319
					</div>
320
				</div>
321
322
				<div class="form-row form-row-last">
323
					<label for="stripe-cvc-element"><?php esc_html_e( 'Card Code (CVC)', 'woocommerce-gateway-stripe' ); ?> <span class="required">*</span></label>
324
				<div id="stripe-cvc-element" class="wc-stripe-elements-field">
325
				<!-- a Stripe Element will be inserted here. -->
326
				</div>
327
				</div>
328
				<div class="clear"></div>
329
			<?php } ?>
330
331
			<!-- Used to display form errors -->
332
			<div class="stripe-source-errors" role="alert"></div>
333
			<br />
334
			<?php do_action( 'woocommerce_credit_card_form_end', $this->id ); ?>
335
			<div class="clear"></div>
336
		</fieldset>
337
		<?php
338
	}
339
340
	/**
341
	 * Load admin scripts.
342
	 *
343
	 * @since 3.1.0
344
	 * @version 3.1.0
345
	 */
346
	public function admin_scripts() {
347
		if ( 'woocommerce_page_wc-settings' !== get_current_screen()->id ) {
348
			return;
349
		}
350
351
		$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
352
353
		wp_enqueue_script( 'woocommerce_stripe_admin', plugins_url( 'assets/js/stripe-admin' . $suffix . '.js', WC_STRIPE_MAIN_FILE ), array(), WC_STRIPE_VERSION, true );
354
		wp_localize_script(
355
			'woocommerce_stripe_admin',
356
			'woocommerce_stripe_admin',
357
			array(
358
				'ajax_url'              => WC_AJAX::get_endpoint( 'wc_stripe_oauth_init' ),
359
				'wc_stripe_oauth_nonce' => wp_create_nonce( '_wc_stripe_oauth_nonce' ),
360
				'wc_stripe_oauth_init'  => ! $this->secret_key && ! $this->publishable_key && ! get_option( 'stripe_state', false ),
361
				'i18n'                  => array(
362
					'wc_stripe_oauth_text' => sprintf(
363
						/* translators: 1) opening anchor tag 2) closing anchor tag */
364
						__( '%1$sSetup or link an existing Stripe Account%2$s', 'woocommerce-gateway-stripe' ),
365
						'<a href="#" class="button button-primary" id="oauth-init">',
366
						'</a>'
367
					),
368
				),
369
			)
370
		);
371
	}
372
373
	/**
374
	 * Payment_scripts function.
375
	 *
376
	 * Outputs scripts used for stripe payment
377
	 *
378
	 * @since 3.1.0
379
	 * @version 4.0.0
380
	 */
381
	public function payment_scripts() {
382
		global $wp;
383
		if (
384
			! is_product()
385
			&& ! is_cart()
386
			&& ! is_checkout()
387
			&& ! isset( $_GET['pay_for_order'] ) // wpcs: csrf ok.
388
			&& ! is_add_payment_method_page()
389
			&& ! isset( $_GET['change_payment_method'] ) // wpcs: csrf ok.
390
			&& ! ( ! 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() )
391
			|| ( is_order_received_page() )
392
		) {
393
			return;
394
		}
395
396
		// If Stripe is not enabled bail.
397
		if ( 'no' === $this->enabled ) {
398
			return;
399
		}
400
401
		// If keys are not set bail.
402
		if ( ! $this->are_keys_set() ) {
403
			WC_Stripe_Logger::log( 'Keys are not set correctly.' );
404
			return;
405
		}
406
407
		// If no SSL bail.
408
		if ( ! $this->testmode && ! is_ssl() ) {
409
			WC_Stripe_Logger::log( 'Stripe live mode requires SSL.' );
410
			return;
411
		}
412
413
		$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...
414
415
		$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
416
417
		wp_register_style( 'stripe_styles', plugins_url( 'assets/css/stripe-styles.css', WC_STRIPE_MAIN_FILE ), array(), WC_STRIPE_VERSION );
418
		wp_enqueue_style( 'stripe_styles' );
419
420
		wp_register_script( 'stripe', 'https://js.stripe.com/v3/', '', '3.0', true );
421
		wp_register_script( 'woocommerce_stripe', plugins_url( 'assets/js/stripe' . $suffix . '.js', WC_STRIPE_MAIN_FILE ), array( 'jquery-payment', 'stripe' ), WC_STRIPE_VERSION, true );
422
423
		$stripe_params = array(
424
			'key'                  => $this->publishable_key,
425
			'i18n_terms'           => __( 'Please accept the terms and conditions first', 'woocommerce-gateway-stripe' ),
426
			'i18n_required_fields' => __( 'Please fill in required checkout fields first', 'woocommerce-gateway-stripe' ),
427
		);
428
429
		// If we're on the pay page we need to pass stripe.js the address of the order.
430
		if ( isset( $_GET['pay_for_order'] ) && 'true' === $_GET['pay_for_order'] ) { // wpcs: csrf ok.
431
			$order_id = wc_clean( $wp->query_vars['order-pay'] ); // wpcs: csrf ok, sanitization ok, xss ok.
432
			$order    = wc_get_order( $order_id );
433
434
			if ( is_a( $order, 'WC_Order' ) ) {
435
				$stripe_params['billing_first_name'] = $order->get_billing_first_name();
436
				$stripe_params['billing_last_name']  = $order->get_billing_last_name();
437
				$stripe_params['billing_address_1']  = $order->get_billing_address_1();
438
				$stripe_params['billing_address_2']  = $order->get_billing_address_2();
439
				$stripe_params['billing_state']      = $order->get_billing_state();
440
				$stripe_params['billing_city']       = $order->get_billing_city();
441
				$stripe_params['billing_postcode']   = $order->get_billing_postcode();
442
				$stripe_params['billing_country']    = $order->get_billing_country();
443
			}
444
		}
445
446
		$sepa_elements_options = apply_filters(
447
			'wc_stripe_sepa_elements_options',
448
			array(
449
				'supportedCountries' => array( 'SEPA' ),
450
				'placeholderCountry' => WC()->countries->get_base_country(),
451
				'style'              => array( 'base' => array( 'fontSize' => '15px' ) ),
452
			)
453
		);
454
455
		$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' );
456
		$stripe_params['no_sepa_owner_msg']         = __( 'Please enter your IBAN account name.', 'woocommerce-gateway-stripe' );
457
		$stripe_params['no_sepa_iban_msg']          = __( 'Please enter your IBAN account number.', 'woocommerce-gateway-stripe' );
458
		$stripe_params['payment_intent_error']      = __( 'We couldn\'t initiate the payment. Please try again.', 'woocommerce-gateway-stripe' );
459
		$stripe_params['sepa_mandate_notification'] = apply_filters( 'wc_stripe_sepa_mandate_notification', 'email' );
460
		$stripe_params['allow_prepaid_card']        = apply_filters( 'wc_stripe_allow_prepaid_card', true ) ? 'yes' : 'no';
461
		$stripe_params['inline_cc_form']            = $this->inline_cc_form ? 'yes' : 'no';
462
		$stripe_params['is_checkout']               = ( is_checkout() && empty( $_GET['pay_for_order'] ) ) ? 'yes' : 'no'; // wpcs: csrf ok.
463
		$stripe_params['return_url']                = $this->get_stripe_return_url();
464
		$stripe_params['ajaxurl']                   = WC_AJAX::get_endpoint( '%%endpoint%%' );
465
		$stripe_params['stripe_nonce']              = wp_create_nonce( '_wc_stripe_nonce' );
466
		$stripe_params['statement_descriptor']      = $this->statement_descriptor;
467
		$stripe_params['elements_options']          = apply_filters( 'wc_stripe_elements_options', array() );
468
		$stripe_params['sepa_elements_options']     = $sepa_elements_options;
469
		$stripe_params['invalid_owner_name']        = __( 'Billing First Name and Last Name are required.', 'woocommerce-gateway-stripe' );
470
		$stripe_params['is_change_payment_page']    = isset( $_GET['change_payment_method'] ) ? 'yes' : 'no'; // wpcs: csrf ok.
471
		$stripe_params['is_add_payment_page']       = is_wc_endpoint_url( 'add-payment-method' ) ? 'yes' : 'no';
472
		$stripe_params['is_pay_for_order_page']     = is_wc_endpoint_url( 'order-pay' ) ? 'yes' : 'no';
473
		$stripe_params['elements_styling']          = apply_filters( 'wc_stripe_elements_styling', false );
474
		$stripe_params['elements_classes']          = apply_filters( 'wc_stripe_elements_classes', false );
475
476
		// Merge localized messages to be use in JS.
477
		$stripe_params = array_merge( $stripe_params, WC_Stripe_Helper::get_localized_messages() );
478
479
		wp_localize_script( 'woocommerce_stripe', 'wc_stripe_params', apply_filters( 'wc_stripe_params', $stripe_params ) );
480
481
		$this->tokenization_script();
482
		wp_enqueue_script( 'woocommerce_stripe' );
483
	}
484
485
	/**
486
	 * Checks if a source object represents a prepaid credit card and
487
	 * throws an exception if it is one, but that is not allowed.
488
	 *
489
	 * @since 4.2.0
490
	 * @param object $prepared_source The object with source details.
491
	 * @throws WC_Stripe_Exception An exception if the card is prepaid, but prepaid cards are not allowed.
492
	 */
493
	public function maybe_disallow_prepaid_card( $prepared_source ) {
494
		// Check if we don't allow prepaid credit cards.
495
		if ( apply_filters( 'wc_stripe_allow_prepaid_card', true ) || ! $this->is_prepaid_card( $prepared_source->source_object ) ) {
496
			return;
497
		}
498
499
		$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' );
500
		throw new WC_Stripe_Exception( print_r( $prepared_source->source_object, true ), $localized_message );
501
	}
502
503
	/**
504
	 * Checks whether a source exists.
505
	 *
506
	 * @since 4.2.0
507
	 * @param  object $prepared_source The source that should be verified.
508
	 * @throws WC_Stripe_Exception     An exception if the source ID is missing.
509
	 */
510
	public function check_source( $prepared_source ) {
511 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...
512
			$localized_message = __( 'Payment processing failed. Please retry.', 'woocommerce-gateway-stripe' );
513
			throw new WC_Stripe_Exception( print_r( $prepared_source, true ), $localized_message );
514
		}
515
	}
516
517
	/**
518
	 * Customer param wrong? The user may have been deleted on stripe's end. Remove customer_id. Can be retried without.
519
	 *
520
	 * @since 4.2.0
521
	 * @param object   $error The error that was returned from Stripe's API.
522
	 * @param WC_Order $order The order those payment is being processed.
523
	 * @return bool           A flag that indicates that the customer does not exist and should be removed.
524
	 */
525
	public function maybe_remove_non_existent_customer( $error, $order ) {
526
		if ( ! $this->is_no_such_customer_error( $error ) ) {
527
			return false;
528
		}
529
530
		delete_user_option( $order->get_customer_id(), '_stripe_customer_id' );
531
		$order->delete_meta_data( '_stripe_customer_id' );
532
		$order->save();
533
534
		return true;
535
	}
536
537
	/**
538
	 * Completes an order without a positive value.
539
	 *
540
	 * @since 4.2.0
541
	 * @param WC_Order $order             The order to complete.
542
	 * @param WC_Order $prepared_source   Payment source and customer data.
543
	 * @param boolean  $force_save_source Whether the payment source must be saved, like when dealing with a Subscription setup.
544
	 * @return array                      Redirection data for `process_payment`.
545
	 */
546
	public function complete_free_order( $order, $prepared_source, $force_save_source ) {
547
		if ( $force_save_source ) {
548
			$intent_secret = $this->setup_intent( $order, $prepared_source );
549
550
			if ( ! empty( $intent_secret ) ) {
551
				// `get_return_url()` must be called immediately before returning a value.
552
				return array(
553
					'result'              => 'success',
554
					'redirect'            => $this->get_return_url( $order ),
555
					'setup_intent_secret' => $intent_secret,
556
				);
557
			}
558
		}
559
560
		// Remove cart.
561
		WC()->cart->empty_cart();
562
563
		$order->payment_complete();
564
565
		// Return thank you page redirect.
566
		return array(
567
			'result'   => 'success',
568
			'redirect' => $this->get_return_url( $order ),
569
		);
570
	}
571
572
	/**
573
	 * Process the payment
574
	 *
575
	 * @since 1.0.0
576
	 * @since 4.1.0 Add 4th parameter to track previous error.
577
	 * @param int  $order_id Reference.
578
	 * @param bool $retry Should we retry on fail.
579
	 * @param bool $force_save_source Force save the payment source.
580
	 * @param mix  $previous_error Any error message from previous request.
581
	 * @param bool $use_order_source Whether to use the source, which should already be attached to the order.
582
	 *
583
	 * @throws Exception If payment will not be accepted.
584
	 * @return array|void
585
	 */
586
	public function process_payment( $order_id, $retry = true, $force_save_source = false, $previous_error = false, $use_order_source = false ) {
587
		try {
588
			$order = wc_get_order( $order_id );
589
590
			// ToDo: `process_pre_order` saves the source to the order for a later payment.
591
			// This might not work well with PaymentIntents.
592
			if ( $this->maybe_process_pre_orders( $order_id ) ) {
593
				return $this->pre_orders->process_pre_order( $order_id );
594
			}
595
596
			// Check whether there is an existing intent.
597
			$intent = $this->get_intent_from_order( $order );
598
			if ( isset( $intent->object ) && 'setup_intent' === $intent->object ) {
599
				$intent = false; // This function can only deal with *payment* intents
600
			}
601
602
			$stripe_customer_id = null;
603
			if ( $intent && ! empty( $intent->customer ) ) {
604
				$stripe_customer_id = $intent->customer;
605
			}
606
607
			// For some payments the source should already be present in the order.
608
			if ( $use_order_source ) {
609
				$prepared_source = $this->prepare_order_source( $order );
610
			} else {
611
				$prepared_source = $this->prepare_source( get_current_user_id(), $force_save_source, $stripe_customer_id );
612
			}
613
614
			$this->maybe_disallow_prepaid_card( $prepared_source );
615
			$this->check_source( $prepared_source );
616
			$this->save_source_to_order( $order, $prepared_source );
617
618
			if ( 0 >= $order->get_total() ) {
619
				return $this->complete_free_order( $order, $prepared_source, $force_save_source );
620
			}
621
622
			// This will throw exception if not valid.
623
			$this->validate_minimum_order_amount( $order );
624
625
			WC_Stripe_Logger::log( "Info: Begin processing payment for order $order_id for the amount of {$order->get_total()}" );
626
627
			if ( $intent ) {
628
				$intent = $this->update_existing_intent( $intent, $order, $prepared_source );
629
			} else {
630
				$intent = $this->create_intent( $order, $prepared_source );
631
			}
632
633
			// Confirm the intent after locking the order to make sure webhooks will not interfere.
634
			if ( empty( $intent->error ) ) {
635
				$this->lock_order_payment( $order, $intent );
636
				$intent = $this->confirm_intent( $intent, $order, $prepared_source );
637
			}
638
639
			if ( ! empty( $intent->error ) ) {
640
				$this->maybe_remove_non_existent_customer( $intent->error, $order );
641
642
				// We want to retry.
643
				if ( $this->is_retryable_error( $intent->error ) ) {
644
					return $this->retry_after_error( $intent, $order, $retry, $force_save_source, $previous_error, $use_order_source );
645
				}
646
647
				$this->unlock_order_payment( $order );
648
				$this->throw_localized_message( $intent, $order );
649
			}
650
651
			if ( ! empty( $intent ) ) {
652
				// Use the last charge within the intent to proceed.
653
				$response = end( $intent->charges->data );
654
655
				// If the intent requires a 3DS flow, redirect to it.
656
				if ( 'requires_action' === $intent->status ) {
657
					$this->unlock_order_payment( $order );
658
659
					if ( is_wc_endpoint_url( 'order-pay' ) ) {
660
						$redirect_url = add_query_arg( 'wc-stripe-confirmation', 1, $order->get_checkout_payment_url( false ) );
661
662
						return array(
663
							'result'   => 'success',
664
							'redirect' => $redirect_url,
665
						);
666
					} else {
667
						/**
668
						 * This URL contains only a hash, which will be sent to `checkout.js` where it will be set like this:
669
						 * `window.location = result.redirect`
670
						 * Once this redirect is sent to JS, the `onHashChange` function will execute `handleCardPayment`.
671
						 */
672
673
						return array(
674
							'result'                => 'success',
675
							'redirect'              => $this->get_return_url( $order ),
676
							'payment_intent_secret' => $intent->client_secret,
677
						);
678
					}
679
				}
680
			}
681
682
			// Process valid response.
683
			$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...
684
685
			// Remove cart.
686
			if ( isset( WC()->cart ) ) {
687
				WC()->cart->empty_cart();
688
			}
689
690
			// Unlock the order.
691
			$this->unlock_order_payment( $order );
692
693
			// Return thank you page redirect.
694
			return array(
695
				'result'   => 'success',
696
				'redirect' => $this->get_return_url( $order ),
697
			);
698
699
		} catch ( WC_Stripe_Exception $e ) {
700
			wc_add_notice( $e->getLocalizedMessage(), 'error' );
701
			WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() );
702
703
			do_action( 'wc_gateway_stripe_process_payment_error', $e, $order );
704
705
			/* translators: error message */
706
			$order->update_status( 'failed' );
707
708
			return array(
709
				'result'   => 'fail',
710
				'redirect' => '',
711
			);
712
		}
713
	}
714
715
	/**
716
	 * Displays the Stripe fee
717
	 *
718
	 * @since 4.1.0
719
	 *
720
	 * @param int $order_id The ID of the order.
721
	 */
722 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...
723
		if ( apply_filters( 'wc_stripe_hide_display_order_fee', false, $order_id ) ) {
724
			return;
725
		}
726
727
		$order = wc_get_order( $order_id );
728
729
		$fee      = WC_Stripe_Helper::get_stripe_fee( $order );
730
		$currency = WC_Stripe_Helper::get_stripe_currency( $order );
731
732
		if ( ! $fee || ! $currency ) {
733
			return;
734
		}
735
736
		?>
737
738
		<tr>
739
			<td class="label stripe-fee">
740
				<?php echo wc_help_tip( __( 'This represents the fee Stripe collects for the transaction.', 'woocommerce-gateway-stripe' ) ); // wpcs: xss ok. ?>
741
				<?php esc_html_e( 'Stripe Fee:', 'woocommerce-gateway-stripe' ); ?>
742
			</td>
743
			<td width="1%"></td>
744
			<td class="total">
745
				-&nbsp;<?php echo wc_price( $fee, array( 'currency' => $currency ) ); // wpcs: xss ok. ?>
746
			</td>
747
		</tr>
748
749
		<?php
750
	}
751
752
	/**
753
	 * Displays the net total of the transaction without the charges of Stripe.
754
	 *
755
	 * @since 4.1.0
756
	 *
757
	 * @param int $order_id The ID of the order.
758
	 */
759 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...
760
		if ( apply_filters( 'wc_stripe_hide_display_order_payout', false, $order_id ) ) {
761
			return;
762
		}
763
764
		$order = wc_get_order( $order_id );
765
766
		$net      = WC_Stripe_Helper::get_stripe_net( $order );
767
		$currency = WC_Stripe_Helper::get_stripe_currency( $order );
768
769
		if ( ! $net || ! $currency ) {
770
			return;
771
		}
772
773
		?>
774
775
		<tr>
776
			<td class="label stripe-payout">
777
				<?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. ?>
778
				<?php esc_html_e( 'Stripe Payout:', 'woocommerce-gateway-stripe' ); ?>
779
			</td>
780
			<td width="1%"></td>
781
			<td class="total">
782
				<?php echo wc_price( $net, array( 'currency' => $currency ) ); // wpcs: xss ok. ?>
783
			</td>
784
		</tr>
785
786
		<?php
787
	}
788
789
	/**
790
	 * Generates a localized message for an error from a response.
791
	 *
792
	 * @since 4.3.2
793
	 *
794
	 * @param stdClass $response The response from the Stripe API.
795
	 *
796
	 * @return string The localized error message.
797
	 */
798
	public function get_localized_error_message_from_response( $response ) {
799
		$localized_messages = WC_Stripe_Helper::get_localized_messages();
800
801
		if ( 'card_error' === $response->error->type ) {
802
			$localized_message = isset( $localized_messages[ $response->error->code ] ) ? $localized_messages[ $response->error->code ] : $response->error->message;
803
		} else {
804
			$localized_message = isset( $localized_messages[ $response->error->type ] ) ? $localized_messages[ $response->error->type ] : $response->error->message;
805
		}
806
807
		return $localized_message;
808
	}
809
810
	/**
811
	 * Gets a localized message for an error from a response, adds it as a note to the order, and throws it.
812
	 *
813
	 * @since 4.2.0
814
	 * @param  stdClass $response  The response from the Stripe API.
815
	 * @param  WC_Order $order     The order to add a note to.
816
	 * @throws WC_Stripe_Exception An exception with the right message.
817
	 */
818
	public function throw_localized_message( $response, $order ) {
819
		$localized_message = $this->get_localized_error_message_from_response( $response );
820
821
		$order->add_order_note( $localized_message );
822
823
		throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message );
824
	}
825
826
	/**
827
	 * Retries the payment process once an error occured.
828
	 *
829
	 * @since 4.2.0
830
	 * @param object   $response          The response from the Stripe API.
831
	 * @param WC_Order $order             An order that is being paid for.
832
	 * @param bool     $retry             A flag that indicates whether another retry should be attempted.
833
	 * @param bool     $force_save_source Force save the payment source.
834
	 * @param mixed    $previous_error    Any error message from previous request.
835
	 * @param bool     $use_order_source  Whether to use the source, which should already be attached to the order.
836
	 * @throws WC_Stripe_Exception        If the payment is not accepted.
837
	 * @return array|void
838
	 */
839
	public function retry_after_error( $response, $order, $retry, $force_save_source, $previous_error, $use_order_source ) {
840
		if ( ! $retry ) {
841
			$localized_message = __( 'Sorry, we are unable to process your payment at this time. Please retry later.', 'woocommerce-gateway-stripe' );
842
			$order->add_order_note( $localized_message );
843
			throw new WC_Stripe_Exception( print_r( $response, true ), $localized_message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.
844
		}
845
846
		// Don't do anymore retries after this.
847
		if ( 5 <= $this->retry_interval ) {
848
			return $this->process_payment( $order->get_id(), false, $force_save_source, $response->error, $previous_error );
849
		}
850
851
		sleep( $this->retry_interval );
852
		$this->retry_interval++;
853
854
		return $this->process_payment( $order->get_id(), true, $force_save_source, $response->error, $previous_error, $use_order_source );
855
	}
856
857
	/**
858
	 * Adds the necessary hooks to modify the "Pay for order" page in order to clean
859
	 * it up and prepare it for the Stripe PaymentIntents modal to confirm a payment.
860
	 *
861
	 * @since 4.2
862
	 * @param WC_Payment_Gateway[] $gateways A list of all available gateways.
863
	 * @return WC_Payment_Gateway[]          Either the same list or an empty one in the right conditions.
864
	 */
865
	public function prepare_order_pay_page( $gateways ) {
866
		if ( ! is_wc_endpoint_url( 'order-pay' ) || ! isset( $_GET['wc-stripe-confirmation'] ) ) { // wpcs: csrf ok.
867
			return $gateways;
868
		}
869
870
		try {
871
			$this->prepare_intent_for_order_pay_page();
872
		} catch ( WC_Stripe_Exception $e ) {
873
			// Just show the full order pay page if there was a problem preparing the Payment Intent
874
			return $gateways;
875
		}
876
877
		add_filter( 'woocommerce_checkout_show_terms', '__return_false' );
878
		add_filter( 'woocommerce_pay_order_button_html', '__return_false' );
879
		add_filter( 'woocommerce_available_payment_gateways', '__return_empty_array' );
880
		add_filter( 'woocommerce_no_available_payment_methods_message', array( $this, 'change_no_available_methods_message' ) );
881
		add_action( 'woocommerce_pay_order_after_submit', array( $this, 'render_payment_intent_inputs' ) );
882
883
		return array();
884
	}
885
886
	/**
887
	 * Changes the text of the "No available methods" message to one that indicates
888
	 * the need for a PaymentIntent to be confirmed.
889
	 *
890
	 * @since 4.2
891
	 * @return string the new message.
892
	 */
893
	public function change_no_available_methods_message() {
894
		return wpautop( __( "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' ) );
895
	}
896
897
	/**
898
	 * Prepares the Payment Intent for it to be completed in the "Pay for Order" page.
899
	 *
900
	 * @param WC_Order|null $order Order object, or null to get the order from the "order-pay" URL parameter
901
	 *
902
	 * @throws WC_Stripe_Exception
903
	 * @since 4.3
904
	 */
905
	public function prepare_intent_for_order_pay_page( $order = null ) {
906 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...
907
			$order = wc_get_order( absint( get_query_var( 'order-pay' ) ) );
908
		}
909
		$intent = $this->get_intent_from_order( $order );
910
911
		if ( ! $intent ) {
912
			throw new WC_Stripe_Exception( 'Payment Intent not found', __( 'Payment Intent not found for order #' . $order->get_id(), 'woocommerce-gateway-stripe' ) );
913
		}
914
915
		if ( 'requires_payment_method' === $intent->status && isset( $intent->last_payment_error )
916
		     && 'authentication_required' === $intent->last_payment_error->code ) {
917
			$level3_data = $this->get_level3_data_from_order( $order );
918
			$intent      = WC_Stripe_API::request_with_level3_data(
919
				array(
920
					'payment_method' => $intent->last_payment_error->source->id,
921
				),
922
				'payment_intents/' . $intent->id . '/confirm',
923
				$level3_data,
924
				$order
925
			);
926
927
			if ( isset( $intent->error ) ) {
928
				throw new WC_Stripe_Exception( print_r( $intent, true ), $intent->error->message );
929
			}
930
		}
931
932
		$this->order_pay_intent = $intent;
933
	}
934
935
	/**
936
	 * Renders hidden inputs on the "Pay for Order" page in order to let Stripe handle PaymentIntents.
937
	 *
938
	 * @param WC_Order|null $order Order object, or null to get the order from the "order-pay" URL parameter
939
	 *
940
	 * @throws WC_Stripe_Exception
941
	 * @since 4.2
942
	 */
943
	public function render_payment_intent_inputs( $order = null ) {
944 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...
945
			$order = wc_get_order( absint( get_query_var( 'order-pay' ) ) );
946
		}
947
		if ( ! isset( $this->order_pay_intent ) ) {
948
			$this->prepare_intent_for_order_pay_page( $order );
949
		}
950
951
		$verification_url = add_query_arg(
952
			array(
953
				'order'            => $order->get_id(),
954
				'nonce'            => wp_create_nonce( 'wc_stripe_confirm_pi' ),
955
				'redirect_to'      => rawurlencode( $this->get_return_url( $order ) ),
956
				'is_pay_for_order' => true,
957
			),
958
			WC_AJAX::get_endpoint( 'wc_stripe_verify_intent' )
959
		);
960
961
		echo '<input type="hidden" id="stripe-intent-id" value="' . esc_attr( $this->order_pay_intent->client_secret ) . '" />';
962
		echo '<input type="hidden" id="stripe-intent-return" value="' . esc_attr( $verification_url ) . '" />';
963
	}
964
965
	/**
966
	 * Adds an error message wrapper to each saved method.
967
	 *
968
	 * @since 4.2.0
969
	 * @param WC_Payment_Token $token Payment Token.
970
	 * @return string                 Generated payment method HTML
971
	 */
972
	public function get_saved_payment_method_option_html( $token ) {
973
		$html          = parent::get_saved_payment_method_option_html( $token );
974
		$error_wrapper = '<div class="stripe-source-errors" role="alert"></div>';
975
976
		return preg_replace( '~</(\w+)>\s*$~', "$error_wrapper</$1>", $html );
977
	}
978
979
	/**
980
	 * Attempt to manually complete the payment process for orders, which are still pending
981
	 * before displaying the View Order page. This is useful in case webhooks have not been set up.
982
	 *
983
	 * @since 4.2.0
984
	 * @param int $order_id The ID that will be used for the thank you page.
985
	 */
986
	public function check_intent_status_on_order_page( $order_id ) {
987
		if ( empty( $order_id ) || absint( $order_id ) <= 0 ) {
988
			return;
989
		}
990
991
		$order = wc_get_order( absint( $order_id ) );
992
993
		if ( ! $order ) {
994
			return;
995
		}
996
997
		$this->verify_intent_after_checkout( $order );
998
	}
999
1000
	/**
1001
	 * Attached to `woocommerce_payment_successful_result` with a late priority,
1002
	 * this method will combine the "naturally" generated redirect URL from
1003
	 * WooCommerce and a payment/setup intent secret into a hash, which contains both
1004
	 * the secret, and a proper URL, which will confirm whether the intent succeeded.
1005
	 *
1006
	 * @since 4.2.0
1007
	 * @param array $result   The result from `process_payment`.
1008
	 * @param int   $order_id The ID of the order which is being paid for.
1009
	 * @return array
1010
	 */
1011
	public function modify_successful_payment_result( $result, $order_id ) {
1012
		if ( ! isset( $result['payment_intent_secret'] ) && ! isset( $result['setup_intent_secret'] ) ) {
1013
			// Only redirects with intents need to be modified.
1014
			return $result;
1015
		}
1016
1017
		// Put the final thank you page redirect into the verification URL.
1018
		$verification_url = add_query_arg(
1019
			array(
1020
				'order'       => $order_id,
1021
				'nonce'       => wp_create_nonce( 'wc_stripe_confirm_pi' ),
1022
				'redirect_to' => rawurlencode( $result['redirect'] ),
1023
			),
1024
			WC_AJAX::get_endpoint( 'wc_stripe_verify_intent' )
1025
		);
1026
1027
		if ( isset( $result['payment_intent_secret'] ) ) {
1028
			$redirect = sprintf( '#confirm-pi-%s:%s', $result['payment_intent_secret'], rawurlencode( $verification_url ) );
1029
		} else if ( isset( $result['setup_intent_secret'] ) ) {
1030
			$redirect = sprintf( '#confirm-si-%s:%s', $result['setup_intent_secret'], rawurlencode( $verification_url ) );
1031
		}
1032
1033
		return array(
1034
			'result'   => 'success',
1035
			'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...
1036
		);
1037
	}
1038
1039
	/**
1040
	 * Proceed with current request using new login session (to ensure consistent nonce).
1041
	 */
1042
	public function set_cookie_on_current_request( $cookie ) {
1043
		$_COOKIE[ LOGGED_IN_COOKIE ] = $cookie;
1044
	}
1045
1046
	/**
1047
	 * Executed between the "Checkout" and "Thank you" pages, this
1048
	 * method updates orders based on the status of associated PaymentIntents.
1049
	 *
1050
	 * @since 4.2.0
1051
	 * @param WC_Order $order The order which is in a transitional state.
1052
	 */
1053
	public function verify_intent_after_checkout( $order ) {
1054
		$payment_method = $order->get_payment_method();
1055
		if ( $payment_method !== $this->id ) {
1056
			// If this is not the payment method, an intent would not be available.
1057
			return;
1058
		}
1059
1060
		$intent = $this->get_intent_from_order( $order );
1061
		if ( ! $intent ) {
1062
			// No intent, redirect to the order received page for further actions.
1063
			return;
1064
		}
1065
1066
		// A webhook might have modified or locked the order while the intent was retreived. This ensures we are reading the right status.
1067
		clean_post_cache( $order->get_id() );
1068
		$order = wc_get_order( $order->get_id() );
1069
1070
		if ( ! $order->has_status( array( 'pending', 'failed' ) ) ) {
1071
			// If payment has already been completed, this function is redundant.
1072
			return;
1073
		}
1074
1075
		if ( $this->lock_order_payment( $order, $intent ) ) {
1076
			return;
1077
		}
1078
1079
		if ( 'setup_intent' === $intent->object && 'succeeded' === $intent->status ) {
1080
			WC()->cart->empty_cart();
1081
			if ( WC_Stripe_Helper::is_pre_orders_exists() && WC_Pre_Orders_Order::order_contains_pre_order( $order ) ) {
1082
				WC_Pre_Orders_Order::mark_order_as_pre_ordered( $order );
1083
			} else {
1084
				$order->payment_complete();
1085
			}
1086
		} else if ( 'succeeded' === $intent->status || 'requires_capture' === $intent->status ) {
1087
			// Proceed with the payment completion.
1088
			$this->handle_intent_verification_success( $order, $intent );
1089
		} else if ( 'requires_payment_method' === $intent->status ) {
1090
			// `requires_payment_method` means that SCA got denied for the current payment method.
1091
			$this->handle_intent_verification_failure( $order, $intent );
1092
		}
1093
1094
		$this->unlock_order_payment( $order );
1095
	}
1096
1097
	/**
1098
	 * Called after an intent verification succeeds, this allows
1099
	 * specific APNs or children of this class to modify its behavior.
1100
	 *
1101
	 * @param WC_Order $order The order whose verification succeeded.
1102
	 * @param stdClass $intent The Payment Intent object.
1103
	 */
1104
	protected function handle_intent_verification_success( $order, $intent ) {
1105
		$this->process_response( end( $intent->charges->data ), $order );
1106
	}
1107
1108
	/**
1109
	 * Called after an intent verification fails, this allows
1110
	 * specific APNs or children of this class to modify its behavior.
1111
	 *
1112
	 * @param WC_Order $order The order whose verification failed.
1113
	 * @param stdClass $intent The Payment Intent object.
1114
	 */
1115
	protected function handle_intent_verification_failure( $order, $intent ) {
1116
		$this->failed_sca_auth( $order, $intent );
1117
	}
1118
1119
	/**
1120
	 * Checks if the payment intent associated with an order failed and records the event.
1121
	 *
1122
	 * @since 4.2.0
1123
	 * @param WC_Order $order  The order which should be checked.
1124
	 * @param object   $intent The intent, associated with the order.
1125
	 */
1126
	public function failed_sca_auth( $order, $intent ) {
1127
		// If the order has already failed, do not repeat the same message.
1128
		if ( $order->has_status( 'failed' ) ) {
1129
			return;
1130
		}
1131
1132
		// Load the right message and update the status.
1133
		$status_message = isset( $intent->last_payment_error )
1134
			/* translators: 1) The error message that was received from Stripe. */
1135
			? sprintf( __( 'Stripe SCA authentication failed. Reason: %s', 'woocommerce-gateway-stripe' ), $intent->last_payment_error->message )
1136
			: __( 'Stripe SCA authentication failed.', 'woocommerce-gateway-stripe' );
1137
		$order->update_status( 'failed', $status_message );
1138
	}
1139
1140
	/**
1141
	 * Preserves the "wc-stripe-confirmation" URL parameter so the user can complete the SCA authentication after logging in.
1142
	 *
1143
	 * @param string $pay_url Current computed checkout URL for the given order.
1144
	 * @param WC_Order $order Order object.
1145
	 *
1146
	 * @return string Checkout URL for the given order.
1147
	 */
1148
	public function get_checkout_payment_url( $pay_url, $order ) {
1149
		global $wp;
1150
		if ( isset( $_GET['wc-stripe-confirmation'] ) && isset( $wp->query_vars['order-pay'] ) && $wp->query_vars['order-pay'] == $order->get_id() ) {
1151
			$pay_url = add_query_arg( 'wc-stripe-confirmation', 1, $pay_url );
1152
		}
1153
		return $pay_url;
1154
	}
1155
1156
	/**
1157
	 * Checks whether new keys are being entered when saving options.
1158
	 */
1159
	public function process_admin_options() {
1160
		// Load all old values before the new settings get saved.
1161
		$old_publishable_key      = $this->get_option( 'publishable_key' );
1162
		$old_secret_key           = $this->get_option( 'secret_key' );
1163
		$old_test_publishable_key = $this->get_option( 'test_publishable_key' );
1164
		$old_test_secret_key      = $this->get_option( 'test_secret_key' );
1165
1166
		parent::process_admin_options();
1167
1168
		// Load all old values after the new settings have been saved.
1169
		$new_publishable_key      = $this->get_option( 'publishable_key' );
1170
		$new_secret_key           = $this->get_option( 'secret_key' );
1171
		$new_test_publishable_key = $this->get_option( 'test_publishable_key' );
1172
		$new_test_secret_key      = $this->get_option( 'test_secret_key' );
1173
1174
		// Checks whether a value has transitioned from a non-empty value to a new one.
1175
		$has_changed = function( $old_value, $new_value ) {
1176
			return ! empty( $old_value ) && ( $old_value !== $new_value );
1177
		};
1178
1179
		// Look for updates.
1180
		if (
1181
			$has_changed( $old_publishable_key, $new_publishable_key )
1182
			|| $has_changed( $old_secret_key, $new_secret_key )
1183
			|| $has_changed( $old_test_publishable_key, $new_test_publishable_key )
1184
			|| $has_changed( $old_test_secret_key, $new_test_secret_key )
1185
		) {
1186
			update_option( 'wc_stripe_show_changed_keys_notice', 'yes' );
1187
		}
1188
	}
1189
1190 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...
1191
		$value = $this->validate_text_field( $key, $value );
1192
		if ( ! empty( $value ) && ! preg_match( '/^pk_live_/', $value ) ) {
1193
			throw new Exception( __( 'The "Live Publishable Key" should start with "pk_live", enter the correct key.', 'woocommerce-gateway-stripe' ) );
1194
		}
1195
		return $value;
1196
	}
1197
1198 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...
1199
		$value = $this->validate_text_field( $key, $value );
1200
		if ( ! empty( $value ) && ! preg_match( '/^[rs]k_live_/', $value ) ) {
1201
			throw new Exception( __( 'The "Live Secret Key" should start with "sk_live" or "rk_live", enter the correct key.', 'woocommerce-gateway-stripe' ) );
1202
		}
1203
		return $value;
1204
	}
1205
1206 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...
1207
		$value = $this->validate_text_field( $key, $value );
1208
		if ( ! empty( $value ) && ! preg_match( '/^pk_test_/', $value ) ) {
1209
			throw new Exception( __( 'The "Test Publishable Key" should start with "pk_test", enter the correct key.', 'woocommerce-gateway-stripe' ) );
1210
		}
1211
		return $value;
1212
	}
1213
1214 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...
1215
		$value = $this->validate_text_field( $key, $value );
1216
		if ( ! empty( $value ) && ! preg_match( '/^[rs]k_test_/', $value ) ) {
1217
			throw new Exception( __( 'The "Test Secret Key" should start with "sk_test" or "rk_test", enter the correct key.', 'woocommerce-gateway-stripe' ) );
1218
		}
1219
		return $value;
1220
	}
1221
}
1222