Completed
Pull Request — master (#11889)
by Mike
11:08
created

WC_Checkout   D

Complexity

Total Complexity 181

Size/Duplication

Total Lines 811
Duplicated Lines 5.18 %

Coupling/Cohesion

Components 2
Dependencies 10

Importance

Changes 0
Metric Value
dl 42
loc 811
rs 4.4444
c 0
b 0
f 0
wmc 181
lcom 2
cbo 10

12 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() 0 162 18
D update_customer_data() 14 26 10
F process_checkout() 12 383 109
B get_posted_address_data() 0 14 8
C get_value() 0 49 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
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
	 * @throws Exception
168
	 * @return int|WP_ERROR
169
	 */
170
	public function create_order() {
171
		global $wpdb;
172
173
		// Give plugins the opportunity to create an order themselves
174
		if ( $order_id = apply_filters( 'woocommerce_create_order', null, $this ) ) {
175
			return $order_id;
176
		}
177
178
		try {
179
			// Start transaction if available
180
			wc_transaction_query( 'start' );
181
182
			// Insert or update the post data
183
			$order_id  = absint( WC()->session->order_awaiting_payment );
184
			$cart_hash = md5( json_encode( wc_clean( WC()->cart->get_cart_for_session() ) ) . WC()->cart->get_total() );
185
186
			/**
187
			 * If there is an order pending payment, we can resume it here so
188
			 * long as it has not changed. If the order has changed, i.e.
189
			 * different items or cost, create a new order. We use a hash to
190
			 * detect changes which is based on cart items + order total.
191
			 */
192
			if ( $order_id && ( $order = wc_get_order( $order_id ) ) && $order->has_cart_hash( $cart_hash ) && $order->has_status( array( 'pending', 'failed' ) ) ) {
193
				// Action for 3rd parties.
194
				do_action( 'woocommerce_resume_order', $order_id );
195
196
				// Remove all items - we will re-add them later.
197
				$order->remove_order_items();
198
			/**
199
			 * Not resuming - lets create a new order object.
200
			 */
201
			} else {
202
				$order = new WC_Order();
203
			}
204
205
			$order->set_created_via( 'checkout' );
206
			$order->set_cart_hash( $cart_hash );
207
			$order->set_customer_id( $this->customer_id );
208
			$order->set_currency( get_woocommerce_currency() );
209
			$order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
210
			$order->set_customer_ip_address( WC_Geolocation::get_ip_address() );
211
			$order->set_customer_user_agent( wc_get_user_agent() );
212
			$order->set_customer_note( isset( $this->posted['order_comments'] ) ? $this->posted['order_comments'] : '' );
213
			$order->set_payment_method( $this->payment_method );
214
			$order->set_shipping_total( WC()->cart->get_shipping_total() );
215
			$order->set_discount_total( WC()->cart->get_cart_discount_total() );
216
			$order->set_discount_tax( WC()->cart->get_cart_discount_tax_total() );
217
			$order->set_cart_tax( WC()->cart->get_tax_total() );
218
			$order->set_shipping_tax( WC()->cart->get_shipping_tax_total() );
219
			$order->set_total( WC()->cart->get_total() );
220
221
			// Billing and shipping addresses
222
			if ( $address_keys = array_merge( array_keys( $this->checkout_fields['billing'] ), array_keys( $this->checkout_fields['shipping'] ) ) ) {
223
				foreach ( $address_keys as $key ) {
224
					if ( is_callable( array( $order, "set_{$key}" ) ) ) {
225
						$order->{"set_{$key}"}( $this->get_posted_address_data( str_replace( array( 'billing_', 'shipping_' ), '', $key ), strstr( $key, 'billing_' ) ? 'billing' : 'shipping' ) );
226
					}
227
				}
228
			}
229
230
			/**
231
			 * Store items and costs. For tax inclusive prices, we do some extra rounding logic so the stored
232
			 * values "add up" when viewing the order in admin. This does have the disadvatage of not being able to
233
			 * recalculate the tax total/subtotal accurately in the future, but it does ensure the data looks correct.
234
			 */
235
			$item_totals = WC()->cart->get_item_totals();
236
237
			foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
238
				$item                       = new WC_Order_Item_Product( $cart_item );
0 ignored issues
show
Documentation introduced by
$cart_item is of type object<WC_Item_Product>, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
239
				$item->legacy_values        = $cart_item;
240
				$item->legacy_cart_item_key = $cart_item_key;
241
				$item_total                 = $item_totals[ $cart_item_key ];
242
243
				if ( 'taxable' === $item->get_tax_status() || wc_prices_include_tax() ) {
244
					$item->set_subtotal( round( $item_total->subtotal + $item_total->subtotal_tax - wc_round_tax_total( $item_total->subtotal_tax ), wc_get_price_decimals() ) );
245
					$item->set_total( round( $item_total->total + $item_total->total_tax - wc_round_tax_total( $item_total->total_tax ), wc_get_price_decimals() ) );
246
					$item->set_taxes( array( 'total' => array_map( 'wc_round_tax_total', $item_total->taxes ), 'subtotal' => array_map( 'wc_round_tax_total', $item_total->subtotal_taxes ) ) );
247
				} else {
248
					$item->set_subtotal( $item_total->subtotal );
249
					$item->set_total( $item_total->total );
250
					$item->set_taxes( array( 'total' => $item_total->taxes, 'subtotal' => $item_total->subtotal_taxes ) );
251
				}
252
253
				$item->set_backorder_meta();
254
				$order->add_item( $item );
255
			}
256
257
			// Store tax rows
258
			foreach ( WC()->cart->get_taxes() as $tax_rate_id => $tax_item ) {
259
				if ( $tax_rate_id && apply_filters( 'woocommerce_cart_remove_taxes_zero_rate_id', 'zero-rated' ) !== $tax_rate_id ) {
260
					$order->add_item( new WC_Order_Item_Tax( $tax_item ) );
261
				}
262
			}
263
264
			/*// Add fees
0 ignored issues
show
Unused Code Comprehensibility introduced by
47% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
265
			foreach ( WC()->cart->get_fees() as $fee_key => $fee ) {
266
				$item = new WC_Order_Item_Fee( array(
267
					'name'      => $fee->name,
268
					'tax_class' => $fee->taxable ? $fee->tax_class : 0,
269
					'total'     => $fee->amount,
270
					'total_tax' => $fee->tax,
271
					'taxes'     => array(
272
						'total' => $fee->tax_data,
273
					),
274
				) );
275
276
				// Set this to pass to legacy actions @todo remove in future release
277
				$item->legacy_fee     = $fee;
278
				$item->legacy_fee_key = $fee_key;
279
280
				$order->add_item( $item );
281
			}
282
283
			// Store shipping for all packages
284
			foreach ( WC()->shipping->get_packages() as $package_key => $package ) {
285
				if ( isset( $package['rates'][ $this->shipping_methods[ $package_key ] ] ) ) {
286
					$shipping_rate = $package['rates'][ $this->shipping_methods[ $package_key ] ];
287
					$item = new WC_Order_Item_Shipping( array(
288
						'method_title' => $shipping_rate->label,
289
						'method_id'    => $shipping_rate->id,
290
						'total'        => wc_format_decimal( $shipping_rate->cost ),
291
						'taxes'        => $shipping_rate->taxes,
292
						'meta_data'    => $shipping_rate->get_meta_data(),
293
					) );
294
295
					// Set this to pass to legacy actions @todo remove in future release
296
					$item->legacy_package_key = $package_key;
297
298
					$order->add_item( $item );
299
				}
300
			}
301
302
			// Store coupons
303
			foreach ( WC()->cart->get_coupons() as $code => $coupon ) {
304
				$item = new WC_Order_Item_Coupon( array(
305
					'code'         => $code,
306
					'discount'     => WC()->cart->get_coupon_discount_amount( $code ),
307
					'discount_tax' => WC()->cart->get_coupon_discount_tax_amount( $code ),
308
				) );
309
				$order->add_item( $item );
310
			}*/
311
312
			// Save the order
313
			$order_id = $order->save();
314
315
			// Update user meta
316
			$this->update_customer_data();
317
318
			// Let plugins add their own meta data
319
			do_action( 'woocommerce_checkout_update_order_meta', $order_id, $this->posted );
320
321
			// If we got here, the order was created without problems!
322
			wc_transaction_query( 'commit' );
323
324
		} catch ( Exception $e ) {
325
			// There was an error adding order data!
326
			wc_transaction_query( 'rollback' );
327
			return new WP_Error( 'checkout-error', $e->getMessage() );
328
		}
329
330
		return $order_id;
331
	}
332
333
	/**
334
	 * Store customer data to meta.
335
	 * @since 2.7.0
336
	 */
337
	protected function update_customer_data() {
338
		if ( $this->customer_id ) {
339
			if ( apply_filters( 'woocommerce_checkout_update_customer_data', true, $this ) ) {
340
				$customer = new WC_Customer( $this->customer_id );
341
342 View Code Duplication
				if ( $keys = array_keys( $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...
343
					foreach ( $keys as $key ) {
344
						if ( is_callable( array( $customer, "set_{$key}" ) ) ) {
345
							$customer->{"set_{$key}"}( $this->get_posted_address_data( str_replace( array( 'billing_', 'shipping_' ), '', $key ) ) );
346
						}
347
					}
348
				}
349
350 View Code Duplication
				if ( WC()->cart->needs_shipping() && ( $keys = array_keys( $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...
351
					foreach ( $keys as $key ) {
352
						if ( is_callable( array( $customer, "set_{$key}" ) ) ) {
353
							$customer->{"set_{$key}"}( $this->get_posted_address_data( str_replace( array( 'billing_', 'shipping_' ), '', $key ), 'shipping' ) );
354
						}
355
					}
356
				}
357
358
				$customer->save();
359
			}
360
			do_action( 'woocommerce_checkout_update_user_meta', $this->customer_id, $this->posted );
361
		}
362
	}
363
364
	/**
365
	 * Process the checkout after the confirm order button is pressed.
366
	 */
367
	public function process_checkout() {
368
		try {
369
			if ( empty( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'woocommerce-process_checkout' ) ) {
370
				WC()->session->set( 'refresh_totals', true );
371
				throw new Exception( __( 'We were unable to process your order, please try again.', 'woocommerce' ) );
372
			}
373
374
			if ( ! defined( 'WOOCOMMERCE_CHECKOUT' ) ) {
375
				define( 'WOOCOMMERCE_CHECKOUT', true );
376
			}
377
378
			// Prevent timeout
379
			@set_time_limit( 0 );
380
381
			do_action( 'woocommerce_before_checkout_process' );
382
383
			if ( WC()->cart->is_empty() ) {
384
				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' ) ) ) );
385
			}
386
387
			do_action( 'woocommerce_checkout_process' );
388
389
			// Checkout fields (not defined in checkout_fields)
390
			$this->posted['terms']                     = isset( $_POST['terms'] ) ? 1 : 0;
391
			$this->posted['createaccount']             = isset( $_POST['createaccount'] ) && ! empty( $_POST['createaccount'] ) ? 1 : 0;
392
			$this->posted['payment_method']            = isset( $_POST['payment_method'] ) ? stripslashes( $_POST['payment_method'] ) : '';
393
			$this->posted['shipping_method']           = isset( $_POST['shipping_method'] ) ? $_POST['shipping_method'] : '';
394
			$this->posted['ship_to_different_address'] = ! empty( $_POST['ship_to_different_address'] );
395
396
			if ( isset( $_POST['shiptobilling'] ) ) {
397
				_deprecated_argument( 'WC_Checkout::process_checkout()', '2.1', 'The "shiptobilling" field is deprecated. The template files are out of date' );
398
399
				$this->posted['ship_to_different_address'] = $_POST['shiptobilling'] ? false : true;
400
			}
401
402
			// Ship to billing only option
403
			if ( wc_ship_to_billing_address_only() ) {
404
				$this->posted['ship_to_different_address']  = false;
405
			}
406
407
			// Update customer shipping and payment method to posted method
408
			$chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
409
410
			if ( isset( $this->posted['shipping_method'] ) && is_array( $this->posted['shipping_method'] ) ) {
411
				foreach ( $this->posted['shipping_method'] as $i => $value ) {
412
					$chosen_shipping_methods[ $i ] = wc_clean( $value );
413
				}
414
			}
415
416
			WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );
417
			WC()->session->set( 'chosen_payment_method', $this->posted['payment_method'] );
418
419
			// Note if we skip shipping
420
			$skipped_shipping = false;
421
422
			// Get posted checkout_fields and do validation
423
			foreach ( $this->checkout_fields as $fieldset_key => $fieldset ) {
424
425
				// Skip shipping if not needed
426
				if ( 'shipping' === $fieldset_key && ( false == $this->posted['ship_to_different_address'] || ! WC()->cart->needs_shipping_address() ) ) {
427
					$skipped_shipping = true;
428
					continue;
429
				}
430
431
				// Skip account if not needed
432
				if ( 'account' === $fieldset_key && ( is_user_logged_in() || ( false === $this->must_create_account && empty( $this->posted['createaccount'] ) ) ) ) {
433
					continue;
434
				}
435
436
				foreach ( $fieldset as $key => $field ) {
437
438
					if ( ! isset( $field['type'] ) ) {
439
						$field['type'] = 'text';
440
					}
441
442
					// Get Value
443
					switch ( $field['type'] ) {
444
						case "checkbox" :
445
							$this->posted[ $key ] = isset( $_POST[ $key ] ) ? 1 : 0;
446
						break;
447
						case "multiselect" :
448
							$this->posted[ $key ] = isset( $_POST[ $key ] ) ? implode( ', ', array_map( 'wc_clean', $_POST[ $key ] ) ) : '';
449
						break;
450
						case "textarea" :
451
							$this->posted[ $key ] = isset( $_POST[ $key ] ) ? wp_strip_all_tags( wp_check_invalid_utf8( stripslashes( $_POST[ $key ] ) ) ) : '';
452
						break;
453
						default :
454
							$this->posted[ $key ] = isset( $_POST[ $key ] ) ? ( is_array( $_POST[ $key ] ) ? array_map( 'wc_clean', $_POST[ $key ] ) : wc_clean( $_POST[ $key ] ) ) : '';
455
						break;
456
					}
457
458
					// Hooks to allow modification of value
459
					$this->posted[ $key ] = apply_filters( 'woocommerce_process_checkout_' . sanitize_title( $field['type'] ) . '_field', $this->posted[ $key ] );
460
					$this->posted[ $key ] = apply_filters( 'woocommerce_process_checkout_field_' . $key, $this->posted[ $key ] );
461
462
					// Validation: Required fields
463
					if ( isset( $field['required'] ) && $field['required'] && ( ! isset( $this->posted[ $key ] ) || "" === $this->posted[ $key ] ) ) {
464
						switch ( $fieldset_key ) {
465
							case 'shipping' :
466
								$field_label = sprintf( _x( 'Shipping %s', 'Shipping FIELDNAME', 'woocommerce' ), $field['label'] );
467
							break;
468
							case 'billing' :
469
								$field_label = sprintf( _x( 'Billing %s', 'Billing FIELDNAME', 'woocommerce' ), $field['label'] );
470
							break;
471
							default :
472
								$field_label = $field['label'];
473
							break;
474
						}
475
						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' );
476
					}
477
478
					if ( ! empty( $this->posted[ $key ] ) ) {
479
480
						// Validation rules
481
						if ( ! empty( $field['validate'] ) && is_array( $field['validate'] ) ) {
482
							foreach ( $field['validate'] as $rule ) {
483
								switch ( $rule ) {
484
									case 'postcode' :
485
										$this->posted[ $key ] = strtoupper( str_replace( ' ', '', $this->posted[ $key ] ) );
486
487
										if ( ! WC_Validation::is_postcode( $this->posted[ $key ], $_POST[ $fieldset_key . '_country' ] ) ) :
488
											wc_add_notice( __( 'Please enter a valid postcode/ZIP.', 'woocommerce' ), 'error' );
489
										else :
490
											$this->posted[ $key ] = wc_format_postcode( $this->posted[ $key ], $_POST[ $fieldset_key . '_country' ] );
491
										endif;
492
									break;
493 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...
494
										$this->posted[ $key ] = wc_format_phone_number( $this->posted[ $key ] );
495
496
										if ( ! WC_Validation::is_phone( $this->posted[ $key ] ) )
497
											wc_add_notice( '<strong>' . $field['label'] . '</strong> ' . __( 'is not a valid phone number.', 'woocommerce' ), 'error' );
498
									break;
499 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...
500
										$this->posted[ $key ] = strtolower( $this->posted[ $key ] );
501
502
										if ( ! is_email( $this->posted[ $key ] ) )
503
											wc_add_notice( '<strong>' . $field['label'] . '</strong> ' . __( 'is not a valid email address.', 'woocommerce' ), 'error' );
504
									break;
505
									case 'state' :
506
										// Get valid states
507
										$valid_states = WC()->countries->get_states( isset( $_POST[ $fieldset_key . '_country' ] ) ? $_POST[ $fieldset_key . '_country' ] : ( 'billing' === $fieldset_key ? WC()->customer->get_billing_country() : WC()->customer->get_shipping_country() ) );
508
509
										if ( ! empty( $valid_states ) && is_array( $valid_states ) ) {
510
											$valid_state_values = array_flip( array_map( 'strtolower', $valid_states ) );
511
512
											// Convert value to key if set
513
											if ( isset( $valid_state_values[ strtolower( $this->posted[ $key ] ) ] ) ) {
514
												 $this->posted[ $key ] = $valid_state_values[ strtolower( $this->posted[ $key ] ) ];
515
											}
516
										}
517
518
										// Only validate if the country has specific state options
519
										if ( ! empty( $valid_states ) && is_array( $valid_states ) && sizeof( $valid_states ) > 0 ) {
520
											if ( ! in_array( $this->posted[ $key ], array_keys( $valid_states ) ) ) {
521
												wc_add_notice( '<strong>' . $field['label'] . '</strong> ' . __( 'is not valid. Please enter one of the following:', 'woocommerce' ) . ' ' . implode( ', ', $valid_states ), 'error' );
522
											}
523
										}
524
									break;
525
								}
526
							}
527
						}
528
					}
529
				}
530
			}
531
532
			// Update customer location to posted location so we can correctly check available shipping methods
533
			if ( isset( $this->posted['billing_country'] ) ) {
534
				WC()->customer->set_billing_country( $this->posted['billing_country'] );
535
			}
536
			if ( isset( $this->posted['billing_state'] ) ) {
537
				WC()->customer->set_billing_state( $this->posted['billing_state'] );
538
			}
539
			if ( isset( $this->posted['billing_postcode'] ) ) {
540
				WC()->customer->set_billing_postcode( $this->posted['billing_postcode'] );
541
			}
542
543
			// Shipping Information
544
			if ( ! $skipped_shipping ) {
545
546
				// Update customer location to posted location so we can correctly check available shipping methods
547
				if ( isset( $this->posted['shipping_country'] ) ) {
548
					WC()->customer->set_shipping_country( $this->posted['shipping_country'] );
549
				}
550
				if ( isset( $this->posted['shipping_state'] ) ) {
551
					WC()->customer->set_shipping_state( $this->posted['shipping_state'] );
552
				}
553
				if ( isset( $this->posted['shipping_postcode'] ) ) {
554
					WC()->customer->set_shipping_postcode( $this->posted['shipping_postcode'] );
555
				}
556
			} else {
557
558
				// Update customer location to posted location so we can correctly check available shipping methods
559
				if ( isset( $this->posted['billing_country'] ) ) {
560
					WC()->customer->set_shipping_country( $this->posted['billing_country'] );
561
				}
562
				if ( isset( $this->posted['billing_state'] ) ) {
563
					WC()->customer->set_shipping_state( $this->posted['billing_state'] );
564
				}
565
				if ( isset( $this->posted['billing_postcode'] ) ) {
566
					WC()->customer->set_shipping_postcode( $this->posted['billing_postcode'] );
567
				}
568
			}
569
570
			WC()->customer->save();
571
572
			// Update cart totals now we have customer address
573
			WC()->cart->calculate_totals();
574
575
			// Terms
576
			if ( ! isset( $_POST['woocommerce_checkout_update_totals'] ) && empty( $this->posted['terms'] ) && wc_get_page_id( 'terms' ) > 0 && apply_filters( 'woocommerce_checkout_show_terms', true ) ) {
577
				wc_add_notice( __( 'You must accept our Terms &amp; Conditions.', 'woocommerce' ), 'error' );
578
			}
579
580
			if ( WC()->cart->needs_shipping() ) {
581
				$shipping_country = WC()->customer->get_shipping_country();
582
583
				if ( empty( $shipping_country ) ) {
584
					wc_add_notice( __( 'Please enter an address to continue.', 'woocommerce' ), 'error' );
585
				} elseif ( ! in_array( WC()->customer->get_shipping_country(), array_keys( WC()->countries->get_shipping_countries() ) ) ) {
586
					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' );
587
				}
588
589
				// Validate Shipping Methods
590
				$packages               = WC()->shipping->get_packages();
591
				$this->shipping_methods = (array) WC()->session->get( 'chosen_shipping_methods' );
592
593
				foreach ( $packages as $i => $package ) {
594
					if ( ! isset( $package['rates'][ $this->shipping_methods[ $i ] ] ) ) {
595
						wc_add_notice( __( 'No shipping method has been selected. Please double check your address, or contact us if you need any help.', 'woocommerce' ), 'error' );
596
						$this->shipping_methods[ $i ] = '';
597
					}
598
				}
599
			}
600
601
			if ( WC()->cart->needs_payment() ) {
602
				// Payment Method
603
				$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
604
605
				if ( ! isset( $available_gateways[ $this->posted['payment_method'] ] ) ) {
606
					$this->payment_method = '';
607
					wc_add_notice( __( 'Invalid payment method.', 'woocommerce' ), 'error' );
608
				} else {
609
					$this->payment_method = $available_gateways[ $this->posted['payment_method'] ];
610
					$this->payment_method->validate_fields();
611
				}
612
			} else {
613
				$available_gateways = array();
614
			}
615
616
			// Action after validation
617
			do_action( 'woocommerce_after_checkout_validation', $this->posted );
618
619
			if ( ! isset( $_POST['woocommerce_checkout_update_totals'] ) && wc_notice_count( 'error' ) == 0 ) {
620
621
				// Customer accounts
622
				$this->customer_id = apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() );
623
624
				if ( ! is_user_logged_in() && ( $this->must_create_account || ! empty( $this->posted['createaccount'] ) ) ) {
625
626
					$username     = ! empty( $this->posted['account_username'] ) ? $this->posted['account_username'] : '';
627
					$password     = ! empty( $this->posted['account_password'] ) ? $this->posted['account_password'] : '';
628
					$new_customer = wc_create_new_customer( $this->posted['billing_email'], $username, $password );
629
630
					if ( is_wp_error( $new_customer ) ) {
631
						throw new Exception( $new_customer->get_error_message() );
632
					} else {
633
						$this->customer_id = absint( $new_customer );
634
					}
635
636
					wc_set_customer_auth_cookie( $this->customer_id );
637
638
					// As we are now logged in, checkout will need to refresh to show logged in data
639
					WC()->session->set( 'reload_checkout', true );
640
641
					// Also, recalculate cart totals to reveal any role-based discounts that were unavailable before registering
642
					WC()->cart->calculate_totals();
643
644
					// Add customer info from other billing fields
645
					if ( $this->posted['billing_first_name'] && apply_filters( 'woocommerce_checkout_update_customer_data', true, $this ) ) {
646
						$userdata = array(
647
							'ID'           => $this->customer_id,
648
							'first_name'   => $this->posted['billing_first_name'] ? $this->posted['billing_first_name'] : '',
649
							'last_name'    => $this->posted['billing_last_name'] ? $this->posted['billing_last_name'] : '',
650
							'display_name' => $this->posted['billing_first_name'] ? $this->posted['billing_first_name'] : '',
651
						);
652
						wp_update_user( apply_filters( 'woocommerce_checkout_customer_userdata', $userdata, $this ) );
653
					}
654
				}
655
656
				// Do a final stock check at this point
657
				$this->check_cart_items();
658
659
				// Abort if errors are present
660
				if ( wc_notice_count( 'error' ) > 0 ) {
661
					return false;
662
				}
663
664
				$order_id = $this->create_order();
665
666
				if ( is_wp_error( $order_id ) ) {
667
					throw new Exception( $order_id->get_error_message() );
668
				}
669
670
				do_action( 'woocommerce_checkout_order_processed', $order_id, $this->posted );
671
672
				// Process payment
673
				if ( WC()->cart->needs_payment() ) {
674
675
					// Store Order ID in session so it can be re-used after payment failure
676
					WC()->session->order_awaiting_payment = $order_id;
677
678
					// Process Payment
679
					$result = $available_gateways[ $this->posted['payment_method'] ]->process_payment( $order_id );
680
681
					// Redirect to success/confirmation/payment page
682
					if ( isset( $result['result'] ) && 'success' === $result['result'] ) {
683
684
						$result = apply_filters( 'woocommerce_payment_successful_result', $result, $order_id );
685
686
						if ( is_ajax() ) {
687
							wp_send_json( $result );
688
						} else {
689
							wp_redirect( $result['redirect'] );
690
							exit;
691
						}
692
					}
693
				} else {
694
695
					if ( empty( $order ) ) {
696
						$order = wc_get_order( $order_id );
697
					}
698
699
					// No payment was required for order
700
					$order->payment_complete();
701
702
					// Empty the Cart
703
					WC()->cart->empty_cart();
704
705
					// Get redirect
706
					$return_url = $order->get_checkout_order_received_url();
707
708
					// Redirect to success/confirmation/payment page
709
					if ( is_ajax() ) {
710
						wp_send_json( array(
711
							'result' 	=> 'success',
712
							'redirect'  => apply_filters( 'woocommerce_checkout_no_payment_needed_redirect', $return_url, $order ),
713
						) );
714
					} else {
715
						wp_safe_redirect(
716
							apply_filters( 'woocommerce_checkout_no_payment_needed_redirect', $return_url, $order )
717
						);
718
						exit;
719
					}
720
				}
721
			}
722
		} catch ( Exception $e ) {
723
			if ( ! empty( $e ) ) {
724
				wc_add_notice( $e->getMessage(), 'error' );
725
			}
726
		}
727
728
		// If we reached this point then there were errors
729
		if ( is_ajax() ) {
730
731
			// only print notices if not reloading the checkout, otherwise they're lost in the page reload
732
			if ( ! isset( WC()->session->reload_checkout ) ) {
733
				ob_start();
734
				wc_print_notices();
735
				$messages = ob_get_clean();
736
			}
737
738
			$response = array(
739
				'result'	=> 'failure',
740
				'messages' 	=> isset( $messages ) ? $messages : '',
741
				'refresh' 	=> isset( WC()->session->refresh_totals ) ? 'true' : 'false',
742
				'reload'    => isset( WC()->session->reload_checkout ) ? 'true' : 'false',
743
			);
744
745
			unset( WC()->session->refresh_totals, WC()->session->reload_checkout );
746
747
			wp_send_json( $response );
748
		}
749
	}
750
751
	/**
752
	 * Get a posted address field after sanitization and validation.
753
	 * @param string $key
754
	 * @param string $type billing for shipping
755
	 * @return string
756
	 */
757
	public function get_posted_address_data( $key, $type = 'billing' ) {
758
		if ( 'billing' === $type || false === $this->posted['ship_to_different_address'] ) {
759
			$return = isset( $this->posted[ 'billing_' . $key ] ) ? $this->posted[ 'billing_' . $key ] : '';
760
		} else {
761
			$return = isset( $this->posted[ 'shipping_' . $key ] ) ? $this->posted[ 'shipping_' . $key ] : '';
762
		}
763
764
		// Use logged in user's billing email if neccessary
765
		if ( 'email' === $key && empty( $return ) && is_user_logged_in() ) {
766
			$current_user = wp_get_current_user();
767
			$return       = $current_user->user_email;
768
		}
769
		return $return;
770
	}
771
772
	/**
773
	 * Gets the value either from the posted data, or from the users meta data.
774
	 *
775
	 * @access public
776
	 * @param string $input
777
	 * @return string|null
778
	 */
779
	public function get_value( $input ) {
780
		if ( ! empty( $_POST[ $input ] ) ) {
781
782
			return wc_clean( $_POST[ $input ] );
783
784
		} else {
785
786
			$value = apply_filters( 'woocommerce_checkout_get_value', null, $input );
787
788
			if ( null !== $value ) {
789
				return $value;
790
			}
791
792
			// Get the billing_ and shipping_ address fields
793
			if ( isset( $this->checkout_fields['shipping'] ) && isset( $this->checkout_fields['billing'] ) ) {
794
795
				$address_fields = array_merge( $this->checkout_fields['billing'], $this->checkout_fields['shipping'] );
796
797
				if ( is_user_logged_in() && is_array( $address_fields ) && array_key_exists( $input, $address_fields ) ) {
798
					$current_user = wp_get_current_user();
799
800
					if ( $meta = get_user_meta( $current_user->ID, $input, true ) ) {
801
						return $meta;
802
					}
803
804
					if ( 'billing_email' === $input ) {
805
						return $current_user->user_email;
806
					}
807
				}
808
			}
809
810
			switch ( $input ) {
811
				case 'billing_country' :
812
					return apply_filters( 'default_checkout_country', WC()->customer->get_billing_country() ? WC()->customer->get_billing_country() : '', 'billing' );
813
				case 'billing_state' :
814
					return apply_filters( 'default_checkout_state', WC()->customer->get_billing_state() ? WC()->customer->get_billing_state() : '', 'billing' );
815
				case 'billing_postcode' :
816
					return apply_filters( 'default_checkout_postcode', WC()->customer->get_billing_postcode() ? WC()->customer->get_billing_postcode() : '', 'billing' );
817
				case 'shipping_country' :
818
					return apply_filters( 'default_checkout_country', WC()->customer->get_shipping_country() ? WC()->customer->get_shipping_country() : '', 'shipping' );
819
				case 'shipping_state' :
820
					return apply_filters( 'default_checkout_state', WC()->customer->get_shipping_state() ? WC()->customer->get_shipping_state() : '', 'shipping' );
821
				case 'shipping_postcode' :
822
					return apply_filters( 'default_checkout_postcode', WC()->customer->get_shipping_postcode() ? WC()->customer->get_shipping_postcode() : '', 'shipping' );
823
				default :
824
					return apply_filters( 'default_checkout_' . $input, null, $input );
825
			}
826
		}
827
	}
828
}
829