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

WC_Checkout   D

Complexity

Total Complexity 178

Size/Duplication

Total Lines 818
Duplicated Lines 5.13 %

Coupling/Cohesion

Components 2
Dependencies 9

Importance

Changes 0
Metric Value
dl 42
loc 818
rs 4.4444
c 0
b 0
f 0
wmc 178
lcom 2
cbo 9

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