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 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 |
||
16 | class WC_Checkout { |
||
17 | |||
18 | /** |
||
19 | * The single instance of the class. |
||
20 | * |
||
21 | * @var WC_Checkout|null |
||
22 | */ |
||
23 | protected static $instance = null; |
||
24 | |||
25 | /** |
||
26 | * Checkout fields are stored here. |
||
27 | * |
||
28 | * @var array|null |
||
29 | */ |
||
30 | protected $fields = null; |
||
31 | |||
32 | /** |
||
33 | * Holds posted data for backwards compatibility. |
||
34 | * |
||
35 | * @var array |
||
36 | */ |
||
37 | protected $legacy_posted_data = array(); |
||
38 | |||
39 | /** |
||
40 | * Caches customer object. @see get_value. |
||
41 | * |
||
42 | * @var WC_Customer |
||
43 | */ |
||
44 | private $logged_in_customer = null; |
||
45 | |||
46 | /** |
||
47 | * Gets the main WC_Checkout Instance. |
||
48 | * |
||
49 | * @since 2.1 |
||
50 | * @static |
||
51 | * @return WC_Checkout Main instance |
||
52 | */ |
||
53 | 1 | public static function instance() { |
|
54 | 1 | if ( is_null( self::$instance ) ) { |
|
55 | 1 | self::$instance = new self(); |
|
56 | |||
57 | // Hook in actions once. |
||
58 | 1 | add_action( 'woocommerce_checkout_billing', array( self::$instance, 'checkout_form_billing' ) ); |
|
59 | 1 | add_action( 'woocommerce_checkout_shipping', array( self::$instance, 'checkout_form_shipping' ) ); |
|
60 | |||
61 | // woocommerce_checkout_init action is ran once when the class is first constructed. |
||
62 | 1 | do_action( 'woocommerce_checkout_init', self::$instance ); |
|
63 | } |
||
64 | 1 | return self::$instance; |
|
65 | } |
||
66 | |||
67 | /** |
||
68 | * See if variable is set. Used to support legacy public variables which are no longer defined. |
||
69 | * |
||
70 | * @param string $key Key. |
||
71 | * @return bool |
||
72 | */ |
||
73 | public function __isset( $key ) { |
||
74 | return in_array( |
||
75 | $key, |
||
76 | array( |
||
77 | 'enable_signup', |
||
78 | 'enable_guest_checkout', |
||
79 | 'must_create_account', |
||
80 | 'checkout_fields', |
||
81 | 'posted', |
||
82 | 'shipping_method', |
||
83 | 'payment_method', |
||
84 | 'customer_id', |
||
85 | 'shipping_methods', |
||
86 | ), |
||
87 | true |
||
88 | ); |
||
89 | } |
||
90 | |||
91 | /** |
||
92 | * Sets the legacy public variables for backwards compatibility. |
||
93 | * |
||
94 | * @param string $key Key. |
||
95 | * @param mixed $value Value. |
||
96 | */ |
||
97 | public function __set( $key, $value ) { |
||
98 | switch ( $key ) { |
||
99 | View Code Duplication | case 'enable_signup': |
|
100 | $bool_value = wc_string_to_bool( $value ); |
||
101 | |||
102 | if ( $bool_value !== $this->is_registration_enabled() ) { |
||
103 | remove_filter( 'woocommerce_checkout_registration_enabled', '__return_true', 0 ); |
||
104 | remove_filter( 'woocommerce_checkout_registration_enabled', '__return_false', 0 ); |
||
105 | add_filter( 'woocommerce_checkout_registration_enabled', $bool_value ? '__return_true' : '__return_false', 0 ); |
||
106 | } |
||
107 | break; |
||
108 | View Code Duplication | case 'enable_guest_checkout': |
|
109 | $bool_value = wc_string_to_bool( $value ); |
||
110 | |||
111 | if ( $bool_value === $this->is_registration_required() ) { |
||
112 | remove_filter( 'woocommerce_checkout_registration_required', '__return_true', 0 ); |
||
113 | remove_filter( 'woocommerce_checkout_registration_required', '__return_false', 0 ); |
||
114 | add_filter( 'woocommerce_checkout_registration_required', $bool_value ? '__return_false' : '__return_true', 0 ); |
||
115 | } |
||
116 | break; |
||
117 | case 'checkout_fields': |
||
118 | $this->fields = $value; |
||
119 | break; |
||
120 | case 'shipping_methods': |
||
121 | WC()->session->set( 'chosen_shipping_methods', $value ); |
||
122 | break; |
||
123 | case 'posted': |
||
124 | $this->legacy_posted_data = $value; |
||
|
|||
125 | break; |
||
126 | } |
||
127 | } |
||
128 | |||
129 | /** |
||
130 | * Gets the legacy public variables for backwards compatibility. |
||
131 | * |
||
132 | * @param string $key Key. |
||
133 | * @return array|string |
||
134 | */ |
||
135 | public function __get( $key ) { |
||
136 | if ( in_array( $key, array( 'posted', 'shipping_method', 'payment_method' ), true ) && empty( $this->legacy_posted_data ) ) { |
||
137 | $this->legacy_posted_data = $this->get_posted_data(); |
||
138 | } |
||
139 | |||
140 | switch ( $key ) { |
||
141 | case 'enable_signup': |
||
142 | return $this->is_registration_enabled(); |
||
143 | case 'enable_guest_checkout': |
||
144 | return ! $this->is_registration_required(); |
||
145 | case 'must_create_account': |
||
146 | return $this->is_registration_required() && ! is_user_logged_in(); |
||
147 | case 'checkout_fields': |
||
148 | return $this->get_checkout_fields(); |
||
149 | case 'posted': |
||
150 | wc_doing_it_wrong( 'WC_Checkout->posted', 'Use $_POST directly.', '3.0.0' ); |
||
151 | return $this->legacy_posted_data; |
||
152 | case 'shipping_method': |
||
153 | return $this->legacy_posted_data['shipping_method']; |
||
154 | case 'payment_method': |
||
155 | return $this->legacy_posted_data['payment_method']; |
||
156 | case 'customer_id': |
||
157 | return apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() ); |
||
158 | case 'shipping_methods': |
||
159 | return (array) WC()->session->get( 'chosen_shipping_methods' ); |
||
160 | } |
||
161 | } |
||
162 | |||
163 | /** |
||
164 | * Cloning is forbidden. |
||
165 | */ |
||
166 | public function __clone() { |
||
169 | |||
170 | /** |
||
171 | * Unserializing instances of this class is forbidden. |
||
172 | */ |
||
173 | public function __wakeup() { |
||
176 | |||
177 | /** |
||
178 | * Is registration required to checkout? |
||
179 | * |
||
180 | * @since 3.0.0 |
||
181 | * @return boolean |
||
182 | */ |
||
183 | public function is_registration_required() { |
||
184 | return apply_filters( 'woocommerce_checkout_registration_required', 'yes' !== get_option( 'woocommerce_enable_guest_checkout' ) ); |
||
185 | } |
||
186 | |||
187 | /** |
||
188 | * Is registration enabled on the checkout page? |
||
189 | * |
||
190 | * @since 3.0.0 |
||
191 | * @return boolean |
||
192 | */ |
||
193 | public function is_registration_enabled() { |
||
194 | return apply_filters( 'woocommerce_checkout_registration_enabled', 'yes' === get_option( 'woocommerce_enable_signup_and_login_from_checkout' ) ); |
||
195 | } |
||
196 | |||
197 | /** |
||
198 | * Get an array of checkout fields. |
||
199 | * |
||
200 | * @param string $fieldset to get. |
||
201 | * @return array |
||
202 | */ |
||
203 | public function get_checkout_fields( $fieldset = '' ) { |
||
204 | if ( ! is_null( $this->fields ) ) { |
||
205 | return $fieldset ? $this->fields[ $fieldset ] : $this->fields; |
||
206 | } |
||
207 | |||
208 | // Fields are based on billing/shipping country. Grab those values but ensure they are valid for the store before using. |
||
209 | $billing_country = $this->get_value( 'billing_country' ); |
||
210 | $billing_country = empty( $billing_country ) ? WC()->countries->get_base_country() : $billing_country; |
||
211 | $allowed_countries = WC()->countries->get_allowed_countries(); |
||
212 | |||
213 | if ( ! array_key_exists( $billing_country, $allowed_countries ) ) { |
||
214 | $billing_country = current( array_keys( $allowed_countries ) ); |
||
215 | } |
||
216 | |||
217 | $shipping_country = $this->get_value( 'shipping_country' ); |
||
218 | $shipping_country = empty( $shipping_country ) ? WC()->countries->get_base_country() : $shipping_country; |
||
219 | $allowed_countries = WC()->countries->get_shipping_countries(); |
||
220 | |||
221 | if ( ! array_key_exists( $shipping_country, $allowed_countries ) ) { |
||
222 | $shipping_country = current( array_keys( $allowed_countries ) ); |
||
223 | } |
||
224 | |||
225 | $this->fields = array( |
||
226 | 'billing' => WC()->countries->get_address_fields( |
||
227 | $billing_country, |
||
228 | 'billing_' |
||
229 | ), |
||
230 | 'shipping' => WC()->countries->get_address_fields( |
||
231 | $shipping_country, |
||
232 | 'shipping_' |
||
233 | ), |
||
234 | 'account' => array(), |
||
235 | 'order' => array( |
||
236 | 'order_comments' => array( |
||
237 | 'type' => 'textarea', |
||
238 | 'class' => array( 'notes' ), |
||
239 | 'label' => __( 'Order notes', 'woocommerce' ), |
||
240 | 'placeholder' => esc_attr__( |
||
241 | 'Notes about your order, e.g. special notes for delivery.', |
||
242 | 'woocommerce' |
||
243 | ), |
||
244 | ), |
||
245 | ), |
||
246 | ); |
||
247 | |||
248 | View Code Duplication | if ( 'no' === get_option( 'woocommerce_registration_generate_username' ) ) { |
|
249 | $this->fields['account']['account_username'] = array( |
||
250 | 'type' => 'text', |
||
251 | 'label' => __( 'Account username', 'woocommerce' ), |
||
252 | 'required' => true, |
||
253 | 'placeholder' => esc_attr__( 'Username', 'woocommerce' ), |
||
254 | ); |
||
255 | } |
||
256 | |||
257 | View Code Duplication | if ( 'no' === get_option( 'woocommerce_registration_generate_password' ) ) { |
|
258 | $this->fields['account']['account_password'] = array( |
||
259 | 'type' => 'password', |
||
260 | 'label' => __( 'Create account password', 'woocommerce' ), |
||
261 | 'required' => true, |
||
262 | 'placeholder' => esc_attr__( 'Password', 'woocommerce' ), |
||
263 | ); |
||
264 | } |
||
265 | $this->fields = apply_filters( 'woocommerce_checkout_fields', $this->fields ); |
||
266 | |||
267 | foreach ( $this->fields as $field_type => $fields ) { |
||
268 | // Sort each of the checkout field sections based on priority. |
||
269 | uasort( $this->fields[ $field_type ], 'wc_checkout_fields_uasort_comparison' ); |
||
270 | |||
271 | // Add accessibility labels to fields that have placeholders. |
||
272 | foreach ( $fields as $single_field_type => $field ) { |
||
273 | if ( empty( $field['label'] ) && ! empty( $field['placeholder'] ) ) { |
||
274 | $this->fields[ $field_type ][ $single_field_type ]['label'] = $field['placeholder']; |
||
275 | $this->fields[ $field_type ][ $single_field_type ]['label_class'] = 'screen-reader-text'; |
||
276 | } |
||
277 | } |
||
278 | } |
||
279 | |||
280 | return $fieldset ? $this->fields[ $fieldset ] : $this->fields; |
||
281 | } |
||
282 | |||
283 | /** |
||
284 | * When we process the checkout, lets ensure cart items are rechecked to prevent checkout. |
||
285 | */ |
||
286 | public function check_cart_items() { |
||
289 | |||
290 | /** |
||
291 | * Output the billing form. |
||
292 | */ |
||
293 | public function checkout_form_billing() { |
||
296 | |||
297 | /** |
||
298 | * Output the shipping form. |
||
299 | */ |
||
300 | public function checkout_form_shipping() { |
||
303 | |||
304 | /** |
||
305 | * Create an order. Error codes: |
||
306 | * 520 - Cannot insert order into the database. |
||
307 | * 521 - Cannot get order after creation. |
||
308 | * 522 - Cannot update order. |
||
309 | * 525 - Cannot create line item. |
||
310 | * 526 - Cannot create fee item. |
||
311 | * 527 - Cannot create shipping item. |
||
312 | * 528 - Cannot create tax item. |
||
313 | * 529 - Cannot create coupon item. |
||
314 | * |
||
315 | * @throws Exception When checkout validation fails. |
||
316 | * @param array $data Posted data. |
||
317 | * @return int|WP_ERROR |
||
318 | */ |
||
319 | public function create_order( $data ) { |
||
409 | |||
410 | /** |
||
411 | * Add line items to the order. |
||
412 | * |
||
413 | * @param WC_Order $order Order instance. |
||
414 | * @param WC_Cart $cart Cart instance. |
||
415 | */ |
||
416 | public function create_order_line_items( &$order, $cart ) { |
||
417 | foreach ( $cart->get_cart() as $cart_item_key => $values ) { |
||
418 | /** |
||
419 | * Filter hook to get initial item object. |
||
420 | * |
||
421 | * @since 3.1.0 |
||
422 | */ |
||
423 | $item = apply_filters( 'woocommerce_checkout_create_order_line_item_object', new WC_Order_Item_Product(), $cart_item_key, $values, $order ); |
||
424 | $product = $values['data']; |
||
425 | $item->legacy_values = $values; // @deprecated For legacy actions. |
||
426 | $item->legacy_cart_item_key = $cart_item_key; // @deprecated For legacy actions. |
||
427 | $item->set_props( |
||
428 | array( |
||
429 | 'quantity' => $values['quantity'], |
||
430 | 'variation' => $values['variation'], |
||
431 | 'subtotal' => $values['line_subtotal'], |
||
432 | 'total' => $values['line_total'], |
||
433 | 'subtotal_tax' => $values['line_subtotal_tax'], |
||
434 | 'total_tax' => $values['line_tax'], |
||
435 | 'taxes' => $values['line_tax_data'], |
||
436 | ) |
||
437 | ); |
||
438 | |||
439 | if ( $product ) { |
||
440 | $item->set_props( |
||
441 | array( |
||
442 | 'name' => $product->get_name(), |
||
443 | 'tax_class' => $product->get_tax_class(), |
||
444 | 'product_id' => $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id(), |
||
445 | 'variation_id' => $product->is_type( 'variation' ) ? $product->get_id() : 0, |
||
446 | ) |
||
447 | ); |
||
448 | } |
||
449 | |||
450 | $item->set_backorder_meta(); |
||
451 | |||
452 | /** |
||
453 | * Action hook to adjust item before save. |
||
454 | * |
||
455 | * @since 3.0.0 |
||
456 | */ |
||
457 | do_action( 'woocommerce_checkout_create_order_line_item', $item, $cart_item_key, $values, $order ); |
||
458 | |||
459 | // Add item to order and save. |
||
460 | $order->add_item( $item ); |
||
461 | } |
||
462 | } |
||
463 | |||
464 | /** |
||
465 | * Add fees to the order. |
||
466 | * |
||
467 | * @param WC_Order $order Order instance. |
||
468 | * @param WC_Cart $cart Cart instance. |
||
469 | */ |
||
470 | public function create_order_fee_lines( &$order, $cart ) { |
||
471 | foreach ( $cart->get_fees() as $fee_key => $fee ) { |
||
472 | $item = new WC_Order_Item_Fee(); |
||
473 | $item->legacy_fee = $fee; // @deprecated For legacy actions. |
||
474 | $item->legacy_fee_key = $fee_key; // @deprecated For legacy actions. |
||
475 | $item->set_props( |
||
476 | array( |
||
477 | 'name' => $fee->name, |
||
478 | 'tax_class' => $fee->taxable ? $fee->tax_class : 0, |
||
479 | 'amount' => $fee->amount, |
||
480 | 'total' => $fee->total, |
||
481 | 'total_tax' => $fee->tax, |
||
482 | 'taxes' => array( |
||
483 | 'total' => $fee->tax_data, |
||
484 | ), |
||
485 | ) |
||
486 | ); |
||
487 | |||
488 | /** |
||
489 | * Action hook to adjust item before save. |
||
490 | * |
||
491 | * @since 3.0.0 |
||
492 | */ |
||
493 | do_action( 'woocommerce_checkout_create_order_fee_item', $item, $fee_key, $fee, $order ); |
||
494 | |||
495 | // Add item to order and save. |
||
496 | $order->add_item( $item ); |
||
497 | } |
||
498 | } |
||
499 | |||
500 | /** |
||
501 | * Add shipping lines to the order. |
||
502 | * |
||
503 | * @param WC_Order $order Order Instance. |
||
504 | * @param array $chosen_shipping_methods Chosen shipping methods. |
||
505 | * @param array $packages Packages. |
||
506 | */ |
||
507 | public function create_order_shipping_lines( &$order, $chosen_shipping_methods, $packages ) { |
||
508 | foreach ( $packages as $package_key => $package ) { |
||
509 | if ( isset( $chosen_shipping_methods[ $package_key ], $package['rates'][ $chosen_shipping_methods[ $package_key ] ] ) ) { |
||
510 | $shipping_rate = $package['rates'][ $chosen_shipping_methods[ $package_key ] ]; |
||
511 | $item = new WC_Order_Item_Shipping(); |
||
512 | $item->legacy_package_key = $package_key; // @deprecated For legacy actions. |
||
513 | $item->set_props( |
||
514 | array( |
||
515 | 'method_title' => $shipping_rate->label, |
||
516 | 'method_id' => $shipping_rate->method_id, |
||
517 | 'instance_id' => $shipping_rate->instance_id, |
||
518 | 'total' => wc_format_decimal( $shipping_rate->cost ), |
||
519 | 'taxes' => array( |
||
520 | 'total' => $shipping_rate->taxes, |
||
521 | ), |
||
522 | ) |
||
523 | ); |
||
524 | |||
525 | foreach ( $shipping_rate->get_meta_data() as $key => $value ) { |
||
526 | $item->add_meta_data( $key, $value, true ); |
||
527 | } |
||
528 | |||
529 | /** |
||
530 | * Action hook to adjust item before save. |
||
531 | * |
||
532 | * @since 3.0.0 |
||
533 | */ |
||
534 | do_action( 'woocommerce_checkout_create_order_shipping_item', $item, $package_key, $package, $order ); |
||
535 | |||
536 | // Add item to order and save. |
||
537 | $order->add_item( $item ); |
||
538 | } |
||
539 | } |
||
540 | } |
||
541 | |||
542 | /** |
||
543 | * Add tax lines to the order. |
||
544 | * |
||
545 | * @param WC_Order $order Order instance. |
||
546 | * @param WC_Cart $cart Cart instance. |
||
547 | */ |
||
548 | public function create_order_tax_lines( &$order, $cart ) { |
||
549 | foreach ( array_keys( $cart->get_cart_contents_taxes() + $cart->get_shipping_taxes() + $cart->get_fee_taxes() ) as $tax_rate_id ) { |
||
550 | if ( $tax_rate_id && apply_filters( 'woocommerce_cart_remove_taxes_zero_rate_id', 'zero-rated' ) !== $tax_rate_id ) { |
||
551 | $item = new WC_Order_Item_Tax(); |
||
552 | $item->set_props( |
||
553 | array( |
||
554 | 'rate_id' => $tax_rate_id, |
||
555 | 'tax_total' => $cart->get_tax_amount( $tax_rate_id ), |
||
556 | 'shipping_tax_total' => $cart->get_shipping_tax_amount( $tax_rate_id ), |
||
557 | 'rate_code' => WC_Tax::get_rate_code( $tax_rate_id ), |
||
558 | 'label' => WC_Tax::get_rate_label( $tax_rate_id ), |
||
559 | 'compound' => WC_Tax::is_compound( $tax_rate_id ), |
||
560 | 'rate_percent' => WC_Tax::get_rate_percent_value( $tax_rate_id ), |
||
561 | ) |
||
562 | ); |
||
563 | |||
564 | /** |
||
565 | * Action hook to adjust item before save. |
||
566 | * |
||
567 | * @since 3.0.0 |
||
568 | */ |
||
569 | do_action( 'woocommerce_checkout_create_order_tax_item', $item, $tax_rate_id, $order ); |
||
570 | |||
571 | // Add item to order and save. |
||
572 | $order->add_item( $item ); |
||
573 | } |
||
574 | } |
||
575 | } |
||
576 | |||
577 | /** |
||
578 | * Add coupon lines to the order. |
||
579 | * |
||
580 | * @param WC_Order $order Order instance. |
||
581 | * @param WC_Cart $cart Cart instance. |
||
582 | */ |
||
583 | public function create_order_coupon_lines( &$order, $cart ) { |
||
584 | foreach ( $cart->get_coupons() as $code => $coupon ) { |
||
585 | $item = new WC_Order_Item_Coupon(); |
||
586 | $item->set_props( |
||
587 | array( |
||
588 | 'code' => $code, |
||
589 | 'discount' => $cart->get_coupon_discount_amount( $code ), |
||
590 | 'discount_tax' => $cart->get_coupon_discount_tax_amount( $code ), |
||
591 | ) |
||
592 | ); |
||
593 | |||
594 | // Avoid storing used_by - it's not needed and can get large. |
||
595 | $coupon_data = $coupon->get_data(); |
||
596 | unset( $coupon_data['used_by'] ); |
||
597 | $item->add_meta_data( 'coupon_data', $coupon_data ); |
||
598 | |||
599 | /** |
||
600 | * Action hook to adjust item before save. |
||
601 | * |
||
602 | * @since 3.0.0 |
||
603 | */ |
||
604 | do_action( 'woocommerce_checkout_create_order_coupon_item', $item, $code, $coupon, $order ); |
||
605 | |||
606 | // Add item to order and save. |
||
607 | $order->add_item( $item ); |
||
608 | } |
||
609 | } |
||
610 | |||
611 | /** |
||
612 | * See if a fieldset should be skipped. |
||
613 | * |
||
614 | * @since 3.0.0 |
||
615 | * @param string $fieldset_key Fieldset key. |
||
616 | * @param array $data Posted data. |
||
617 | * @return bool |
||
618 | */ |
||
619 | protected function maybe_skip_fieldset( $fieldset_key, $data ) { |
||
620 | if ( 'shipping' === $fieldset_key && ( ! $data['ship_to_different_address'] || ! WC()->cart->needs_shipping_address() ) ) { |
||
621 | return true; |
||
622 | } |
||
623 | |||
624 | if ( 'account' === $fieldset_key && ( is_user_logged_in() || ( ! $this->is_registration_required() && empty( $data['createaccount'] ) ) ) ) { |
||
625 | return true; |
||
626 | } |
||
627 | |||
628 | return false; |
||
629 | } |
||
630 | |||
631 | /** |
||
632 | * Get posted data from the checkout form. |
||
633 | * |
||
634 | * @since 3.1.0 |
||
635 | * @return array of data. |
||
636 | */ |
||
637 | public function get_posted_data() { |
||
638 | $skipped = array(); |
||
639 | $data = array( |
||
640 | 'terms' => (int) isset( $_POST['terms'] ), // WPCS: input var ok, CSRF ok. |
||
641 | 'createaccount' => (int) ! empty( $_POST['createaccount'] ), // WPCS: input var ok, CSRF ok. |
||
642 | 'payment_method' => isset( $_POST['payment_method'] ) ? wc_clean( wp_unslash( $_POST['payment_method'] ) ) : '', // WPCS: input var ok, CSRF ok. |
||
643 | 'shipping_method' => isset( $_POST['shipping_method'] ) ? wc_clean( wp_unslash( $_POST['shipping_method'] ) ) : '', // WPCS: input var ok, CSRF ok. |
||
644 | 'ship_to_different_address' => ! empty( $_POST['ship_to_different_address'] ) && ! wc_ship_to_billing_address_only(), // WPCS: input var ok, CSRF ok. |
||
645 | 'woocommerce_checkout_update_totals' => isset( $_POST['woocommerce_checkout_update_totals'] ), // WPCS: input var ok, CSRF ok. |
||
646 | ); |
||
647 | foreach ( $this->get_checkout_fields() as $fieldset_key => $fieldset ) { |
||
648 | if ( $this->maybe_skip_fieldset( $fieldset_key, $data ) ) { |
||
649 | $skipped[] = $fieldset_key; |
||
650 | continue; |
||
651 | } |
||
652 | |||
653 | foreach ( $fieldset as $key => $field ) { |
||
654 | $type = sanitize_title( isset( $field['type'] ) ? $field['type'] : 'text' ); |
||
655 | |||
656 | switch ( $type ) { |
||
657 | case 'checkbox': |
||
658 | $value = isset( $_POST[ $key ] ) ? 1 : ''; // WPCS: input var ok, CSRF ok. |
||
659 | break; |
||
660 | case 'multiselect': |
||
661 | $value = isset( $_POST[ $key ] ) ? implode( ', ', wc_clean( wp_unslash( $_POST[ $key ] ) ) ) : ''; // WPCS: input var ok, CSRF ok. |
||
662 | break; |
||
663 | View Code Duplication | case 'textarea': |
|
664 | $value = isset( $_POST[ $key ] ) ? wc_sanitize_textarea( wp_unslash( $_POST[ $key ] ) ) : ''; // WPCS: input var ok, CSRF ok. |
||
665 | break; |
||
666 | View Code Duplication | case 'password': |
|
667 | $value = isset( $_POST[ $key ] ) ? wp_unslash( $_POST[ $key ] ) : ''; // WPCS: input var ok, CSRF ok, sanitization ok. |
||
668 | break; |
||
669 | View Code Duplication | default: |
|
670 | $value = isset( $_POST[ $key ] ) ? wc_clean( wp_unslash( $_POST[ $key ] ) ) : ''; // WPCS: input var ok, CSRF ok. |
||
671 | break; |
||
672 | } |
||
673 | |||
674 | $data[ $key ] = apply_filters( 'woocommerce_process_checkout_' . $type . '_field', apply_filters( 'woocommerce_process_checkout_field_' . $key, $value ) ); |
||
675 | } |
||
676 | } |
||
677 | |||
678 | if ( in_array( 'shipping', $skipped, true ) && ( WC()->cart->needs_shipping_address() || wc_ship_to_billing_address_only() ) ) { |
||
679 | foreach ( $this->get_checkout_fields( 'shipping' ) as $key => $field ) { |
||
680 | $data[ $key ] = isset( $data[ 'billing_' . substr( $key, 9 ) ] ) ? $data[ 'billing_' . substr( $key, 9 ) ] : ''; |
||
681 | } |
||
682 | } |
||
683 | |||
684 | // BW compatibility. |
||
685 | $this->legacy_posted_data = $data; |
||
686 | |||
687 | return apply_filters( 'woocommerce_checkout_posted_data', $data ); |
||
688 | } |
||
689 | |||
690 | /** |
||
691 | * Validates the posted checkout data based on field properties. |
||
692 | * |
||
693 | * @since 3.0.0 |
||
694 | * @param array $data An array of posted data. |
||
695 | * @param WP_Error $errors Validation error. |
||
696 | */ |
||
697 | protected function validate_posted_data( &$data, &$errors ) { |
||
698 | foreach ( $this->get_checkout_fields() as $fieldset_key => $fieldset ) { |
||
699 | $validate_fieldset = true; |
||
700 | if ( $this->maybe_skip_fieldset( $fieldset_key, $data ) ) { |
||
701 | $validate_fieldset = false; |
||
702 | } |
||
703 | |||
704 | foreach ( $fieldset as $key => $field ) { |
||
705 | if ( ! isset( $data[ $key ] ) ) { |
||
706 | continue; |
||
707 | } |
||
708 | $required = ! empty( $field['required'] ); |
||
709 | $format = array_filter( isset( $field['validate'] ) ? (array) $field['validate'] : array() ); |
||
710 | $field_label = isset( $field['label'] ) ? $field['label'] : ''; |
||
711 | |||
712 | switch ( $fieldset_key ) { |
||
713 | case 'shipping': |
||
714 | /* translators: %s: field name */ |
||
715 | $field_label = sprintf( __( 'Shipping %s', 'woocommerce' ), $field_label ); |
||
716 | break; |
||
717 | case 'billing': |
||
718 | /* translators: %s: field name */ |
||
719 | $field_label = sprintf( __( 'Billing %s', 'woocommerce' ), $field_label ); |
||
720 | break; |
||
721 | } |
||
722 | |||
723 | if ( in_array( 'postcode', $format, true ) ) { |
||
724 | $country = isset( $data[ $fieldset_key . '_country' ] ) ? $data[ $fieldset_key . '_country' ] : WC()->customer->{"get_{$fieldset_key}_country"}(); |
||
725 | $data[ $key ] = wc_format_postcode( $data[ $key ], $country ); |
||
726 | |||
727 | if ( $validate_fieldset && '' !== $data[ $key ] && ! WC_Validation::is_postcode( $data[ $key ], $country ) ) { |
||
728 | switch ( $country ) { |
||
729 | View Code Duplication | case 'IE': |
|
730 | /* translators: %1$s: field name, %2$s finder.eircode.ie URL */ |
||
731 | $postcode_validation_notice = sprintf( __( '%1$s is not valid. You can look up the correct Eircode <a target="_blank" href="%2$s">here</a>.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>', 'https://finder.eircode.ie' ); |
||
732 | break; |
||
733 | View Code Duplication | default: |
|
734 | /* translators: %s: field name */ |
||
735 | $postcode_validation_notice = sprintf( __( '%s is not a valid postcode / ZIP.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' ); |
||
736 | } |
||
737 | $errors->add( 'validation', apply_filters( 'woocommerce_checkout_postcode_validation_notice', $postcode_validation_notice, $country, $data[ $key ] ) ); |
||
738 | } |
||
739 | } |
||
740 | |||
741 | View Code Duplication | if ( in_array( 'phone', $format, true ) ) { |
|
742 | if ( $validate_fieldset && '' !== $data[ $key ] && ! WC_Validation::is_phone( $data[ $key ] ) ) { |
||
743 | /* translators: %s: phone number */ |
||
744 | $errors->add( 'validation', sprintf( __( '%s is not a valid phone number.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' ) ); |
||
745 | } |
||
746 | } |
||
747 | |||
748 | if ( in_array( 'email', $format, true ) && '' !== $data[ $key ] ) { |
||
749 | $email_is_valid = is_email( $data[ $key ] ); |
||
750 | $data[ $key ] = sanitize_email( $data[ $key ] ); |
||
751 | |||
752 | if ( $validate_fieldset && ! $email_is_valid ) { |
||
753 | /* translators: %s: email address */ |
||
754 | $errors->add( 'validation', sprintf( __( '%s is not a valid email address.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' ) ); |
||
755 | continue; |
||
756 | } |
||
757 | } |
||
758 | |||
759 | if ( '' !== $data[ $key ] && in_array( 'state', $format, true ) ) { |
||
760 | $country = isset( $data[ $fieldset_key . '_country' ] ) ? $data[ $fieldset_key . '_country' ] : WC()->customer->{"get_{$fieldset_key}_country"}(); |
||
761 | $valid_states = WC()->countries->get_states( $country ); |
||
762 | |||
763 | if ( ! empty( $valid_states ) && is_array( $valid_states ) && count( $valid_states ) > 0 ) { |
||
764 | $valid_state_values = array_map( 'wc_strtoupper', array_flip( array_map( 'wc_strtoupper', $valid_states ) ) ); |
||
765 | $data[ $key ] = wc_strtoupper( $data[ $key ] ); |
||
766 | |||
767 | if ( isset( $valid_state_values[ $data[ $key ] ] ) ) { |
||
768 | // With this part we consider state value to be valid as well, convert it to the state key for the valid_states check below. |
||
769 | $data[ $key ] = $valid_state_values[ $data[ $key ] ]; |
||
770 | } |
||
771 | |||
772 | View Code Duplication | if ( $validate_fieldset && ! in_array( $data[ $key ], $valid_state_values, true ) ) { |
|
773 | /* translators: 1: state field 2: valid states */ |
||
774 | $errors->add( 'validation', sprintf( __( '%1$s is not valid. Please enter one of the following: %2$s', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>', implode( ', ', $valid_states ) ) ); |
||
775 | } |
||
776 | } |
||
777 | } |
||
778 | |||
779 | if ( $validate_fieldset && $required && '' === $data[ $key ] ) { |
||
780 | /* translators: %s: field name */ |
||
781 | $errors->add( 'required-field', apply_filters( 'woocommerce_checkout_required_field_notice', sprintf( __( '%s is a required field.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' ), $field_label ) ); |
||
782 | } |
||
783 | } |
||
784 | } |
||
785 | } |
||
786 | |||
787 | /** |
||
788 | * Validates that the checkout has enough info to proceed. |
||
789 | * |
||
790 | * @since 3.0.0 |
||
791 | * @param array $data An array of posted data. |
||
792 | * @param WP_Error $errors Validation errors. |
||
793 | */ |
||
794 | protected function validate_checkout( &$data, &$errors ) { |
||
795 | $this->validate_posted_data( $data, $errors ); |
||
796 | $this->check_cart_items(); |
||
797 | |||
798 | if ( empty( $data['woocommerce_checkout_update_totals'] ) && empty( $data['terms'] ) && ! empty( $_POST['terms-field'] ) ) { // WPCS: input var ok, CSRF ok. |
||
799 | $errors->add( 'terms', __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' ) ); |
||
800 | } |
||
801 | |||
802 | if ( WC()->cart->needs_shipping() ) { |
||
803 | $shipping_country = WC()->customer->get_shipping_country(); |
||
804 | |||
805 | if ( empty( $shipping_country ) ) { |
||
806 | $errors->add( 'shipping', __( 'Please enter an address to continue.', 'woocommerce' ) ); |
||
807 | } elseif ( ! in_array( WC()->customer->get_shipping_country(), array_keys( WC()->countries->get_shipping_countries() ), true ) ) { |
||
808 | /* translators: %s: shipping location */ |
||
809 | $errors->add( 'shipping', 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() ) ); |
||
810 | } else { |
||
811 | $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' ); |
||
812 | |||
813 | foreach ( WC()->shipping()->get_packages() as $i => $package ) { |
||
814 | if ( ! isset( $chosen_shipping_methods[ $i ], $package['rates'][ $chosen_shipping_methods[ $i ] ] ) ) { |
||
815 | $errors->add( 'shipping', __( 'No shipping method has been selected. Please double check your address, or contact us if you need any help.', 'woocommerce' ) ); |
||
816 | } |
||
817 | } |
||
818 | } |
||
819 | } |
||
820 | |||
821 | if ( WC()->cart->needs_payment() ) { |
||
822 | $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); |
||
823 | |||
824 | if ( ! isset( $available_gateways[ $data['payment_method'] ] ) ) { |
||
825 | $errors->add( 'payment', __( 'Invalid payment method.', 'woocommerce' ) ); |
||
826 | } else { |
||
827 | $available_gateways[ $data['payment_method'] ]->validate_fields(); |
||
828 | } |
||
829 | } |
||
830 | |||
831 | do_action( 'woocommerce_after_checkout_validation', $data, $errors ); |
||
832 | } |
||
833 | |||
834 | /** |
||
835 | * Set address field for customer. |
||
836 | * |
||
837 | * @since 3.0.7 |
||
838 | * @param string $field String to update. |
||
839 | * @param string $key Field key. |
||
840 | * @param array $data Array of data to get the value from. |
||
841 | */ |
||
842 | protected function set_customer_address_fields( $field, $key, $data ) { |
||
843 | $billing_value = null; |
||
844 | $shipping_value = null; |
||
845 | |||
846 | if ( isset( $data[ "billing_{$field}" ] ) && is_callable( array( WC()->customer, "set_billing_{$field}" ) ) ) { |
||
847 | $billing_value = $data[ "billing_{$field}" ]; |
||
848 | $shipping_value = $data[ "billing_{$field}" ]; |
||
849 | } |
||
850 | |||
851 | if ( isset( $data[ "shipping_{$field}" ] ) && is_callable( array( WC()->customer, "set_shipping_{$field}" ) ) ) { |
||
852 | $shipping_value = $data[ "shipping_{$field}" ]; |
||
853 | } |
||
854 | |||
855 | View Code Duplication | if ( ! is_null( $billing_value ) && is_callable( array( WC()->customer, "set_billing_{$field}" ) ) ) { |
|
856 | WC()->customer->{"set_billing_{$field}"}( $billing_value ); |
||
857 | } |
||
858 | |||
859 | View Code Duplication | if ( ! is_null( $shipping_value ) && is_callable( array( WC()->customer, "set_shipping_{$field}" ) ) ) { |
|
860 | WC()->customer->{"set_shipping_{$field}"}( $shipping_value ); |
||
861 | } |
||
862 | } |
||
863 | |||
864 | /** |
||
865 | * Update customer and session data from the posted checkout data. |
||
866 | * |
||
867 | * @since 3.0.0 |
||
868 | * @param array $data Posted data. |
||
869 | */ |
||
870 | protected function update_session( $data ) { |
||
871 | // Update both shipping and billing to the passed billing address first if set. |
||
872 | $address_fields = array( |
||
873 | 'first_name', |
||
874 | 'last_name', |
||
875 | 'company', |
||
876 | 'email', |
||
877 | 'phone', |
||
878 | 'address_1', |
||
879 | 'address_2', |
||
880 | 'city', |
||
881 | 'postcode', |
||
882 | 'state', |
||
883 | 'country', |
||
884 | ); |
||
885 | |||
886 | array_walk( $address_fields, array( $this, 'set_customer_address_fields' ), $data ); |
||
887 | WC()->customer->save(); |
||
888 | |||
889 | // Update customer shipping and payment method to posted method. |
||
890 | $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' ); |
||
891 | |||
892 | if ( is_array( $data['shipping_method'] ) ) { |
||
893 | foreach ( $data['shipping_method'] as $i => $value ) { |
||
894 | $chosen_shipping_methods[ $i ] = $value; |
||
895 | } |
||
896 | } |
||
897 | |||
898 | WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); |
||
899 | WC()->session->set( 'chosen_payment_method', $data['payment_method'] ); |
||
900 | |||
901 | // Update cart totals now we have customer address. |
||
902 | WC()->cart->calculate_totals(); |
||
903 | } |
||
904 | |||
905 | |||
906 | /** |
||
907 | * Process an order that does require payment. |
||
908 | * |
||
909 | * @since 3.0.0 |
||
910 | * @param int $order_id Order ID. |
||
911 | * @param string $payment_method Payment method. |
||
912 | */ |
||
913 | protected function process_order_payment( $order_id, $payment_method ) { |
||
914 | $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); |
||
915 | |||
916 | if ( ! isset( $available_gateways[ $payment_method ] ) ) { |
||
917 | return; |
||
918 | } |
||
919 | |||
920 | // Store Order ID in session so it can be re-used after payment failure. |
||
921 | WC()->session->set( 'order_awaiting_payment', $order_id ); |
||
922 | |||
923 | // Process Payment. |
||
924 | $result = $available_gateways[ $payment_method ]->process_payment( $order_id ); |
||
925 | |||
926 | // Redirect to success/confirmation/payment page. |
||
927 | if ( isset( $result['result'] ) && 'success' === $result['result'] ) { |
||
928 | $result = apply_filters( 'woocommerce_payment_successful_result', $result, $order_id ); |
||
929 | |||
930 | if ( ! is_ajax() ) { |
||
931 | wp_redirect( $result['redirect'] ); |
||
932 | exit; |
||
933 | } |
||
934 | |||
935 | wp_send_json( $result ); |
||
936 | } |
||
937 | } |
||
938 | |||
939 | /** |
||
940 | * Process an order that doesn't require payment. |
||
941 | * |
||
942 | * @since 3.0.0 |
||
943 | * @param int $order_id Order ID. |
||
944 | */ |
||
945 | protected function process_order_without_payment( $order_id ) { |
||
946 | $order = wc_get_order( $order_id ); |
||
947 | $order->payment_complete(); |
||
948 | wc_empty_cart(); |
||
949 | |||
950 | if ( ! is_ajax() ) { |
||
951 | wp_safe_redirect( |
||
952 | apply_filters( 'woocommerce_checkout_no_payment_needed_redirect', $order->get_checkout_order_received_url(), $order ) |
||
953 | ); |
||
954 | exit; |
||
955 | } |
||
956 | |||
957 | wp_send_json( |
||
958 | array( |
||
959 | 'result' => 'success', |
||
960 | 'redirect' => apply_filters( 'woocommerce_checkout_no_payment_needed_redirect', $order->get_checkout_order_received_url(), $order ), |
||
961 | ) |
||
962 | ); |
||
963 | } |
||
964 | |||
965 | /** |
||
966 | * Create a new customer account if needed. |
||
967 | * |
||
968 | * @throws Exception When not able to create customer. |
||
969 | * @param array $data Posted data. |
||
970 | */ |
||
971 | protected function process_customer( $data ) { |
||
972 | $customer_id = apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() ); |
||
973 | |||
974 | if ( ! is_user_logged_in() && ( $this->is_registration_required() || ! empty( $data['createaccount'] ) ) ) { |
||
975 | $username = ! empty( $data['account_username'] ) ? $data['account_username'] : ''; |
||
976 | $password = ! empty( $data['account_password'] ) ? $data['account_password'] : ''; |
||
977 | $customer_id = wc_create_new_customer( |
||
978 | $data['billing_email'], |
||
979 | $username, |
||
980 | $password, |
||
981 | array( |
||
982 | 'first_name' => ! empty( $data['billing_first_name'] ) ? $data['billing_first_name'] : '', |
||
983 | 'last_name' => ! empty( $data['billing_last_name'] ) ? $data['billing_last_name'] : '', |
||
984 | ) |
||
985 | ); |
||
986 | |||
987 | if ( is_wp_error( $customer_id ) ) { |
||
988 | throw new Exception( $customer_id->get_error_message() ); |
||
989 | } |
||
990 | |||
991 | wc_set_customer_auth_cookie( $customer_id ); |
||
992 | |||
993 | // As we are now logged in, checkout will need to refresh to show logged in data. |
||
994 | WC()->session->set( 'reload_checkout', true ); |
||
995 | |||
996 | // Also, recalculate cart totals to reveal any role-based discounts that were unavailable before registering. |
||
997 | WC()->cart->calculate_totals(); |
||
998 | } |
||
999 | |||
1000 | // On multisite, ensure user exists on current site, if not add them before allowing login. |
||
1001 | if ( $customer_id && is_multisite() && is_user_logged_in() && ! is_user_member_of_blog() ) { |
||
1002 | add_user_to_blog( get_current_blog_id(), $customer_id, 'customer' ); |
||
1003 | } |
||
1004 | |||
1005 | // Add customer info from other fields. |
||
1006 | if ( $customer_id && apply_filters( 'woocommerce_checkout_update_customer_data', true, $this ) ) { |
||
1007 | $customer = new WC_Customer( $customer_id ); |
||
1008 | |||
1009 | if ( ! empty( $data['billing_first_name'] ) && '' === $customer->get_first_name() ) { |
||
1010 | $customer->set_first_name( $data['billing_first_name'] ); |
||
1011 | } |
||
1012 | |||
1013 | if ( ! empty( $data['billing_last_name'] ) && '' === $customer->get_last_name() ) { |
||
1014 | $customer->set_last_name( $data['billing_last_name'] ); |
||
1015 | } |
||
1016 | |||
1017 | // If the display name is an email, update to the user's full name. |
||
1018 | if ( is_email( $customer->get_display_name() ) ) { |
||
1019 | $customer->set_display_name( $customer->get_first_name() . ' ' . $customer->get_last_name() ); |
||
1020 | } |
||
1021 | |||
1022 | foreach ( $data as $key => $value ) { |
||
1023 | // Use setters where available. |
||
1024 | if ( is_callable( array( $customer, "set_{$key}" ) ) ) { |
||
1025 | $customer->{"set_{$key}"}( $value ); |
||
1026 | |||
1027 | // Store custom fields prefixed with wither shipping_ or billing_. |
||
1028 | } elseif ( 0 === stripos( $key, 'billing_' ) || 0 === stripos( $key, 'shipping_' ) ) { |
||
1029 | $customer->update_meta_data( $key, $value ); |
||
1030 | } |
||
1031 | } |
||
1032 | |||
1033 | /** |
||
1034 | * Action hook to adjust customer before save. |
||
1035 | * |
||
1036 | * @since 3.0.0 |
||
1037 | */ |
||
1038 | do_action( 'woocommerce_checkout_update_customer', $customer, $data ); |
||
1039 | |||
1040 | $customer->save(); |
||
1041 | } |
||
1042 | |||
1043 | do_action( 'woocommerce_checkout_update_user_meta', $customer_id, $data ); |
||
1044 | } |
||
1045 | |||
1046 | /** |
||
1047 | * If checkout failed during an AJAX call, send failure response. |
||
1048 | */ |
||
1049 | protected function send_ajax_failure_response() { |
||
1050 | if ( is_ajax() ) { |
||
1051 | // Only print notices if not reloading the checkout, otherwise they're lost in the page reload. |
||
1052 | if ( ! isset( WC()->session->reload_checkout ) ) { |
||
1053 | $messages = wc_print_notices( true ); |
||
1054 | } |
||
1055 | |||
1056 | $response = array( |
||
1057 | 'result' => 'failure', |
||
1058 | 'messages' => isset( $messages ) ? $messages : '', |
||
1059 | 'refresh' => isset( WC()->session->refresh_totals ), |
||
1060 | 'reload' => isset( WC()->session->reload_checkout ), |
||
1061 | ); |
||
1062 | |||
1063 | unset( WC()->session->refresh_totals, WC()->session->reload_checkout ); |
||
1064 | |||
1065 | wp_send_json( $response ); |
||
1066 | } |
||
1067 | } |
||
1068 | |||
1069 | /** |
||
1070 | * Process the checkout after the confirm order button is pressed. |
||
1071 | * |
||
1072 | * @throws Exception When validation fails. |
||
1073 | */ |
||
1074 | public function process_checkout() { |
||
1134 | |||
1135 | /** |
||
1136 | * Get a posted address field after sanitization and validation. |
||
1137 | * |
||
1138 | * @param string $key Field key. |
||
1139 | * @param string $type Type of address. Available options: 'billing' or 'shipping'. |
||
1140 | * @return string |
||
1141 | */ |
||
1142 | public function get_posted_address_data( $key, $type = 'billing' ) { |
||
1150 | |||
1151 | /** |
||
1152 | * Gets the value either from POST, or from the customer object. Sets the default values in checkout fields. |
||
1153 | * |
||
1154 | * @param string $input Name of the input we want to grab data for. e.g. billing_country. |
||
1155 | * @return string The default value. |
||
1156 | */ |
||
1157 | public function get_value( $input ) { |
||
1201 | } |
||
1202 |
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..