WC_Checkout   D
last analyzed

Complexity

Total Complexity 187

Size/Duplication

Total Lines 801
Duplicated Lines 4.99 %

Coupling/Cohesion

Components 2
Dependencies 6

Importance

Changes 0
Metric Value
dl 40
loc 801
rs 4.4444
c 0
b 0
f 0
wmc 187
lcom 2
cbo 6

11 Methods

Rating   Name   Duplication   Size   Complexity  
A instance() 0 5 2
A __clone() 0 3 1
A __wakeup() 0 3 1
C __construct() 16 43 7
A check_cart_items() 0 4 1
A checkout_form_billing() 0 3 1
A checkout_form_shipping() 0 3 1
F create_order() 12 177 33
B get_posted_address_data() 0 14 8
C get_value() 0 50 22
F process_checkout() 12 387 110

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like WC_Checkout often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WC_Checkout, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
if ( ! defined( 'ABSPATH' ) ) {
4
	exit; // Exit if accessed directly
5
}
6
7
/**
8
 * Checkout
9
 *
10
 * The WooCommerce checkout class handles the checkout process, collecting user data and processing the payment.
11
 *
12
 * @class 		WC_Checkout
13
 * @version		2.1.0
14
 * @package		WooCommerce/Classes
15
 * @category		Class
16
 * @author 		WooThemes
17
 */
18
class WC_Checkout {
19
20
	/** @var array Array of posted form data. */
21
	public $posted;
22
23
	/** @var array Array of fields to display on the checkout. */
24
	public $checkout_fields;
25
26
	/** @var bool Whether or not the user must create an account to checkout. */
27
	public $must_create_account;
28
29
	/** @var bool Whether or not signups are allowed. */
30
	public $enable_signup;
31
32
	/** @var object The shipping method being used. */
33
	private $shipping_method;
0 ignored issues
show
Unused Code introduced by
The property $shipping_method is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
34
35
	/** @var WC_Payment_Gateway|string The payment gateway being used. */
36
	private $payment_method;
37
38
	/** @var int ID of customer. */
39
	private $customer_id;
40
41
	/** @var array Where shipping_methods are stored. */
42
	public $shipping_methods;
43
44
	/**
45
	 * @var WC_Checkout The single instance of the class
46
	 * @since 2.1
47
	 */
48
	protected static $_instance = null;
49
50
	/** @var Bool */
51
	public $enable_guest_checkout;
52
53
	/**
54
	 * Main WC_Checkout Instance.
55
	 *
56
	 * Ensures only one instance of WC_Checkout is loaded or can be loaded.
57
	 *
58
	 * @since 2.1
59
	 * @static
60
	 * @return WC_Checkout Main instance
61
	 */
62
	public static function instance() {
63
		if ( is_null( self::$_instance ) )
64
			self::$_instance = new self();
65
		return self::$_instance;
66
	}
67
68
	/**
69
	 * Cloning is forbidden.
70
	 *
71
	 * @since 2.1
72
	 */
73
	public function __clone() {
74
		_doing_it_wrong( __FUNCTION__, __( 'Cheatin&#8217; huh?', 'woocommerce' ), '2.1' );
75
	}
76
77
	/**
78
	 * Unserializing instances of this class is forbidden.
79
	 *
80
	 * @since 2.1
81
	 */
82
	public function __wakeup() {
83
		_doing_it_wrong( __FUNCTION__, __( 'Cheatin&#8217; huh?', 'woocommerce' ), '2.1' );
84
	}
85
86
	/**
87
	 * Constructor for the checkout class. Hooks in methods and defines checkout fields.
88
	 *
89
	 * @access public
90
	 */
91
	public function __construct () {
92
		add_action( 'woocommerce_checkout_billing', array( $this,'checkout_form_billing' ) );
93
		add_action( 'woocommerce_checkout_shipping', array( $this,'checkout_form_shipping' ) );
94
95
		$this->enable_signup         = get_option( 'woocommerce_enable_signup_and_login_from_checkout' ) == 'yes' ? true : false;
96
		$this->enable_guest_checkout = get_option( 'woocommerce_enable_guest_checkout' ) == 'yes' ? true : false;
97
		$this->must_create_account   = $this->enable_guest_checkout || is_user_logged_in() ? false : true;
98
99
		// Define all Checkout fields
100
		$this->checkout_fields['billing'] 	= WC()->countries->get_address_fields( $this->get_value( 'billing_country' ), 'billing_' );
101
		$this->checkout_fields['shipping'] 	= WC()->countries->get_address_fields( $this->get_value( 'shipping_country' ), 'shipping_' );
102
103 View Code Duplication
		if ( get_option( 'woocommerce_registration_generate_username' ) == 'no' ) {
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...
104
			$this->checkout_fields['account']['account_username'] = array(
105
				'type' 			=> 'text',
106
				'label' 		=> __( 'Account username', 'woocommerce' ),
107
				'required'      => true,
108
				'placeholder' 	=> _x( 'Username', 'placeholder', 'woocommerce' )
109
			);
110
		}
111
112 View Code Duplication
		if ( get_option( 'woocommerce_registration_generate_password' ) == 'no' ) {
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...
113
			$this->checkout_fields['account']['account_password'] = array(
114
				'type' 				=> 'password',
115
				'label' 			=> __( 'Account password', 'woocommerce' ),
116
				'required'          => true,
117
				'placeholder' 		=> _x( 'Password', 'placeholder', 'woocommerce' )
118
			);
119
		}
120
121
		$this->checkout_fields['order']	= array(
122
			'order_comments' => array(
123
				'type' => 'textarea',
124
				'class' => array('notes'),
125
				'label' => __( 'Order Notes', 'woocommerce' ),
126
				'placeholder' => _x('Notes about your order, e.g. special notes for delivery.', 'placeholder', 'woocommerce')
127
			)
128
		);
129
130
		$this->checkout_fields = apply_filters( 'woocommerce_checkout_fields', $this->checkout_fields );
131
132
		do_action( 'woocommerce_checkout_init', $this );
133
	}
134
135
	/**
136
	 * Checkout process.
137
	 */
138
	public function check_cart_items() {
139
		// When we process the checkout, lets ensure cart items are rechecked to prevent checkout
140
		do_action('woocommerce_check_cart_items');
141
	}
142
143
	/**
144
	 * Output the billing information form.
145
	 */
146
	public function checkout_form_billing() {
147
		wc_get_template( 'checkout/form-billing.php', array( 'checkout' => $this ) );
148
	}
149
150
	/**
151
	 * Output the shipping information form.
152
	 */
153
	public function checkout_form_shipping() {
154
		wc_get_template( 'checkout/form-shipping.php', array( 'checkout' => $this ) );
155
	}
156
157
	/**
158
	 * Create an order. Error codes:
159
	 * 		520 - Cannot insert order into the database.
160
	 * 		521 - Cannot get order after creation.
161
	 * 		522 - Cannot update order.
162
	 * 		525 - Cannot create line item.
163
	 * 		526 - Cannot create fee item.
164
	 * 		527 - Cannot create shipping item.
165
	 * 		528 - Cannot create tax item.
166
	 * 		529 - Cannot create coupon item.
167
	 * @access public
168
	 * @throws Exception
169
	 * @return int|WP_ERROR
170
	 */
171
	public function create_order() {
172
		global $wpdb;
173
174
		// Give plugins the opportunity to create an order themselves
175
		if ( $order_id = apply_filters( 'woocommerce_create_order', null, $this ) ) {
176
			return $order_id;
177
		}
178
179
		try {
180
			// Start transaction if available
181
			wc_transaction_query( 'start' );
182
183
			$order_data = array(
184
				'status'        => apply_filters( 'woocommerce_default_order_status', 'pending' ),
185
				'customer_id'   => $this->customer_id,
186
				'customer_note' => isset( $this->posted['order_comments'] ) ? $this->posted['order_comments'] : '',
187
				'cart_hash'     => md5( json_encode( wc_clean( WC()->cart->get_cart_for_session() ) ) . WC()->cart->total ),
188
				'created_via'   => 'checkout',
189
			);
190
191
			// Insert or update the post data
192
			$order_id = absint( WC()->session->order_awaiting_payment );
193
194
			/**
195
			 * If there is an order pending payment, we can resume it here so
196
			 * long as it has not changed. If the order has changed, i.e.
197
			 * different items or cost, create a new order. We use a hash to
198
			 * detect changes which is based on cart items + order total.
199
			 */
200
			if ( $order_id && $order_data['cart_hash'] === get_post_meta( $order_id, '_cart_hash', true ) && ( $order = wc_get_order( $order_id ) ) && $order->has_status( array( 'pending', 'failed' ) ) ) {
201
202
				$order_data['order_id'] = $order_id;
203
				$order                  = wc_update_order( $order_data );
204
205
				if ( is_wp_error( $order ) ) {
206
					throw new Exception( sprintf( __( 'Error %d: Unable to create order. Please try again.', 'woocommerce' ), 522 ) );
207
				} else {
208
					$order->remove_order_items();
209
					do_action( 'woocommerce_resume_order', $order_id );
210
				}
211
212
			} else {
213
214
				$order = wc_create_order( $order_data );
215
216
				if ( is_wp_error( $order ) ) {
217
					throw new Exception( sprintf( __( 'Error %d: Unable to create order. Please try again.', 'woocommerce' ), 520 ) );
218
				} elseif ( false === $order ) {
219
					throw new Exception( sprintf( __( 'Error %d: Unable to create order. Please try again.', 'woocommerce' ), 521 ) );
220
				} else {
221
					$order_id = $order->id;
222
					do_action( 'woocommerce_new_order', $order_id );
223
				}
224
			}
225
226
			// Store the line items to the new/resumed order
227
			foreach ( WC()->cart->get_cart() as $cart_item_key => $values ) {
228
				$item_id = $order->add_product(
229
					$values['data'],
230
					$values['quantity'],
231
					array(
232
						'variation' => $values['variation'],
233
						'totals'    => array(
234
							'subtotal'     => $values['line_subtotal'],
235
							'subtotal_tax' => $values['line_subtotal_tax'],
236
							'total'        => $values['line_total'],
237
							'tax'          => $values['line_tax'],
238
							'tax_data'     => $values['line_tax_data'] // Since 2.2
239
						)
240
					)
241
				);
242
243
				if ( ! $item_id ) {
244
					throw new Exception( sprintf( __( 'Error %d: Unable to create order. Please try again.', 'woocommerce' ), 525 ) );
245
				}
246
247
				// Allow plugins to add order item meta
248
				do_action( 'woocommerce_add_order_item_meta', $item_id, $values, $cart_item_key );
249
			}
250
251
			// Store fees
252
			foreach ( WC()->cart->get_fees() as $fee_key => $fee ) {
253
				$item_id = $order->add_fee( $fee );
254
255
				if ( ! $item_id ) {
256
					throw new Exception( sprintf( __( 'Error %d: Unable to create order. Please try again.', 'woocommerce' ), 526 ) );
257
				}
258
259
				// Allow plugins to add order item meta to fees
260
				do_action( 'woocommerce_add_order_fee_meta', $order_id, $item_id, $fee, $fee_key );
261
			}
262
263
			// Store shipping for all packages
264
			foreach ( WC()->shipping->get_packages() as $package_key => $package ) {
265
				if ( isset( $package['rates'][ $this->shipping_methods[ $package_key ] ] ) ) {
266
					$item_id = $order->add_shipping( $package['rates'][ $this->shipping_methods[ $package_key ] ] );
267
268
					if ( ! $item_id ) {
269
						throw new Exception( sprintf( __( 'Error %d: Unable to create order. Please try again.', 'woocommerce' ), 527 ) );
270
					}
271
272
					// Allows plugins to add order item meta to shipping
273
					do_action( 'woocommerce_add_shipping_order_item', $order_id, $item_id, $package_key );
274
				}
275
			}
276
277
			// Store tax rows
278
			foreach ( array_keys( WC()->cart->taxes + WC()->cart->shipping_taxes ) as $tax_rate_id ) {
279
				if ( $tax_rate_id && ! $order->add_tax( $tax_rate_id, WC()->cart->get_tax_amount( $tax_rate_id ), WC()->cart->get_shipping_tax_amount( $tax_rate_id ) ) && apply_filters( 'woocommerce_cart_remove_taxes_zero_rate_id', 'zero-rated' ) !== $tax_rate_id ) {
280
					throw new Exception( sprintf( __( 'Error %d: Unable to create order. Please try again.', 'woocommerce' ), 528 ) );
281
				}
282
			}
283
284
			// Store coupons
285
			foreach ( WC()->cart->get_coupons() as $code => $coupon ) {
286
				if ( ! $order->add_coupon( $code, WC()->cart->get_coupon_discount_amount( $code ), WC()->cart->get_coupon_discount_tax_amount( $code ) ) ) {
287
					throw new Exception( sprintf( __( 'Error %d: Unable to create order. Please try again.', 'woocommerce' ), 529 ) );
288
				}
289
			}
290
291
			// Billing address
292
			$billing_address = array();
293 View Code Duplication
			if ( $this->checkout_fields['billing'] ) {
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...
294
				foreach ( array_keys( $this->checkout_fields['billing'] ) as $field ) {
295
					$field_name = str_replace( 'billing_', '', $field );
296
					$billing_address[ $field_name ] = $this->get_posted_address_data( $field_name );
297
				}
298
			}
299
300
			// Shipping address.
301
			$shipping_address = array();
302 View Code Duplication
			if ( $this->checkout_fields['shipping'] ) {
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...
303
				foreach ( array_keys( $this->checkout_fields['shipping'] ) as $field ) {
304
					$field_name = str_replace( 'shipping_', '', $field );
305
					$shipping_address[ $field_name ] = $this->get_posted_address_data( $field_name, 'shipping' );
306
				}
307
			}
308
309
			$order->set_address( $billing_address, 'billing' );
310
			$order->set_address( $shipping_address, 'shipping' );
311
			$order->set_payment_method( $this->payment_method );
0 ignored issues
show
Bug introduced by
It seems like $this->payment_method can also be of type string; however, WC_Abstract_Order::set_payment_method() does only seem to accept object<WC_Payment_Gateway>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
312
			$order->set_total( WC()->cart->shipping_total, 'shipping' );
313
			$order->set_total( WC()->cart->get_cart_discount_total(), 'cart_discount' );
314
			$order->set_total( WC()->cart->get_cart_discount_tax_total(), 'cart_discount_tax' );
315
			$order->set_total( WC()->cart->tax_total, 'tax' );
316
			$order->set_total( WC()->cart->shipping_tax_total, 'shipping_tax' );
317
			$order->set_total( WC()->cart->total );
318
319
			// Update user meta
320
			if ( $this->customer_id ) {
321
				if ( apply_filters( 'woocommerce_checkout_update_customer_data', true, $this ) ) {
322
					foreach ( $billing_address as $key => $value ) {
323
						update_user_meta( $this->customer_id, 'billing_' . $key, $value );
324
					}
325
					if ( WC()->cart->needs_shipping() ) {
326
						foreach ( $shipping_address as $key => $value ) {
327
							update_user_meta( $this->customer_id, 'shipping_' . $key, $value );
328
						}
329
					}
330
				}
331
				do_action( 'woocommerce_checkout_update_user_meta', $this->customer_id, $this->posted );
332
			}
333
334
			// Let plugins add meta
335
			do_action( 'woocommerce_checkout_update_order_meta', $order_id, $this->posted );
336
337
			// If we got here, the order was created without problems!
338
			wc_transaction_query( 'commit' );
339
340
		} catch ( Exception $e ) {
341
			// There was an error adding order data!
342
			wc_transaction_query( 'rollback' );
343
			return new WP_Error( 'checkout-error', $e->getMessage() );
344
		}
345
346
		return $order_id;
347
	}
348
349
	/**
350
	 * Process the checkout after the confirm order button is pressed.
351
	 */
352
	public function process_checkout() {
353
		try {
354
			if ( empty( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'woocommerce-process_checkout' ) ) {
355
				WC()->session->set( 'refresh_totals', true );
356
				throw new Exception( __( 'We were unable to process your order, please try again.', 'woocommerce' ) );
357
			}
358
359
			if ( ! defined( 'WOOCOMMERCE_CHECKOUT' ) ) {
360
				define( 'WOOCOMMERCE_CHECKOUT', true );
361
			}
362
363
			// Prevent timeout
364
			@set_time_limit(0);
365
366
			do_action( 'woocommerce_before_checkout_process' );
367
368
			if ( WC()->cart->is_empty() ) {
369
				throw new Exception( sprintf( __( 'Sorry, your session has expired. <a href="%s" class="wc-backward">Return to shop</a>', 'woocommerce' ), esc_url( wc_get_page_permalink( 'shop' ) ) ) );
370
			}
371
372
			do_action( 'woocommerce_checkout_process' );
373
374
			// Checkout fields (not defined in checkout_fields)
375
			$this->posted['terms']                     = isset( $_POST['terms'] ) ? 1 : 0;
376
			$this->posted['createaccount']             = isset( $_POST['createaccount'] ) && ! empty( $_POST['createaccount'] ) ? 1 : 0;
377
			$this->posted['payment_method']            = isset( $_POST['payment_method'] ) ? stripslashes( $_POST['payment_method'] ) : '';
378
			$this->posted['shipping_method']           = isset( $_POST['shipping_method'] ) ? $_POST['shipping_method'] : '';
379
			$this->posted['ship_to_different_address'] = isset( $_POST['ship_to_different_address'] ) ? true : false;
380
381
			if ( isset( $_POST['shiptobilling'] ) ) {
382
				_deprecated_argument( 'WC_Checkout::process_checkout()', '2.1', 'The "shiptobilling" field is deprecated. The template files are out of date' );
383
384
				$this->posted['ship_to_different_address'] = $_POST['shiptobilling'] ? false : true;
385
			}
386
387
			// Ship to billing only option
388
			if ( wc_ship_to_billing_address_only() ) {
389
				$this->posted['ship_to_different_address']  = false;
390
			}
391
392
			// Update customer shipping and payment method to posted method
393
			$chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
394
395
			if ( isset( $this->posted['shipping_method'] ) && is_array( $this->posted['shipping_method'] ) ) {
396
				foreach ( $this->posted['shipping_method'] as $i => $value ) {
397
					$chosen_shipping_methods[ $i ] = wc_clean( $value );
398
				}
399
			}
400
401
			WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );
402
			WC()->session->set( 'chosen_payment_method', $this->posted['payment_method'] );
403
404
			// Note if we skip shipping
405
			$skipped_shipping = false;
406
407
			// Get posted checkout_fields and do validation
408
			foreach ( $this->checkout_fields as $fieldset_key => $fieldset ) {
409
410
				// Skip shipping if not needed
411
				if ( $fieldset_key == 'shipping' && ( $this->posted['ship_to_different_address'] == false || ! WC()->cart->needs_shipping_address() ) ) {
412
					$skipped_shipping = true;
413
					continue;
414
				}
415
416
				// Skip account if not needed
417
				if ( $fieldset_key == 'account' && ( is_user_logged_in() || ( $this->must_create_account == false && empty( $this->posted['createaccount'] ) ) ) ) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
418
					continue;
419
				}
420
421
				foreach ( $fieldset as $key => $field ) {
422
423
					if ( ! isset( $field['type'] ) ) {
424
						$field['type'] = 'text';
425
					}
426
427
					// Get Value
428
					switch ( $field['type'] ) {
429
						case "checkbox" :
430
							$this->posted[ $key ] = isset( $_POST[ $key ] ) ? 1 : 0;
431
						break;
432
						case "multiselect" :
433
							$this->posted[ $key ] = isset( $_POST[ $key ] ) ? implode( ', ', array_map( 'wc_clean', $_POST[ $key ] ) ) : '';
434
						break;
435
						case "textarea" :
436
							$this->posted[ $key ] = isset( $_POST[ $key ] ) ? wp_strip_all_tags( wp_check_invalid_utf8( stripslashes( $_POST[ $key ] ) ) ) : '';
437
						break;
438
						default :
439
							$this->posted[ $key ] = isset( $_POST[ $key ] ) ? ( is_array( $_POST[ $key ] ) ? array_map( 'wc_clean', $_POST[ $key ] ) : wc_clean( $_POST[ $key ] ) ) : '';
440
						break;
441
					}
442
443
					// Hooks to allow modification of value
444
					$this->posted[ $key ] = apply_filters( 'woocommerce_process_checkout_' . sanitize_title( $field['type'] ) . '_field', $this->posted[ $key ] );
445
					$this->posted[ $key ] = apply_filters( 'woocommerce_process_checkout_field_' . $key, $this->posted[ $key ] );
446
447
					// Validation: Required fields
448
					if ( isset( $field['required'] ) && $field['required'] && ( ! isset( $this->posted[ $key ] ) || "" === $this->posted[ $key ] ) ) {
449
						switch ( $fieldset_key ) {
450
							case 'shipping' :
451
								$field_label = sprintf( _x( 'Shipping %s', 'Shipping FIELDNAME', 'woocommerce' ), $field['label'] );
452
							break;
453
							case 'billing' :
454
								$field_label = sprintf( _x( 'Billing %s', 'Billing FIELDNAME', 'woocommerce' ), $field['label'] );
455
							break;
456
							default :
457
								$field_label = $field['label'];
458
							break;
459
						}
460
						wc_add_notice( apply_filters( 'woocommerce_checkout_required_field_notice', sprintf( _x( '%s is a required field.', 'FIELDNAME is a required field.', 'woocommerce' ), '<strong>' . $field_label . '</strong>' ), $field_label ), 'error' );
461
					}
462
463
					if ( ! empty( $this->posted[ $key ] ) ) {
464
465
						// Validation rules
466
						if ( ! empty( $field['validate'] ) && is_array( $field['validate'] ) ) {
467
							foreach ( $field['validate'] as $rule ) {
468
								switch ( $rule ) {
469
									case 'postcode' :
470
										$this->posted[ $key ] = strtoupper( str_replace( ' ', '', $this->posted[ $key ] ) );
471
472
										if ( ! WC_Validation::is_postcode( $this->posted[ $key ], $_POST[ $fieldset_key . '_country' ] ) ) :
473
											wc_add_notice( __( 'Please enter a valid postcode/ZIP.', 'woocommerce' ), 'error' );
474
										else :
475
											$this->posted[ $key ] = wc_format_postcode( $this->posted[ $key ], $_POST[ $fieldset_key . '_country' ] );
476
										endif;
477
									break;
478 View Code Duplication
									case 'phone' :
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...
479
										$this->posted[ $key ] = wc_format_phone_number( $this->posted[ $key ] );
480
481
										if ( ! WC_Validation::is_phone( $this->posted[ $key ] ) )
482
											wc_add_notice( '<strong>' . $field['label'] . '</strong> ' . __( 'is not a valid phone number.', 'woocommerce' ), 'error' );
483
									break;
484 View Code Duplication
									case 'email' :
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...
485
										$this->posted[ $key ] = strtolower( $this->posted[ $key ] );
486
487
										if ( ! is_email( $this->posted[ $key ] ) )
488
											wc_add_notice( '<strong>' . $field['label'] . '</strong> ' . __( 'is not a valid email address.', 'woocommerce' ), 'error' );
489
									break;
490
									case 'state' :
491
										// Get valid states
492
										$valid_states = WC()->countries->get_states( isset( $_POST[ $fieldset_key . '_country' ] ) ? $_POST[ $fieldset_key . '_country' ] : ( 'billing' === $fieldset_key ? WC()->customer->get_country() : WC()->customer->get_shipping_country() ) );
493
494
										if ( ! empty( $valid_states ) && is_array( $valid_states ) ) {
495
											$valid_state_values = array_flip( array_map( 'strtolower', $valid_states ) );
496
497
											// Convert value to key if set
498
											if ( isset( $valid_state_values[ strtolower( $this->posted[ $key ] ) ] ) ) {
499
												 $this->posted[ $key ] = $valid_state_values[ strtolower( $this->posted[ $key ] ) ];
500
											}
501
										}
502
503
										// Only validate if the country has specific state options
504
										if ( ! empty( $valid_states ) && is_array( $valid_states ) && sizeof( $valid_states ) > 0 ) {
505
											if ( ! in_array( $this->posted[ $key ], array_keys( $valid_states ) ) ) {
506
												wc_add_notice( '<strong>' . $field['label'] . '</strong> ' . __( 'is not valid. Please enter one of the following:', 'woocommerce' ) . ' ' . implode( ', ', $valid_states ), 'error' );
507
											}
508
										}
509
									break;
510
								}
511
							}
512
						}
513
					}
514
				}
515
			}
516
517
			// Update customer location to posted location so we can correctly check available shipping methods
518
			if ( isset( $this->posted['billing_country'] ) ) {
519
				WC()->customer->set_country( $this->posted['billing_country'] );
520
			}
521
			if ( isset( $this->posted['billing_state'] ) ) {
522
				WC()->customer->set_state( $this->posted['billing_state'] );
523
			}
524
			if ( isset( $this->posted['billing_postcode'] ) ) {
525
				WC()->customer->set_postcode( $this->posted['billing_postcode'] );
526
			}
527
528
			// Shipping Information
529
			if ( ! $skipped_shipping ) {
530
531
				// Update customer location to posted location so we can correctly check available shipping methods
532
				if ( isset( $this->posted['shipping_country'] ) ) {
533
					WC()->customer->set_shipping_country( $this->posted['shipping_country'] );
534
				}
535
				if ( isset( $this->posted['shipping_state'] ) ) {
536
					WC()->customer->set_shipping_state( $this->posted['shipping_state'] );
537
				}
538
				if ( isset( $this->posted['shipping_postcode'] ) ) {
539
					WC()->customer->set_shipping_postcode( $this->posted['shipping_postcode'] );
540
				}
541
542
			} else {
543
544
				// Update customer location to posted location so we can correctly check available shipping methods
545
				if ( isset( $this->posted['billing_country'] ) ) {
546
					WC()->customer->set_shipping_country( $this->posted['billing_country'] );
547
				}
548
				if ( isset( $this->posted['billing_state'] ) ) {
549
					WC()->customer->set_shipping_state( $this->posted['billing_state'] );
550
				}
551
				if ( isset( $this->posted['billing_postcode'] ) ) {
552
					WC()->customer->set_shipping_postcode( $this->posted['billing_postcode'] );
553
				}
554
555
			}
556
557
			// Update cart totals now we have customer address
558
			WC()->cart->calculate_totals();
559
560
			// Terms
561
			if ( ! isset( $_POST['woocommerce_checkout_update_totals'] ) && empty( $this->posted['terms'] ) && wc_get_page_id( 'terms' ) > 0 && apply_filters( 'woocommerce_checkout_show_terms', true ) ) {
562
				wc_add_notice( __( 'You must accept our Terms &amp; Conditions.', 'woocommerce' ), 'error' );
563
			}
564
565
			if ( WC()->cart->needs_shipping() ) {
566
				$shipping_country = WC()->customer->get_shipping_country();
567
568
				if ( empty( $shipping_country ) ) {
569
					wc_add_notice( __( 'Please enter an address to continue.', 'woocommerce' ), 'error' );
570
				} elseif ( ! in_array( WC()->customer->get_shipping_country(), array_keys( WC()->countries->get_shipping_countries() ) ) ) {
571
					wc_add_notice( sprintf( __( 'Unfortunately <strong>we do not ship %s</strong>. Please enter an alternative shipping address.', 'woocommerce' ), WC()->countries->shipping_to_prefix() . ' ' . WC()->customer->get_shipping_country() ), 'error' );
572
				}
573
574
				// Validate Shipping Methods
575
				$packages               = WC()->shipping->get_packages();
576
				$this->shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
0 ignored issues
show
Documentation Bug introduced by
It seems like WC()->session->get('chosen_shipping_methods') can also be of type string. However, the property $shipping_methods is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
577
578
				foreach ( $packages as $i => $package ) {
579
					if ( ! isset( $package['rates'][ $this->shipping_methods[ $i ] ] ) ) {
580
						wc_add_notice( __( 'No shipping method has been selected. Please double check your address, or contact us if you need any help.', 'woocommerce' ), 'error' );
581
						$this->shipping_methods[ $i ] = '';
582
					}
583
				}
584
			}
585
586
			if ( WC()->cart->needs_payment() ) {
587
				// Payment Method
588
				$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
589
590
				if ( ! isset( $available_gateways[ $this->posted['payment_method'] ] ) ) {
591
					$this->payment_method = '';
592
					wc_add_notice( __( 'Invalid payment method.', 'woocommerce' ), 'error' );
593
				} else {
594
					$this->payment_method = $available_gateways[ $this->posted['payment_method'] ];
595
					$this->payment_method->validate_fields();
596
				}
597
			} else {
598
				$available_gateways = array();
599
			}
600
601
			// Action after validation
602
			do_action( 'woocommerce_after_checkout_validation', $this->posted );
603
604
			if ( ! isset( $_POST['woocommerce_checkout_update_totals'] ) && wc_notice_count( 'error' ) == 0 ) {
605
606
				// Customer accounts
607
				$this->customer_id = apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() );
608
609
				if ( ! is_user_logged_in() && ( $this->must_create_account || ! empty( $this->posted['createaccount'] ) ) ) {
610
611
					$username     = ! empty( $this->posted['account_username'] ) ? $this->posted['account_username'] : '';
612
					$password     = ! empty( $this->posted['account_password'] ) ? $this->posted['account_password'] : '';
613
					$new_customer = wc_create_new_customer( $this->posted['billing_email'], $username, $password );
614
615
					if ( is_wp_error( $new_customer ) ) {
616
						throw new Exception( $new_customer->get_error_message() );
617
					}
618
619
					$this->customer_id = $new_customer;
0 ignored issues
show
Documentation Bug introduced by
It seems like $new_customer can also be of type object<WP_Error>. However, the property $customer_id is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
620
621
					wc_set_customer_auth_cookie( $this->customer_id );
622
623
					// As we are now logged in, checkout will need to refresh to show logged in data
624
					WC()->session->set( 'reload_checkout', true );
625
626
					// Also, recalculate cart totals to reveal any role-based discounts that were unavailable before registering
627
					WC()->cart->calculate_totals();
628
629
					// Add customer info from other billing fields
630
					if ( $this->posted['billing_first_name'] && apply_filters( 'woocommerce_checkout_update_customer_data', true, $this ) ) {
631
						$userdata = array(
632
							'ID'           => $this->customer_id,
633
							'first_name'   => $this->posted['billing_first_name'] ? $this->posted['billing_first_name'] : '',
634
							'last_name'    => $this->posted['billing_last_name'] ? $this->posted['billing_last_name'] : '',
635
							'display_name' => $this->posted['billing_first_name'] ? $this->posted['billing_first_name'] : ''
636
						);
637
						wp_update_user( apply_filters( 'woocommerce_checkout_customer_userdata', $userdata, $this ) );
638
					}
639
				}
640
641
				// Do a final stock check at this point
642
				$this->check_cart_items();
643
644
				// Abort if errors are present
645
				if ( wc_notice_count( 'error' ) > 0 )
646
					throw new Exception();
647
648
				$order_id = $this->create_order();
649
650
				if ( is_wp_error( $order_id ) ) {
651
					throw new Exception( $order_id->get_error_message() );
652
				}
653
654
				do_action( 'woocommerce_checkout_order_processed', $order_id, $this->posted );
655
656
				// Process payment
657
				if ( WC()->cart->needs_payment() ) {
658
659
					// Store Order ID in session so it can be re-used after payment failure
660
					WC()->session->order_awaiting_payment = $order_id;
661
662
					// Process Payment
663
					$result = $available_gateways[ $this->posted['payment_method'] ]->process_payment( $order_id );
664
665
					// Redirect to success/confirmation/payment page
666
					if ( isset( $result['result'] ) && 'success' === $result['result'] ) {
667
668
						$result = apply_filters( 'woocommerce_payment_successful_result', $result, $order_id );
669
670
						if ( is_ajax() ) {
671
							wp_send_json( $result );
672
						} else {
673
							wp_redirect( $result['redirect'] );
674
							exit;
675
						}
676
677
					}
678
679
				} else {
680
681
					if ( empty( $order ) ) {
682
						$order = wc_get_order( $order_id );
683
					}
684
685
					// No payment was required for order
686
					$order->payment_complete();
687
688
					// Empty the Cart
689
					WC()->cart->empty_cart();
690
691
					// Get redirect
692
					$return_url = $order->get_checkout_order_received_url();
693
694
					// Redirect to success/confirmation/payment page
695
					if ( is_ajax() ) {
696
						wp_send_json( array(
697
							'result' 	=> 'success',
698
							'redirect'  => apply_filters( 'woocommerce_checkout_no_payment_needed_redirect', $return_url, $order )
699
						) );
700
					} else {
701
						wp_safe_redirect(
702
							apply_filters( 'woocommerce_checkout_no_payment_needed_redirect', $return_url, $order )
703
						);
704
						exit;
705
					}
706
707
				}
708
709
			}
710
711
		} catch ( Exception $e ) {
712
			if ( ! empty( $e ) ) {
713
				wc_add_notice( $e->getMessage(), 'error' );
714
			}
715
		}
716
717
		// If we reached this point then there were errors
718
		if ( is_ajax() ) {
719
720
			// only print notices if not reloading the checkout, otherwise they're lost in the page reload
721
			if ( ! isset( WC()->session->reload_checkout ) ) {
722
				ob_start();
723
				wc_print_notices();
724
				$messages = ob_get_clean();
725
			}
726
727
			$response = array(
728
				'result'	=> 'failure',
729
				'messages' 	=> isset( $messages ) ? $messages : '',
730
				'refresh' 	=> isset( WC()->session->refresh_totals ) ? 'true' : 'false',
731
				'reload'    => isset( WC()->session->reload_checkout ) ? 'true' : 'false'
732
			);
733
734
			unset( WC()->session->refresh_totals, WC()->session->reload_checkout );
735
736
			wp_send_json( $response );
737
		}
738
	}
739
740
	/**
741
	 * Get a posted address field after sanitization and validation.
742
	 * @param string $key
743
	 * @param string $type billing for shipping
744
	 * @return string
745
	 */
746
	public function get_posted_address_data( $key, $type = 'billing' ) {
747
		if ( 'billing' === $type || false === $this->posted['ship_to_different_address'] ) {
748
			$return = isset( $this->posted[ 'billing_' . $key ] ) ? $this->posted[ 'billing_' . $key ] : '';
749
		} else {
750
			$return = isset( $this->posted[ 'shipping_' . $key ] ) ? $this->posted[ 'shipping_' . $key ] : '';
751
		}
752
753
		// Use logged in user's billing email if neccessary
754
		if ( 'email' === $key && empty( $return ) && is_user_logged_in() ) {
755
			$current_user = wp_get_current_user();
756
			$return       = $current_user->user_email;
757
		}
758
		return $return;
759
	}
760
761
	/**
762
	 * Gets the value either from the posted data, or from the users meta data.
763
	 *
764
	 * @access public
765
	 * @param string $input
766
	 * @return string|null
767
	 */
768
	public function get_value( $input ) {
769
		if ( ! empty( $_POST[ $input ] ) ) {
770
771
			return wc_clean( $_POST[ $input ] );
772
773
		} else {
774
775
			$value = apply_filters( 'woocommerce_checkout_get_value', null, $input );
776
777
			if ( $value !== null ) {
778
				return $value;
779
			}
780
781
			// Get the billing_ and shipping_ address fields
782
			if ( isset( $this->checkout_fields['shipping'] ) && isset( $this->checkout_fields['billing'] ) ) {
783
784
				$address_fields = array_merge( $this->checkout_fields['billing'], $this->checkout_fields['shipping'] );
785
786
				if ( is_user_logged_in() && is_array( $address_fields ) && array_key_exists( $input, $address_fields ) ) {
787
					$current_user = wp_get_current_user();
788
789
					if ( $meta = get_user_meta( $current_user->ID, $input, true ) ) {
790
						return $meta;
791
					}
792
793
					if ( $input == 'billing_email' ) {
794
						return $current_user->user_email;
795
					}
796
				}
797
798
			}
799
800
			switch ( $input ) {
801
				case 'billing_country' :
802
					return apply_filters( 'default_checkout_country', WC()->customer->get_country() ? WC()->customer->get_country() : '', 'billing' );
803
				case 'billing_state' :
804
					return apply_filters( 'default_checkout_state', WC()->customer->get_state() ? WC()->customer->get_state() : '', 'billing' );
805
				case 'billing_postcode' :
806
					return apply_filters( 'default_checkout_postcode', WC()->customer->get_postcode() ? WC()->customer->get_postcode() : '', 'billing' );
807
				case 'shipping_country' :
808
					return apply_filters( 'default_checkout_country', WC()->customer->get_shipping_country() ? WC()->customer->get_shipping_country() : '', 'shipping' );
809
				case 'shipping_state' :
810
					return apply_filters( 'default_checkout_state', WC()->customer->get_shipping_state() ? WC()->customer->get_shipping_state() : '', 'shipping' );
811
				case 'shipping_postcode' :
812
					return apply_filters( 'default_checkout_postcode', WC()->customer->get_shipping_postcode() ? WC()->customer->get_shipping_postcode() : '', 'shipping' );
813
				default :
814
					return apply_filters( 'default_checkout_' . $input, null, $input );
815
			}
816
		}
817
	}
818
}
819