class-wc-checkout.php 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096
  1. <?php
  2. /**
  3. * Checkout functionality
  4. *
  5. * The WooCommerce checkout class handles the checkout process, collecting user data and processing the payment.
  6. *
  7. * @package WooCommerce/Classes
  8. * @version 3.4.0
  9. */
  10. defined( 'ABSPATH' ) || exit;
  11. /**
  12. * Checkout class.
  13. */
  14. class WC_Checkout {
  15. /**
  16. * The single instance of the class.
  17. *
  18. * @var WC_Checkout|null
  19. */
  20. protected static $instance = null;
  21. /**
  22. * Checkout fields are stored here.
  23. *
  24. * @var array|null
  25. */
  26. protected $fields = null;
  27. /**
  28. * Holds posted data for backwards compatibility.
  29. *
  30. * @var array
  31. */
  32. protected $legacy_posted_data = array();
  33. /**
  34. * Gets the main WC_Checkout Instance.
  35. *
  36. * @since 2.1
  37. * @static
  38. * @return WC_Checkout Main instance
  39. */
  40. public static function instance() {
  41. if ( is_null( self::$instance ) ) {
  42. self::$instance = new self();
  43. // Hook in actions once.
  44. add_action( 'woocommerce_checkout_billing', array( self::$instance, 'checkout_form_billing' ) );
  45. add_action( 'woocommerce_checkout_shipping', array( self::$instance, 'checkout_form_shipping' ) );
  46. // woocommerce_checkout_init action is ran once when the class is first constructed.
  47. do_action( 'woocommerce_checkout_init', self::$instance );
  48. }
  49. return self::$instance;
  50. }
  51. /**
  52. * See if variable is set. Used to support legacy public variables which are no longer defined.
  53. *
  54. * @param string $key Key.
  55. * @return bool
  56. */
  57. public function __isset( $key ) {
  58. return in_array(
  59. $key,
  60. array(
  61. 'enable_signup',
  62. 'enable_guest_checkout',
  63. 'must_create_account',
  64. 'checkout_fields',
  65. 'posted',
  66. 'shipping_method',
  67. 'payment_method',
  68. 'customer_id',
  69. 'shipping_methods',
  70. ),
  71. true
  72. );
  73. }
  74. /**
  75. * Sets the legacy public variables for backwards compatibility.
  76. *
  77. * @param string $key Key.
  78. * @param mixed $value Value.
  79. */
  80. public function __set( $key, $value ) {
  81. switch ( $key ) {
  82. case 'enable_signup':
  83. $bool_value = wc_string_to_bool( $value );
  84. if ( $bool_value !== $this->is_registration_enabled() ) {
  85. remove_filter( 'woocommerce_checkout_registration_enabled', '__return_true', 0 );
  86. remove_filter( 'woocommerce_checkout_registration_enabled', '__return_false', 0 );
  87. add_filter( 'woocommerce_checkout_registration_enabled', $bool_value ? '__return_true' : '__return_false', 0 );
  88. }
  89. break;
  90. case 'enable_guest_checkout':
  91. $bool_value = wc_string_to_bool( $value );
  92. if ( $bool_value === $this->is_registration_required() ) {
  93. remove_filter( 'woocommerce_checkout_registration_required', '__return_true', 0 );
  94. remove_filter( 'woocommerce_checkout_registration_required', '__return_false', 0 );
  95. add_filter( 'woocommerce_checkout_registration_required', $bool_value ? '__return_false' : '__return_true', 0 );
  96. }
  97. break;
  98. case 'checkout_fields':
  99. $this->fields = $value;
  100. break;
  101. case 'shipping_methods':
  102. WC()->session->set( 'chosen_shipping_methods', $value );
  103. break;
  104. case 'posted':
  105. $this->legacy_posted_data = $value;
  106. break;
  107. }
  108. }
  109. /**
  110. * Gets the legacy public variables for backwards compatibility.
  111. *
  112. * @param string $key Key.
  113. * @return array|string
  114. */
  115. public function __get( $key ) {
  116. if ( in_array( $key, array( 'posted', 'shipping_method', 'payment_method' ), true ) && empty( $this->legacy_posted_data ) ) {
  117. $this->legacy_posted_data = $this->get_posted_data();
  118. }
  119. switch ( $key ) {
  120. case 'enable_signup':
  121. return $this->is_registration_enabled();
  122. case 'enable_guest_checkout':
  123. return ! $this->is_registration_required();
  124. case 'must_create_account':
  125. return $this->is_registration_required() && ! is_user_logged_in();
  126. case 'checkout_fields':
  127. return $this->get_checkout_fields();
  128. case 'posted':
  129. wc_doing_it_wrong( 'WC_Checkout->posted', 'Use $_POST directly.', '3.0.0' );
  130. return $this->legacy_posted_data;
  131. case 'shipping_method':
  132. return $this->legacy_posted_data['shipping_method'];
  133. case 'payment_method':
  134. return $this->legacy_posted_data['payment_method'];
  135. case 'customer_id':
  136. return apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() );
  137. case 'shipping_methods':
  138. return (array) WC()->session->get( 'chosen_shipping_methods' );
  139. }
  140. }
  141. /**
  142. * Cloning is forbidden.
  143. */
  144. public function __clone() {
  145. wc_doing_it_wrong( __FUNCTION__, __( 'Cloning is forbidden.', 'woocommerce' ), '2.1' );
  146. }
  147. /**
  148. * Unserializing instances of this class is forbidden.
  149. */
  150. public function __wakeup() {
  151. wc_doing_it_wrong( __FUNCTION__, __( 'Unserializing instances of this class is forbidden.', 'woocommerce' ), '2.1' );
  152. }
  153. /**
  154. * Is registration required to checkout?
  155. *
  156. * @since 3.0.0
  157. * @return boolean
  158. */
  159. public function is_registration_required() {
  160. return apply_filters( 'woocommerce_checkout_registration_required', 'yes' !== get_option( 'woocommerce_enable_guest_checkout' ) );
  161. }
  162. /**
  163. * Is registration enabled on the checkout page?
  164. *
  165. * @since 3.0.0
  166. * @return boolean
  167. */
  168. public function is_registration_enabled() {
  169. return apply_filters( 'woocommerce_checkout_registration_enabled', 'yes' === get_option( 'woocommerce_enable_signup_and_login_from_checkout' ) );
  170. }
  171. /**
  172. * Get an array of checkout fields.
  173. *
  174. * @param string $fieldset to get.
  175. * @return array
  176. */
  177. public function get_checkout_fields( $fieldset = '' ) {
  178. if ( ! is_null( $this->fields ) ) {
  179. return $fieldset ? $this->fields[ $fieldset ] : $this->fields;
  180. }
  181. $this->fields = array(
  182. 'billing' => WC()->countries->get_address_fields(
  183. $this->get_value( 'billing_country' ),
  184. 'billing_'
  185. ),
  186. 'shipping' => WC()->countries->get_address_fields(
  187. $this->get_value( 'shipping_country' ),
  188. 'shipping_'
  189. ),
  190. 'account' => array(),
  191. 'order' => array(
  192. 'order_comments' => array(
  193. 'type' => 'textarea',
  194. 'class' => array( 'notes' ),
  195. 'label' => __( 'Order notes', 'woocommerce' ),
  196. 'placeholder' => esc_attr__(
  197. 'Notes about your order, e.g. special notes for delivery.',
  198. 'woocommerce'
  199. ),
  200. ),
  201. ),
  202. );
  203. if ( 'no' === get_option( 'woocommerce_registration_generate_username' ) ) {
  204. $this->fields['account']['account_username'] = array(
  205. 'type' => 'text',
  206. 'label' => __( 'Account username', 'woocommerce' ),
  207. 'required' => true,
  208. 'placeholder' => esc_attr__( 'Username', 'woocommerce' ),
  209. );
  210. }
  211. if ( 'no' === get_option( 'woocommerce_registration_generate_password' ) ) {
  212. $this->fields['account']['account_password'] = array(
  213. 'type' => 'password',
  214. 'label' => __( 'Create account password', 'woocommerce' ),
  215. 'required' => true,
  216. 'placeholder' => esc_attr__( 'Password', 'woocommerce' ),
  217. );
  218. }
  219. $this->fields = apply_filters( 'woocommerce_checkout_fields', $this->fields );
  220. return $fieldset ? $this->fields[ $fieldset ] : $this->fields;
  221. }
  222. /**
  223. * When we process the checkout, lets ensure cart items are rechecked to prevent checkout.
  224. */
  225. public function check_cart_items() {
  226. do_action( 'woocommerce_check_cart_items' );
  227. }
  228. /**
  229. * Output the billing form.
  230. */
  231. public function checkout_form_billing() {
  232. wc_get_template( 'checkout/form-billing.php', array( 'checkout' => $this ) );
  233. }
  234. /**
  235. * Output the shipping form.
  236. */
  237. public function checkout_form_shipping() {
  238. wc_get_template( 'checkout/form-shipping.php', array( 'checkout' => $this ) );
  239. }
  240. /**
  241. * Create an order. Error codes:
  242. * 520 - Cannot insert order into the database.
  243. * 521 - Cannot get order after creation.
  244. * 522 - Cannot update order.
  245. * 525 - Cannot create line item.
  246. * 526 - Cannot create fee item.
  247. * 527 - Cannot create shipping item.
  248. * 528 - Cannot create tax item.
  249. * 529 - Cannot create coupon item.
  250. *
  251. * @throws Exception When checkout validation fails.
  252. * @param array $data Posted data.
  253. * @return int|WP_ERROR
  254. */
  255. public function create_order( $data ) {
  256. // Give plugins the opportunity to create an order themselves.
  257. $order_id = apply_filters( 'woocommerce_create_order', null, $this );
  258. if ( $order_id ) {
  259. return $order_id;
  260. }
  261. try {
  262. $order_id = absint( WC()->session->get( 'order_awaiting_payment' ) );
  263. $cart_hash = md5( wp_json_encode( wc_clean( WC()->cart->get_cart_for_session() ) ) . WC()->cart->total );
  264. $available_gateways = WC()->payment_gateways->get_available_payment_gateways();
  265. $order = $order_id ? wc_get_order( $order_id ) : null;
  266. /**
  267. * If there is an order pending payment, we can resume it here so
  268. * long as it has not changed. If the order has changed, i.e.
  269. * different items or cost, create a new order. We use a hash to
  270. * detect changes which is based on cart items + order total.
  271. */
  272. if ( $order && $order->has_cart_hash( $cart_hash ) && $order->has_status( array( 'pending', 'failed' ) ) ) {
  273. // Action for 3rd parties.
  274. do_action( 'woocommerce_resume_order', $order_id );
  275. // Remove all items - we will re-add them later.
  276. $order->remove_order_items();
  277. } else {
  278. $order = new WC_Order();
  279. }
  280. foreach ( $data as $key => $value ) {
  281. if ( is_callable( array( $order, "set_{$key}" ) ) ) {
  282. $order->{"set_{$key}"}( $value );
  283. // Store custom fields prefixed with wither shipping_ or billing_. This is for backwards compatibility with 2.6.x.
  284. // TODO: Fix conditional to only include shipping/billing address fields in a smarter way without str(i)pos.
  285. } elseif ( ( 0 === stripos( $key, 'billing_' ) || 0 === stripos( $key, 'shipping_' ) )
  286. && ! in_array( $key, array( 'shipping_method', 'shipping_total', 'shipping_tax' ), true ) ) {
  287. $order->update_meta_data( '_' . $key, $value );
  288. }
  289. }
  290. $order->set_created_via( 'checkout' );
  291. $order->set_cart_hash( $cart_hash );
  292. $order->set_customer_id( apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() ) );
  293. $order->set_currency( get_woocommerce_currency() );
  294. $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
  295. $order->set_customer_ip_address( WC_Geolocation::get_ip_address() );
  296. $order->set_customer_user_agent( wc_get_user_agent() );
  297. $order->set_customer_note( isset( $data['order_comments'] ) ? $data['order_comments'] : '' );
  298. $order->set_payment_method( isset( $available_gateways[ $data['payment_method'] ] ) ? $available_gateways[ $data['payment_method'] ] : $data['payment_method'] );
  299. $order->set_shipping_total( WC()->cart->get_shipping_total() );
  300. $order->set_discount_total( WC()->cart->get_discount_total() );
  301. $order->set_discount_tax( WC()->cart->get_discount_tax() );
  302. $order->set_cart_tax( WC()->cart->get_cart_contents_tax() + WC()->cart->get_fee_tax() );
  303. $order->set_shipping_tax( WC()->cart->get_shipping_tax() );
  304. $order->set_total( WC()->cart->get_total( 'edit' ) );
  305. $this->create_order_line_items( $order, WC()->cart );
  306. $this->create_order_fee_lines( $order, WC()->cart );
  307. $this->create_order_shipping_lines( $order, WC()->session->get( 'chosen_shipping_methods' ), WC()->shipping->get_packages() );
  308. $this->create_order_tax_lines( $order, WC()->cart );
  309. $this->create_order_coupon_lines( $order, WC()->cart );
  310. /**
  311. * Action hook to adjust order before save.
  312. *
  313. * @since 3.0.0
  314. */
  315. do_action( 'woocommerce_checkout_create_order', $order, $data );
  316. // Save the order.
  317. $order_id = $order->save();
  318. do_action( 'woocommerce_checkout_update_order_meta', $order_id, $data );
  319. return $order_id;
  320. } catch ( Exception $e ) {
  321. return new WP_Error( 'checkout-error', $e->getMessage() );
  322. }
  323. }
  324. /**
  325. * Add line items to the order.
  326. *
  327. * @param WC_Order $order Order instance.
  328. * @param WC_Cart $cart Cart instance.
  329. */
  330. public function create_order_line_items( &$order, $cart ) {
  331. foreach ( $cart->get_cart() as $cart_item_key => $values ) {
  332. /**
  333. * Filter hook to get initial item object.
  334. *
  335. * @since 3.1.0
  336. */
  337. $item = apply_filters( 'woocommerce_checkout_create_order_line_item_object', new WC_Order_Item_Product(), $cart_item_key, $values, $order );
  338. $product = $values['data'];
  339. $item->legacy_values = $values; // @deprecated For legacy actions.
  340. $item->legacy_cart_item_key = $cart_item_key; // @deprecated For legacy actions.
  341. $item->set_props(
  342. array(
  343. 'quantity' => $values['quantity'],
  344. 'variation' => $values['variation'],
  345. 'subtotal' => $values['line_subtotal'],
  346. 'total' => $values['line_total'],
  347. 'subtotal_tax' => $values['line_subtotal_tax'],
  348. 'total_tax' => $values['line_tax'],
  349. 'taxes' => $values['line_tax_data'],
  350. )
  351. );
  352. if ( $product ) {
  353. $item->set_props(
  354. array(
  355. 'name' => $product->get_name(),
  356. 'tax_class' => $product->get_tax_class(),
  357. 'product_id' => $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id(),
  358. 'variation_id' => $product->is_type( 'variation' ) ? $product->get_id() : 0,
  359. )
  360. );
  361. }
  362. $item->set_backorder_meta();
  363. /**
  364. * Action hook to adjust item before save.
  365. *
  366. * @since 3.0.0
  367. */
  368. do_action( 'woocommerce_checkout_create_order_line_item', $item, $cart_item_key, $values, $order );
  369. // Add item to order and save.
  370. $order->add_item( $item );
  371. }
  372. }
  373. /**
  374. * Add fees to the order.
  375. *
  376. * @param WC_Order $order Order instance.
  377. * @param WC_Cart $cart Cart instance.
  378. */
  379. public function create_order_fee_lines( &$order, $cart ) {
  380. foreach ( $cart->get_fees() as $fee_key => $fee ) {
  381. $item = new WC_Order_Item_Fee();
  382. $item->legacy_fee = $fee; // @deprecated For legacy actions.
  383. $item->legacy_fee_key = $fee_key; // @deprecated For legacy actions.
  384. $item->set_props(
  385. array(
  386. 'name' => $fee->name,
  387. 'tax_class' => $fee->taxable ? $fee->tax_class : 0,
  388. 'amount' => $fee->amount,
  389. 'total' => $fee->total,
  390. 'total_tax' => $fee->tax,
  391. 'taxes' => array(
  392. 'total' => $fee->tax_data,
  393. ),
  394. )
  395. );
  396. /**
  397. * Action hook to adjust item before save.
  398. *
  399. * @since 3.0.0
  400. */
  401. do_action( 'woocommerce_checkout_create_order_fee_item', $item, $fee_key, $fee, $order );
  402. // Add item to order and save.
  403. $order->add_item( $item );
  404. }
  405. }
  406. /**
  407. * Add shipping lines to the order.
  408. *
  409. * @param WC_Order $order Order Instance.
  410. * @param array $chosen_shipping_methods Chosen shipping methods.
  411. * @param array $packages Packages.
  412. */
  413. public function create_order_shipping_lines( &$order, $chosen_shipping_methods, $packages ) {
  414. foreach ( $packages as $package_key => $package ) {
  415. if ( isset( $chosen_shipping_methods[ $package_key ], $package['rates'][ $chosen_shipping_methods[ $package_key ] ] ) ) {
  416. $shipping_rate = $package['rates'][ $chosen_shipping_methods[ $package_key ] ];
  417. $item = new WC_Order_Item_Shipping();
  418. $item->legacy_package_key = $package_key; // @deprecated For legacy actions.
  419. $item->set_props(
  420. array(
  421. 'method_title' => $shipping_rate->label,
  422. 'method_id' => $shipping_rate->method_id,
  423. 'instance_id' => $shipping_rate->instance_id,
  424. 'total' => wc_format_decimal( $shipping_rate->cost ),
  425. 'taxes' => array(
  426. 'total' => $shipping_rate->taxes,
  427. ),
  428. )
  429. );
  430. foreach ( $shipping_rate->get_meta_data() as $key => $value ) {
  431. $item->add_meta_data( $key, $value, true );
  432. }
  433. /**
  434. * Action hook to adjust item before save.
  435. *
  436. * @since 3.0.0
  437. */
  438. do_action( 'woocommerce_checkout_create_order_shipping_item', $item, $package_key, $package, $order );
  439. // Add item to order and save.
  440. $order->add_item( $item );
  441. }
  442. }
  443. }
  444. /**
  445. * Add tax lines to the order.
  446. *
  447. * @param WC_Order $order Order instance.
  448. * @param WC_Cart $cart Cart instance.
  449. */
  450. public function create_order_tax_lines( &$order, $cart ) {
  451. foreach ( array_keys( $cart->get_cart_contents_taxes() + $cart->get_shipping_taxes() + $cart->get_fee_taxes() ) as $tax_rate_id ) {
  452. if ( $tax_rate_id && apply_filters( 'woocommerce_cart_remove_taxes_zero_rate_id', 'zero-rated' ) !== $tax_rate_id ) {
  453. $item = new WC_Order_Item_Tax();
  454. $item->set_props(
  455. array(
  456. 'rate_id' => $tax_rate_id,
  457. 'tax_total' => $cart->get_tax_amount( $tax_rate_id ),
  458. 'shipping_tax_total' => $cart->get_shipping_tax_amount( $tax_rate_id ),
  459. 'rate_code' => WC_Tax::get_rate_code( $tax_rate_id ),
  460. 'label' => WC_Tax::get_rate_label( $tax_rate_id ),
  461. 'compound' => WC_Tax::is_compound( $tax_rate_id ),
  462. )
  463. );
  464. /**
  465. * Action hook to adjust item before save.
  466. *
  467. * @since 3.0.0
  468. */
  469. do_action( 'woocommerce_checkout_create_order_tax_item', $item, $tax_rate_id, $order );
  470. // Add item to order and save.
  471. $order->add_item( $item );
  472. }
  473. }
  474. }
  475. /**
  476. * Add coupon lines to the order.
  477. *
  478. * @param WC_Order $order Order instance.
  479. * @param WC_Cart $cart Cart instance.
  480. */
  481. public function create_order_coupon_lines( &$order, $cart ) {
  482. foreach ( $cart->get_coupons() as $code => $coupon ) {
  483. $item = new WC_Order_Item_Coupon();
  484. $item->set_props(
  485. array(
  486. 'code' => $code,
  487. 'discount' => $cart->get_coupon_discount_amount( $code ),
  488. 'discount_tax' => $cart->get_coupon_discount_tax_amount( $code ),
  489. )
  490. );
  491. // Avoid storing used_by - it's not needed and can get large.
  492. $coupon_data = $coupon->get_data();
  493. unset( $coupon_data['used_by'] );
  494. $item->add_meta_data( 'coupon_data', $coupon_data );
  495. /**
  496. * Action hook to adjust item before save.
  497. *
  498. * @since 3.0.0
  499. */
  500. do_action( 'woocommerce_checkout_create_order_coupon_item', $item, $code, $coupon, $order );
  501. // Add item to order and save.
  502. $order->add_item( $item );
  503. }
  504. }
  505. /**
  506. * See if a fieldset should be skipped.
  507. *
  508. * @since 3.0.0
  509. * @param string $fieldset_key Fieldset key.
  510. * @param array $data Posted data.
  511. * @return bool
  512. */
  513. protected function maybe_skip_fieldset( $fieldset_key, $data ) {
  514. if ( 'shipping' === $fieldset_key && ( ! $data['ship_to_different_address'] || ! WC()->cart->needs_shipping_address() ) ) {
  515. return true;
  516. }
  517. if ( 'account' === $fieldset_key && ( is_user_logged_in() || ( ! $this->is_registration_required() && empty( $data['createaccount'] ) ) ) ) {
  518. return true;
  519. }
  520. return false;
  521. }
  522. /**
  523. * Get posted data from the checkout form.
  524. *
  525. * @since 3.1.0
  526. * @return array of data.
  527. */
  528. public function get_posted_data() {
  529. $skipped = array();
  530. $data = array(
  531. 'terms' => (int) isset( $_POST['terms'] ), // WPCS: input var ok, CSRF ok.
  532. 'createaccount' => (int) ! empty( $_POST['createaccount'] ), // WPCS: input var ok, CSRF ok.
  533. 'payment_method' => isset( $_POST['payment_method'] ) ? wc_clean( wp_unslash( $_POST['payment_method'] ) ) : '', // WPCS: input var ok, CSRF ok.
  534. 'shipping_method' => isset( $_POST['shipping_method'] ) ? wc_clean( wp_unslash( $_POST['shipping_method'] ) ) : '', // WPCS: input var ok, CSRF ok.
  535. 'ship_to_different_address' => ! empty( $_POST['ship_to_different_address'] ) && ! wc_ship_to_billing_address_only(), // WPCS: input var ok, CSRF ok.
  536. 'woocommerce_checkout_update_totals' => isset( $_POST['woocommerce_checkout_update_totals'] ), // WPCS: input var ok, CSRF ok.
  537. );
  538. foreach ( $this->get_checkout_fields() as $fieldset_key => $fieldset ) {
  539. if ( $this->maybe_skip_fieldset( $fieldset_key, $data ) ) {
  540. $skipped[] = $fieldset_key;
  541. continue;
  542. }
  543. foreach ( $fieldset as $key => $field ) {
  544. $type = sanitize_title( isset( $field['type'] ) ? $field['type'] : 'text' );
  545. switch ( $type ) {
  546. case 'checkbox':
  547. $value = isset( $_POST[ $key ] ) ? 1 : ''; // WPCS: input var ok, CSRF ok.
  548. break;
  549. case 'multiselect':
  550. $value = isset( $_POST[ $key ] ) ? implode( ', ', wc_clean( wp_unslash( $_POST[ $key ] ) ) ) : ''; // WPCS: input var ok, CSRF ok.
  551. break;
  552. case 'textarea':
  553. $value = isset( $_POST[ $key ] ) ? wc_sanitize_textarea( wp_unslash( $_POST[ $key ] ) ) : ''; // WPCS: input var ok, CSRF ok.
  554. break;
  555. default:
  556. $value = isset( $_POST[ $key ] ) ? wc_clean( wp_unslash( $_POST[ $key ] ) ) : ''; // WPCS: input var ok, CSRF ok.
  557. break;
  558. }
  559. $data[ $key ] = apply_filters( 'woocommerce_process_checkout_' . $type . '_field', apply_filters( 'woocommerce_process_checkout_field_' . $key, $value ) );
  560. }
  561. }
  562. if ( in_array( 'shipping', $skipped, true ) && ( WC()->cart->needs_shipping_address() || wc_ship_to_billing_address_only() ) ) {
  563. foreach ( $this->get_checkout_fields( 'shipping' ) as $key => $field ) {
  564. $data[ $key ] = isset( $data[ 'billing_' . substr( $key, 9 ) ] ) ? $data[ 'billing_' . substr( $key, 9 ) ] : '';
  565. }
  566. }
  567. // BW compatibility.
  568. $this->legacy_posted_data = $data;
  569. return apply_filters( 'woocommerce_checkout_posted_data', $data );
  570. }
  571. /**
  572. * Validates the posted checkout data based on field properties.
  573. *
  574. * @since 3.0.0
  575. * @param array $data An array of posted data.
  576. * @param WP_Error $errors Validation error.
  577. */
  578. protected function validate_posted_data( &$data, &$errors ) {
  579. foreach ( $this->get_checkout_fields() as $fieldset_key => $fieldset ) {
  580. if ( $this->maybe_skip_fieldset( $fieldset_key, $data ) ) {
  581. continue;
  582. }
  583. foreach ( $fieldset as $key => $field ) {
  584. if ( ! isset( $data[ $key ] ) ) {
  585. continue;
  586. }
  587. $required = ! empty( $field['required'] );
  588. $format = array_filter( isset( $field['validate'] ) ? (array) $field['validate'] : array() );
  589. $field_label = isset( $field['label'] ) ? $field['label'] : '';
  590. switch ( $fieldset_key ) {
  591. case 'shipping':
  592. /* translators: %s: field name */
  593. $field_label = sprintf( __( 'Shipping %s', 'woocommerce' ), $field_label );
  594. break;
  595. case 'billing':
  596. /* translators: %s: field name */
  597. $field_label = sprintf( __( 'Billing %s', 'woocommerce' ), $field_label );
  598. break;
  599. }
  600. if ( in_array( 'postcode', $format, true ) ) {
  601. $country = isset( $data[ $fieldset_key . '_country' ] ) ? $data[ $fieldset_key . '_country' ] : WC()->customer->{"get_{$fieldset_key}_country"}();
  602. $data[ $key ] = wc_format_postcode( $data[ $key ], $country );
  603. if ( '' !== $data[ $key ] && ! WC_Validation::is_postcode( $data[ $key ], $country ) ) {
  604. /* translators: %s: field name */
  605. $errors->add( 'validation', sprintf( __( '%s is not a valid postcode / ZIP.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' ) );
  606. }
  607. }
  608. if ( in_array( 'phone', $format, true ) ) {
  609. $data[ $key ] = wc_format_phone_number( $data[ $key ] );
  610. if ( '' !== $data[ $key ] && ! WC_Validation::is_phone( $data[ $key ] ) ) {
  611. /* translators: %s: phone number */
  612. $errors->add( 'validation', sprintf( __( '%s is not a valid phone number.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' ) );
  613. }
  614. }
  615. if ( in_array( 'email', $format, true ) && '' !== $data[ $key ] ) {
  616. $data[ $key ] = sanitize_email( $data[ $key ] );
  617. if ( ! is_email( $data[ $key ] ) ) {
  618. /* translators: %s: email address */
  619. $errors->add( 'validation', sprintf( __( '%s is not a valid email address.', 'woocommerce' ), '<strong>' . esc_html( $field_label ) . '</strong>' ) );
  620. continue;
  621. }
  622. }
  623. if ( '' !== $data[ $key ] && in_array( 'state', $format, true ) ) {
  624. $country = isset( $data[ $fieldset_key . '_country' ] ) ? $data[ $fieldset_key . '_country' ] : WC()->customer->{"get_{$fieldset_key}_country"}();
  625. $valid_states = WC()->countries->get_states( $country );
  626. if ( ! empty( $valid_states ) && is_array( $valid_states ) && count( $valid_states ) > 0 ) {
  627. $valid_state_values = array_map( 'wc_strtoupper', array_flip( array_map( 'wc_strtoupper', $valid_states ) ) );
  628. $data[ $key ] = wc_strtoupper( $data[ $key ] );
  629. if ( isset( $valid_state_values[ $data[ $key ] ] ) ) {
  630. // With this part we consider state value to be valid as well, convert it to the state key for the valid_states check below.
  631. $data[ $key ] = $valid_state_values[ $data[ $key ] ];
  632. }
  633. if ( ! in_array( $data[ $key ], $valid_state_values, true ) ) {
  634. /* translators: 1: state field 2: valid states */
  635. $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 ) ) );
  636. }
  637. }
  638. }
  639. if ( $required && '' === $data[ $key ] ) {
  640. /* translators: %s: field name */
  641. $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 ) );
  642. }
  643. }
  644. }
  645. }
  646. /**
  647. * Validates that the checkout has enough info to proceed.
  648. *
  649. * @since 3.0.0
  650. * @param array $data An array of posted data.
  651. * @param WP_Error $errors Validation errors.
  652. */
  653. protected function validate_checkout( &$data, &$errors ) {
  654. $this->validate_posted_data( $data, $errors );
  655. $this->check_cart_items();
  656. if ( empty( $data['woocommerce_checkout_update_totals'] ) && empty( $data['terms'] ) && ! empty( $_POST['terms-field'] ) ) { // WPCS: input var ok, CSRF ok.
  657. $errors->add( 'terms', __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' ) );
  658. }
  659. if ( WC()->cart->needs_shipping() ) {
  660. $shipping_country = WC()->customer->get_shipping_country();
  661. if ( empty( $shipping_country ) ) {
  662. $errors->add( 'shipping', __( 'Please enter an address to continue.', 'woocommerce' ) );
  663. } elseif ( ! in_array( WC()->customer->get_shipping_country(), array_keys( WC()->countries->get_shipping_countries() ), true ) ) {
  664. /* translators: %s: shipping location */
  665. $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() ) );
  666. } else {
  667. $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
  668. foreach ( WC()->shipping->get_packages() as $i => $package ) {
  669. if ( ! isset( $chosen_shipping_methods[ $i ], $package['rates'][ $chosen_shipping_methods[ $i ] ] ) ) {
  670. $errors->add( 'shipping', __( 'No shipping method has been selected. Please double check your address, or contact us if you need any help.', 'woocommerce' ) );
  671. }
  672. }
  673. }
  674. }
  675. if ( WC()->cart->needs_payment() ) {
  676. $available_gateways = WC()->payment_gateways->get_available_payment_gateways();
  677. if ( ! isset( $available_gateways[ $data['payment_method'] ] ) ) {
  678. $errors->add( 'payment', __( 'Invalid payment method.', 'woocommerce' ) );
  679. } else {
  680. $available_gateways[ $data['payment_method'] ]->validate_fields();
  681. }
  682. }
  683. do_action( 'woocommerce_after_checkout_validation', $data, $errors );
  684. }
  685. /**
  686. * Set address field for customer.
  687. *
  688. * @since 3.0.7
  689. * @param string $field String to update.
  690. * @param string $key Field key.
  691. * @param array $data Array of data to get the value from.
  692. */
  693. protected function set_customer_address_fields( $field, $key, $data ) {
  694. if ( isset( $data[ "billing_{$field}" ] ) ) {
  695. WC()->customer->{"set_billing_{$field}"}( $data[ "billing_{$field}" ] );
  696. WC()->customer->{"set_shipping_{$field}"}( $data[ "billing_{$field}" ] );
  697. }
  698. if ( isset( $data[ "shipping_{$field}" ] ) ) {
  699. WC()->customer->{"set_shipping_{$field}"}( $data[ "shipping_{$field}" ] );
  700. }
  701. }
  702. /**
  703. * Update customer and session data from the posted checkout data.
  704. *
  705. * @since 3.0.0
  706. * @param array $data Posted data.
  707. */
  708. protected function update_session( $data ) {
  709. // Update both shipping and billing to the passed billing address first if set.
  710. $address_fields = array(
  711. 'address_1',
  712. 'address_2',
  713. 'city',
  714. 'postcode',
  715. 'state',
  716. 'country',
  717. );
  718. array_walk( $address_fields, array( $this, 'set_customer_address_fields' ), $data );
  719. WC()->customer->save();
  720. // Update customer shipping and payment method to posted method.
  721. $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods' );
  722. if ( is_array( $data['shipping_method'] ) ) {
  723. foreach ( $data['shipping_method'] as $i => $value ) {
  724. $chosen_shipping_methods[ $i ] = $value;
  725. }
  726. }
  727. WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );
  728. WC()->session->set( 'chosen_payment_method', $data['payment_method'] );
  729. // Update cart totals now we have customer address.
  730. WC()->cart->calculate_totals();
  731. }
  732. /**
  733. * Process an order that does require payment.
  734. *
  735. * @since 3.0.0
  736. * @param int $order_id Order ID.
  737. * @param string $payment_method Payment method.
  738. */
  739. protected function process_order_payment( $order_id, $payment_method ) {
  740. $available_gateways = WC()->payment_gateways->get_available_payment_gateways();
  741. if ( ! isset( $available_gateways[ $payment_method ] ) ) {
  742. return;
  743. }
  744. // Store Order ID in session so it can be re-used after payment failure.
  745. WC()->session->set( 'order_awaiting_payment', $order_id );
  746. // Process Payment.
  747. $result = $available_gateways[ $payment_method ]->process_payment( $order_id );
  748. // Redirect to success/confirmation/payment page.
  749. if ( isset( $result['result'] ) && 'success' === $result['result'] ) {
  750. $result = apply_filters( 'woocommerce_payment_successful_result', $result, $order_id );
  751. if ( ! is_ajax() ) {
  752. wp_redirect( $result['redirect'] );
  753. exit;
  754. }
  755. wp_send_json( $result );
  756. }
  757. }
  758. /**
  759. * Process an order that doesn't require payment.
  760. *
  761. * @since 3.0.0
  762. * @param int $order_id Order ID.
  763. */
  764. protected function process_order_without_payment( $order_id ) {
  765. $order = wc_get_order( $order_id );
  766. $order->payment_complete();
  767. wc_empty_cart();
  768. if ( ! is_ajax() ) {
  769. wp_safe_redirect(
  770. apply_filters( 'woocommerce_checkout_no_payment_needed_redirect', $order->get_checkout_order_received_url(), $order )
  771. );
  772. exit;
  773. }
  774. wp_send_json(
  775. array(
  776. 'result' => 'success',
  777. 'redirect' => apply_filters( 'woocommerce_checkout_no_payment_needed_redirect', $order->get_checkout_order_received_url(), $order ),
  778. )
  779. );
  780. }
  781. /**
  782. * Create a new customer account if needed.
  783. *
  784. * @throws Exception When not able to create customer.
  785. * @param array $data Posted data.
  786. */
  787. protected function process_customer( $data ) {
  788. $customer_id = apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() );
  789. if ( ! is_user_logged_in() && ( $this->is_registration_required() || ! empty( $data['createaccount'] ) ) ) {
  790. $username = ! empty( $data['account_username'] ) ? $data['account_username'] : '';
  791. $password = ! empty( $data['account_password'] ) ? $data['account_password'] : '';
  792. $customer_id = wc_create_new_customer( $data['billing_email'], $username, $password );
  793. if ( is_wp_error( $customer_id ) ) {
  794. throw new Exception( $customer_id->get_error_message() );
  795. }
  796. wp_set_current_user( $customer_id );
  797. wc_set_customer_auth_cookie( $customer_id );
  798. // As we are now logged in, checkout will need to refresh to show logged in data.
  799. WC()->session->set( 'reload_checkout', true );
  800. // Also, recalculate cart totals to reveal any role-based discounts that were unavailable before registering.
  801. WC()->cart->calculate_totals();
  802. }
  803. // On multisite, ensure user exists on current site, if not add them before allowing login.
  804. if ( $customer_id && is_multisite() && is_user_logged_in() && ! is_user_member_of_blog() ) {
  805. add_user_to_blog( get_current_blog_id(), $customer_id, 'customer' );
  806. }
  807. // Add customer info from other fields.
  808. if ( $customer_id && apply_filters( 'woocommerce_checkout_update_customer_data', true, $this ) ) {
  809. $customer = new WC_Customer( $customer_id );
  810. if ( ! empty( $data['billing_first_name'] ) ) {
  811. $customer->set_first_name( $data['billing_first_name'] );
  812. }
  813. if ( ! empty( $data['billing_last_name'] ) ) {
  814. $customer->set_last_name( $data['billing_last_name'] );
  815. }
  816. // If the display name is an email, update to the user's full name.
  817. if ( is_email( $customer->get_display_name() ) ) {
  818. $customer->set_display_name( $data['billing_first_name'] . ' ' . $data['billing_last_name'] );
  819. }
  820. foreach ( $data as $key => $value ) {
  821. // Use setters where available.
  822. if ( is_callable( array( $customer, "set_{$key}" ) ) ) {
  823. $customer->{"set_{$key}"}( $value );
  824. // Store custom fields prefixed with wither shipping_ or billing_.
  825. } elseif ( 0 === stripos( $key, 'billing_' ) || 0 === stripos( $key, 'shipping_' ) ) {
  826. $customer->update_meta_data( $key, $value );
  827. }
  828. }
  829. /**
  830. * Action hook to adjust customer before save.
  831. *
  832. * @since 3.0.0
  833. */
  834. do_action( 'woocommerce_checkout_update_customer', $customer, $data );
  835. $customer->save();
  836. }
  837. do_action( 'woocommerce_checkout_update_user_meta', $customer_id, $data );
  838. }
  839. /**
  840. * If checkout failed during an AJAX call, send failure response.
  841. */
  842. protected function send_ajax_failure_response() {
  843. if ( is_ajax() ) {
  844. // Only print notices if not reloading the checkout, otherwise they're lost in the page reload.
  845. if ( ! isset( WC()->session->reload_checkout ) ) {
  846. ob_start();
  847. wc_print_notices();
  848. $messages = ob_get_clean();
  849. }
  850. $response = array(
  851. 'result' => 'failure',
  852. 'messages' => isset( $messages ) ? $messages : '',
  853. 'refresh' => isset( WC()->session->refresh_totals ),
  854. 'reload' => isset( WC()->session->reload_checkout ),
  855. );
  856. unset( WC()->session->refresh_totals, WC()->session->reload_checkout );
  857. wp_send_json( $response );
  858. }
  859. }
  860. /**
  861. * Process the checkout after the confirm order button is pressed.
  862. *
  863. * @throws Exception When validation fails.
  864. */
  865. public function process_checkout() {
  866. try {
  867. $nonce_value = wc_get_var( $_REQUEST['woocommerce-process-checkout-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine.
  868. if ( empty( $nonce_value ) || ! wp_verify_nonce( $nonce_value, 'woocommerce-process_checkout' ) ) {
  869. WC()->session->set( 'refresh_totals', true );
  870. throw new Exception( __( 'We were unable to process your order, please try again.', 'woocommerce' ) );
  871. }
  872. wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true );
  873. wc_set_time_limit( 0 );
  874. do_action( 'woocommerce_before_checkout_process' );
  875. if ( WC()->cart->is_empty() ) {
  876. /* translators: %s: shop cart url */
  877. 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' ) ) ) );
  878. }
  879. do_action( 'woocommerce_checkout_process' );
  880. $errors = new WP_Error();
  881. $posted_data = $this->get_posted_data();
  882. // Update session for customer and totals.
  883. $this->update_session( $posted_data );
  884. // Validate posted data and cart items before proceeding.
  885. $this->validate_checkout( $posted_data, $errors );
  886. foreach ( $errors->get_error_messages() as $message ) {
  887. wc_add_notice( $message, 'error' );
  888. }
  889. if ( empty( $posted_data['woocommerce_checkout_update_totals'] ) && 0 === wc_notice_count( 'error' ) ) {
  890. $this->process_customer( $posted_data );
  891. $order_id = $this->create_order( $posted_data );
  892. $order = wc_get_order( $order_id );
  893. if ( is_wp_error( $order_id ) ) {
  894. throw new Exception( $order_id->get_error_message() );
  895. }
  896. if ( ! $order ) {
  897. throw new Exception( __( 'Unable to create order.', 'woocommerce' ) );
  898. }
  899. do_action( 'woocommerce_checkout_order_processed', $order_id, $posted_data, $order );
  900. if ( WC()->cart->needs_payment() ) {
  901. $this->process_order_payment( $order_id, $posted_data['payment_method'] );
  902. } else {
  903. $this->process_order_without_payment( $order_id );
  904. }
  905. }
  906. } catch ( Exception $e ) {
  907. wc_add_notice( $e->getMessage(), 'error' );
  908. }
  909. $this->send_ajax_failure_response();
  910. }
  911. /**
  912. * Get a posted address field after sanitization and validation.
  913. *
  914. * @param string $key Field key.
  915. * @param string $type Type of address. Available options: 'billing' or 'shipping'.
  916. * @return string
  917. */
  918. public function get_posted_address_data( $key, $type = 'billing' ) {
  919. if ( 'billing' === $type || false === $this->legacy_posted_data['ship_to_different_address'] ) {
  920. $return = isset( $this->legacy_posted_data[ 'billing_' . $key ] ) ? $this->legacy_posted_data[ 'billing_' . $key ] : '';
  921. } else {
  922. $return = isset( $this->legacy_posted_data[ 'shipping_' . $key ] ) ? $this->legacy_posted_data[ 'shipping_' . $key ] : '';
  923. }
  924. return $return;
  925. }
  926. /**
  927. * Gets the value either from the posted data, or from the users meta data.
  928. *
  929. * @param string $input Input key.
  930. * @return string
  931. */
  932. public function get_value( $input ) {
  933. if ( ! empty( $_POST[ $input ] ) ) { // WPCS: input var ok, CSRF OK.
  934. return wc_clean( wp_unslash( $_POST[ $input ] ) ); // WPCS: input var ok, CSRF OK.
  935. }
  936. $value = apply_filters( 'woocommerce_checkout_get_value', null, $input );
  937. if ( null !== $value ) {
  938. return $value;
  939. }
  940. if ( is_callable( array( WC()->customer, "get_$input" ) ) ) {
  941. $value = WC()->customer->{"get_$input"}() ? WC()->customer->{"get_$input"}() : null;
  942. } elseif ( WC()->customer->meta_exists( $input ) ) {
  943. $value = WC()->customer->get_meta( $input, true );
  944. }
  945. return apply_filters( 'default_checkout_' . $input, $value, $input );
  946. }
  947. }