Completed
Push — master ( f57f30...e4e4a8 )
by Mike
08:01
created

WC_Checkout   D

Complexity

Total Complexity 183

Size/Duplication

Total Lines 787
Duplicated Lines 4.07 %

Coupling/Cohesion

Components 2
Dependencies 7
Metric Value
wmc 183
lcom 2
cbo 7
dl 32
loc 787
rs 4.4444

11 Methods

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

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
1 ignored issue
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 18 and the first side effect is on line 4.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
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
		if ( get_option( 'woocommerce_registration_generate_username' ) == 'no' ) {
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
		if ( get_option( 'woocommerce_registration_generate_password' ) == 'no' ) {
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()->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);
1 ignored issue
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
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 homepage</a>', 'woocommerce' ), home_url() ) );
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'] && empty( $this->posted[ $key ] ) ) {
449
						wc_add_notice( '<strong>' . $field['label'] . '</strong> ' . __( 'is a required field.', 'woocommerce' ), 'error' );
450
					}
451
452
					if ( ! empty( $this->posted[ $key ] ) ) {
453
454
						// Validation rules
455
						if ( ! empty( $field['validate'] ) && is_array( $field['validate'] ) ) {
456
							foreach ( $field['validate'] as $rule ) {
457
								switch ( $rule ) {
458
									case 'postcode' :
459
										$this->posted[ $key ] = strtoupper( str_replace( ' ', '', $this->posted[ $key ] ) );
460
461
										if ( ! WC_Validation::is_postcode( $this->posted[ $key ], $_POST[ $fieldset_key . '_country' ] ) ) :
462
											wc_add_notice( __( 'Please enter a valid postcode/ZIP.', 'woocommerce' ), 'error' );
463
										else :
464
											$this->posted[ $key ] = wc_format_postcode( $this->posted[ $key ], $_POST[ $fieldset_key . '_country' ] );
465
										endif;
466
									break;
467 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...
468
										$this->posted[ $key ] = wc_format_phone_number( $this->posted[ $key ] );
469
470
										if ( ! WC_Validation::is_phone( $this->posted[ $key ] ) )
471
											wc_add_notice( '<strong>' . $field['label'] . '</strong> ' . __( 'is not a valid phone number.', 'woocommerce' ), 'error' );
472
									break;
473 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...
474
										$this->posted[ $key ] = strtolower( $this->posted[ $key ] );
475
476
										if ( ! is_email( $this->posted[ $key ] ) )
477
											wc_add_notice( '<strong>' . $field['label'] . '</strong> ' . __( 'is not a valid email address.', 'woocommerce' ), 'error' );
478
									break;
479
									case 'state' :
480
										// Get valid states
481
										$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() ) );
482
483
										if ( ! empty( $valid_states ) && is_array( $valid_states ) ) {
484
											$valid_state_values = array_flip( array_map( 'strtolower', $valid_states ) );
485
486
											// Convert value to key if set
487
											if ( isset( $valid_state_values[ strtolower( $this->posted[ $key ] ) ] ) ) {
488
												 $this->posted[ $key ] = $valid_state_values[ strtolower( $this->posted[ $key ] ) ];
489
											}
490
										}
491
492
										// Only validate if the country has specific state options
493
										if ( ! empty( $valid_states ) && is_array( $valid_states ) && sizeof( $valid_states ) > 0 ) {
494
											if ( ! in_array( $this->posted[ $key ], array_keys( $valid_states ) ) ) {
495
												wc_add_notice( '<strong>' . $field['label'] . '</strong> ' . __( 'is not valid. Please enter one of the following:', 'woocommerce' ) . ' ' . implode( ', ', $valid_states ), 'error' );
496
											}
497
										}
498
									break;
499
								}
500
							}
501
						}
502
					}
503
				}
504
			}
505
506
			// Update customer location to posted location so we can correctly check available shipping methods
507
			if ( isset( $this->posted['billing_country'] ) ) {
508
				WC()->customer->set_country( $this->posted['billing_country'] );
509
			}
510
			if ( isset( $this->posted['billing_state'] ) ) {
511
				WC()->customer->set_state( $this->posted['billing_state'] );
512
			}
513
			if ( isset( $this->posted['billing_postcode'] ) ) {
514
				WC()->customer->set_postcode( $this->posted['billing_postcode'] );
515
			}
516
517
			// Shipping Information
518
			if ( ! $skipped_shipping ) {
519
520
				// Update customer location to posted location so we can correctly check available shipping methods
521
				if ( isset( $this->posted['shipping_country'] ) ) {
522
					WC()->customer->set_shipping_country( $this->posted['shipping_country'] );
523
				}
524
				if ( isset( $this->posted['shipping_state'] ) ) {
525
					WC()->customer->set_shipping_state( $this->posted['shipping_state'] );
526
				}
527
				if ( isset( $this->posted['shipping_postcode'] ) ) {
528
					WC()->customer->set_shipping_postcode( $this->posted['shipping_postcode'] );
529
				}
530
531
			} else {
532
533
				// Update customer location to posted location so we can correctly check available shipping methods
534
				if ( isset( $this->posted['billing_country'] ) ) {
535
					WC()->customer->set_shipping_country( $this->posted['billing_country'] );
536
				}
537
				if ( isset( $this->posted['billing_state'] ) ) {
538
					WC()->customer->set_shipping_state( $this->posted['billing_state'] );
539
				}
540
				if ( isset( $this->posted['billing_postcode'] ) ) {
541
					WC()->customer->set_shipping_postcode( $this->posted['billing_postcode'] );
542
				}
543
544
			}
545
546
			// Update cart totals now we have customer address
547
			WC()->cart->calculate_totals();
548
549
			// Terms
550
			if ( ! isset( $_POST['woocommerce_checkout_update_totals'] ) && empty( $this->posted['terms'] ) && wc_get_page_id( 'terms' ) > 0 && apply_filters( 'woocommerce_checkout_show_terms', true ) ) {
551
				wc_add_notice( __( 'You must accept our Terms &amp; Conditions.', 'woocommerce' ), 'error' );
552
			}
553
554
			if ( WC()->cart->needs_shipping() ) {
555
556
				if ( ! in_array( WC()->customer->get_shipping_country(), array_keys( WC()->countries->get_shipping_countries() ) ) ) {
557
					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' );
558
				}
559
560
				// Validate Shipping Methods
561
				$packages               = WC()->shipping->get_packages();
562
				$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') of type * is incompatible with the declared type array of property $shipping_methods.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
563
564
				foreach ( $packages as $i => $package ) {
565
					if ( ! isset( $package['rates'][ $this->shipping_methods[ $i ] ] ) ) {
566
						wc_add_notice( __( 'Invalid shipping method.', 'woocommerce' ), 'error' );
567
						$this->shipping_methods[ $i ] = '';
568
					}
569
				}
570
			}
571
572
			if ( WC()->cart->needs_payment() ) {
573
				// Payment Method
574
				$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
575
576
				if ( ! isset( $available_gateways[ $this->posted['payment_method'] ] ) ) {
577
					$this->payment_method = '';
578
					wc_add_notice( __( 'Invalid payment method.', 'woocommerce' ), 'error' );
579
				} else {
580
					$this->payment_method = $available_gateways[ $this->posted['payment_method'] ];
581
					$this->payment_method->validate_fields();
582
				}
583
			} else {
584
				$available_gateways = array();
585
			}
586
587
			// Action after validation
588
			do_action( 'woocommerce_after_checkout_validation', $this->posted );
589
590
			if ( ! isset( $_POST['woocommerce_checkout_update_totals'] ) && wc_notice_count( 'error' ) == 0 ) {
591
592
				// Customer accounts
593
				$this->customer_id = apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() );
594
595
				if ( ! is_user_logged_in() && ( $this->must_create_account || ! empty( $this->posted['createaccount'] ) ) ) {
596
597
					$username     = ! empty( $this->posted['account_username'] ) ? $this->posted['account_username'] : '';
598
					$password     = ! empty( $this->posted['account_password'] ) ? $this->posted['account_password'] : '';
599
					$new_customer = wc_create_new_customer( $this->posted['billing_email'], $username, $password );
600
601
					if ( is_wp_error( $new_customer ) ) {
602
						throw new Exception( $new_customer->get_error_message() );
603
					}
604
605
					$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...
606
607
					wc_set_customer_auth_cookie( $this->customer_id );
608
609
					// As we are now logged in, checkout will need to refresh to show logged in data
610
					WC()->session->set( 'reload_checkout', true );
611
612
					// Also, recalculate cart totals to reveal any role-based discounts that were unavailable before registering
613
					WC()->cart->calculate_totals();
614
615
					// Add customer info from other billing fields
616
					if ( $this->posted['billing_first_name'] && apply_filters( 'woocommerce_checkout_update_customer_data', true, $this ) ) {
617
						$userdata = array(
618
							'ID'           => $this->customer_id,
619
							'first_name'   => $this->posted['billing_first_name'] ? $this->posted['billing_first_name'] : '',
620
							'last_name'    => $this->posted['billing_last_name'] ? $this->posted['billing_last_name'] : '',
621
							'display_name' => $this->posted['billing_first_name'] ? $this->posted['billing_first_name'] : ''
622
						);
623
						wp_update_user( apply_filters( 'woocommerce_checkout_customer_userdata', $userdata, $this ) );
624
					}
625
				}
626
627
				// Do a final stock check at this point
628
				$this->check_cart_items();
629
630
				// Abort if errors are present
631
				if ( wc_notice_count( 'error' ) > 0 )
632
					throw new Exception();
633
634
				$order_id = $this->create_order();
635
636
				if ( is_wp_error( $order_id ) ) {
637
					throw new Exception( $order_id->get_error_message() );
638
				}
639
640
				do_action( 'woocommerce_checkout_order_processed', $order_id, $this->posted );
641
642
				// Process payment
643
				if ( WC()->cart->needs_payment() ) {
644
645
					// Store Order ID in session so it can be re-used after payment failure
646
					WC()->session->order_awaiting_payment = $order_id;
647
648
					// Process Payment
649
					$result = $available_gateways[ $this->posted['payment_method'] ]->process_payment( $order_id );
650
651
					// Redirect to success/confirmation/payment page
652
					if ( isset( $result['result'] ) && 'success' === $result['result'] ) {
653
654
						$result = apply_filters( 'woocommerce_payment_successful_result', $result, $order_id );
655
656
						if ( is_ajax() ) {
657
							wp_send_json( $result );
658
						} else {
659
							wp_redirect( $result['redirect'] );
660
							exit;
661
						}
662
663
					}
664
665
				} else {
666
667
					if ( empty( $order ) ) {
668
						$order = wc_get_order( $order_id );
669
					}
670
671
					// No payment was required for order
672
					$order->payment_complete();
673
674
					// Empty the Cart
675
					WC()->cart->empty_cart();
676
677
					// Get redirect
678
					$return_url = $order->get_checkout_order_received_url();
679
680
					// Redirect to success/confirmation/payment page
681
					if ( is_ajax() ) {
682
						wp_send_json( array(
683
							'result' 	=> 'success',
684
							'redirect'  => apply_filters( 'woocommerce_checkout_no_payment_needed_redirect', $return_url, $order )
685
						) );
686
					} else {
687
						wp_safe_redirect(
688
							apply_filters( 'woocommerce_checkout_no_payment_needed_redirect', $return_url, $order )
689
						);
690
						exit;
691
					}
692
693
				}
694
695
			}
696
697
		} catch ( Exception $e ) {
698
			if ( ! empty( $e ) ) {
699
				wc_add_notice( $e->getMessage(), 'error' );
700
			}
701
		}
702
703
		// If we reached this point then there were errors
704
		if ( is_ajax() ) {
705
706
			// only print notices if not reloading the checkout, otherwise they're lost in the page reload
707
			if ( ! isset( WC()->session->reload_checkout ) ) {
708
				ob_start();
709
				wc_print_notices();
710
				$messages = ob_get_clean();
711
			}
712
713
			$response = array(
714
				'result'	=> 'failure',
715
				'messages' 	=> isset( $messages ) ? $messages : '',
716
				'refresh' 	=> isset( WC()->session->refresh_totals ) ? 'true' : 'false',
717
				'reload'    => isset( WC()->session->reload_checkout ) ? 'true' : 'false'
718
			);
719
720
			unset( WC()->session->refresh_totals, WC()->session->reload_checkout );
721
722
			wp_send_json( $response );
723
		}
724
	}
725
726
	/**
727
	 * Get a posted address field after sanitization and validation.
728
	 * @param string $key
729
	 * @param string $type billing for shipping
730
	 * @return string
731
	 */
732
	public function get_posted_address_data( $key, $type = 'billing' ) {
733
		if ( 'billing' === $type || false === $this->posted['ship_to_different_address'] ) {
734
			$return = isset( $this->posted[ 'billing_' . $key ] ) ? $this->posted[ 'billing_' . $key ] : '';
735
		} else {
736
			$return = isset( $this->posted[ 'shipping_' . $key ] ) ? $this->posted[ 'shipping_' . $key ] : '';
737
		}
738
739
		// Use logged in user's billing email if neccessary
740
		if ( 'email' === $key && empty( $return ) && is_user_logged_in() ) {
741
			$current_user = wp_get_current_user();
742
			$return       = $current_user->user_email;
743
		}
744
		return $return;
745
	}
746
747
	/**
748
	 * Gets the value either from the posted data, or from the users meta data.
749
	 *
750
	 * @access public
751
	 * @param string $input
752
	 * @return string|null
753
	 */
754
	public function get_value( $input ) {
755
		if ( ! empty( $_POST[ $input ] ) ) {
756
757
			return wc_clean( $_POST[ $input ] );
758
759
		} else {
760
761
			$value = apply_filters( 'woocommerce_checkout_get_value', null, $input );
762
763
			if ( $value !== null ) {
764
				return $value;
765
			}
766
767
			// Get the billing_ and shipping_ address fields
768
			if ( isset( $this->checkout_fields['shipping'] ) && isset( $this->checkout_fields['billing'] ) ) {
769
770
				$address_fields = array_merge( $this->checkout_fields['billing'], $this->checkout_fields['shipping'] );
771
772
				if ( is_user_logged_in() && is_array( $address_fields ) && array_key_exists( $input, $address_fields ) ) {
773
					$current_user = wp_get_current_user();
774
775
					if ( $meta = get_user_meta( $current_user->ID, $input, true ) ) {
776
						return $meta;
777
					}
778
779
					if ( $input == 'billing_email' ) {
780
						return $current_user->user_email;
781
					}
782
				}
783
784
			}
785
786
			switch ( $input ) {
787 View Code Duplication
				case 'billing_country' :
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...
788
					return apply_filters( 'default_checkout_country', WC()->customer->get_country() ? WC()->customer->get_country() : WC()->countries->get_base_country(), 'billing' );
789 View Code Duplication
				case 'billing_state' :
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...
790
					return apply_filters( 'default_checkout_state', WC()->customer->has_calculated_shipping() ? WC()->customer->get_state() : '', 'billing' );
791
				case 'billing_postcode' :
792
					return apply_filters( 'default_checkout_postcode', WC()->customer->get_postcode() ? WC()->customer->get_postcode() : '', 'billing' );
793 View Code Duplication
				case 'shipping_country' :
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...
794
					return apply_filters( 'default_checkout_country', WC()->customer->get_shipping_country() ? WC()->customer->get_shipping_country() : WC()->countries->get_base_country(), 'shipping' );
795 View Code Duplication
				case 'shipping_state' :
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...
796
					return apply_filters( 'default_checkout_state', WC()->customer->has_calculated_shipping() ? WC()->customer->get_shipping_state() : '', 'shipping' );
797
				case 'shipping_postcode' :
798
					return apply_filters( 'default_checkout_postcode', WC()->customer->get_shipping_postcode() ? WC()->customer->get_shipping_postcode() : '', 'shipping' );
799
				default :
800
					return apply_filters( 'default_checkout_' . $input, null, $input );
801
			}
802
		}
803
	}
804
}
805