Consolidate Store API and Core account creation handling (#50913)

* Consolidate Store API and Core account creation handling

* Changelog

* Add hook doc

* Update tests to not use global settings

* Ignore lints for other hooks in file
This commit is contained in:
Mike Jolley 2024-09-03 13:42:51 +01:00 committed by GitHub
parent bdf8f2b1a4
commit a7f29ce98e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 167 additions and 257 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Consolidate Store API and Core account creation functions.

View File

@ -1132,7 +1132,17 @@ class WC_Checkout {
);
if ( is_wp_error( $customer_id ) ) {
throw new Exception( $customer_id->get_error_message() );
if ( 'registration-error-email-exists' === $customer_id->get_error_code() ) {
/**
* Filter the notice shown when a customer tries to register with an existing email address.
*
* @since 3.3.0
* @param string $message The notice.
* @param string $email The email address.
*/
throw new Exception( apply_filters( 'woocommerce_registration_error_email_exists', __( 'An account is already registered with your email address. <a href="#" class="showlogin">Please log in.</a>', 'woocommerce' ), $data['billing_email'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
throw new Exception( $customer_id->get_error_message() ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
wc_set_customer_auth_cookie( $customer_id );

View File

@ -43,6 +43,9 @@ if ( ! function_exists( 'wc_create_new_customer' ) ) {
/**
* Create a new customer.
*
* @since 9.4.0 Moved woocommerce_registration_error_email_exists filter to the shortcode checkout class.
* @since 9.4.0 Removed handling for generating username/password based on settings--this is consumed at form level. Here, if data is missing it will be generated.
*
* @param string $email Customer email.
* @param string $username Customer username.
* @param string $password Customer password.
@ -55,17 +58,24 @@ if ( ! function_exists( 'wc_create_new_customer' ) ) {
}
if ( email_exists( $email ) ) {
return new WP_Error( 'registration-error-email-exists', apply_filters( 'woocommerce_registration_error_email_exists', __( 'An account is already registered with your email address. <a href="#" class="showlogin">Please log in.</a>', 'woocommerce' ), $email ) );
return new WP_Error(
'registration-error-email-exists',
sprintf(
// Translators: %s Email address.
esc_html__( 'An account is already registered with %s. Please log in or use a different email address.', 'woocommerce' ),
esc_html( $email )
)
);
}
if ( 'yes' === get_option( 'woocommerce_registration_generate_username', 'yes' ) && empty( $username ) ) {
if ( empty( $username ) ) {
$username = wc_create_new_customer_username( $email, $args );
}
$username = sanitize_user( $username );
if ( empty( $username ) || ! validate_username( $username ) ) {
return new WP_Error( 'registration-error-invalid-username', __( 'Please enter a valid account username.', 'woocommerce' ) );
return new WP_Error( 'registration-error-invalid-username', __( 'Please provide a valid account username.', 'woocommerce' ) );
}
if ( username_exists( $username ) ) {
@ -74,35 +84,88 @@ if ( ! function_exists( 'wc_create_new_customer' ) ) {
// Handle password creation.
$password_generated = false;
if ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) && empty( $password ) ) {
if ( empty( $password ) ) {
$password = wp_generate_password();
$password_generated = true;
}
if ( empty( $password ) ) {
return new WP_Error( 'registration-error-missing-password', __( 'Please enter an account password.', 'woocommerce' ) );
return new WP_Error( 'registration-error-missing-password', __( 'Please create a password for your account.', 'woocommerce' ) );
}
// Use WP_Error to handle registration errors.
$errors = new WP_Error();
/**
* Fires before a customer account is registered.
*
* This hook fires before customer accounts are created and passes the form data (username, email) and an array
* of errors.
*
* This could be used to add extra validation logic and append errors to the array.
*
* @since 7.2.0
*
* @internal Matches filter name in WooCommerce core.
*
* @param string $username Customer username.
* @param string $user_email Customer email address.
* @param \WP_Error $errors Error object.
*/
do_action( 'woocommerce_register_post', $username, $email, $errors );
/**
* Filters registration errors before a customer account is registered.
*
* This hook filters registration errors. This can be used to manipulate the array of errors before
* they are displayed.
*
* @since 7.2.0
*
* @internal Matches filter name in WooCommerce core.
*
* @param \WP_Error $errors Error object.
* @param string $username Customer username.
* @param string $user_email Customer email address.
* @return \WP_Error
*/
$errors = apply_filters( 'woocommerce_registration_errors', $errors, $username, $email );
if ( $errors->get_error_code() ) {
if ( is_wp_error( $errors ) && $errors->get_error_code() ) {
return $errors;
}
// Merged passed args with sanitized username, email, and password.
$customer_data = array_merge(
$args,
array(
'user_login' => $username,
'user_pass' => $password,
'user_email' => $email,
'role' => 'customer',
)
);
/**
* Filters customer data before a customer account is registered.
*
* This hook filters customer data. It allows user data to be changed, for example, username, password, email,
* first name, last name, and role.
*
* @since 7.2.0
*
* @param array $customer_data An array of customer (user) data.
* @return array
*/
$new_customer_data = apply_filters(
'woocommerce_new_customer_data',
array_merge(
$args,
wp_parse_args(
$customer_data,
array(
'user_login' => $username,
'user_pass' => $password,
'user_email' => $email,
'role' => 'customer',
'first_name' => '',
'last_name' => '',
'source' => 'unknown',
)
)
);
@ -113,6 +176,24 @@ if ( ! function_exists( 'wc_create_new_customer' ) ) {
return $customer_id;
}
// Set account flag to remind customer to update generated password.
if ( $password_generated ) {
update_user_option( $customer_id, 'default_password_nag', true, true );
}
/**
* Fires after a customer account has been registered.
*
* This hook fires after customer accounts are created and passes the customer data.
*
* @since 7.2.0
*
* @internal Matches filter name in WooCommerce core.
*
* @param integer $customer_id New customer (user) ID.
* @param array $new_customer_data Array of customer (user) data.
* @param string $password_generated The generated password for the account.
*/
do_action( 'woocommerce_created_customer', $customer_id, $new_customer_data, $password_generated );
return $customer_id;
@ -232,7 +313,9 @@ function wc_set_customer_auth_cookie( $customer_id ) {
wp_set_auth_cookie( $customer_id, true );
// Update session.
WC()->session->init_session_cookie();
if ( is_callable( array( WC()->session, 'init_session_cookie' ) ) ) {
WC()->session->init_session_cookie();
}
}
/**
@ -272,10 +355,10 @@ function wc_update_new_customer_past_orders( $customer_id ) {
do_action( 'woocommerce_update_new_customer_past_order', $order_id, $customer );
if ( $order->get_status() === 'wc-completed' ) {
$complete++;
++$complete;
}
$linked++;
++$linked;
}
}
@ -356,18 +439,18 @@ function wc_customer_bought_product( $customer_email, $user_id, $product_id ) {
}
if ( OrderUtil::custom_orders_table_usage_is_enabled() ) {
$statuses = array_map(
$statuses = array_map(
function ( $status ) {
return "wc-$status";
},
$statuses
);
$order_table = OrdersTableDataStore::get_orders_table_name();
$order_table = OrdersTableDataStore::get_orders_table_name();
$user_id_clause = '';
if ( $user_id ) {
$user_id_clause = 'OR o.customer_id = ' . absint( $user_id );
}
$sql = "
$sql = "
SELECT DISTINCT im.meta_value FROM $order_table AS o
INNER JOIN {$wpdb->prefix}woocommerce_order_items AS i ON o.id = i.order_id
INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS im ON i.order_item_id = im.order_item_id
@ -568,17 +651,15 @@ function wc_modify_map_meta_cap( $caps, $cap, $user_id, $args ) {
case 'delete_user':
if ( ! isset( $args[0] ) || $args[0] === $user_id ) {
break;
} else {
if ( ! wc_current_user_has_role( 'administrator' ) ) {
if ( wc_user_has_role( $args[0], 'administrator' ) ) {
} elseif ( ! wc_current_user_has_role( 'administrator' ) ) {
if ( wc_user_has_role( $args[0], 'administrator' ) ) {
$caps[] = 'do_not_allow';
} elseif ( wc_current_user_has_role( 'shop_manager' ) ) {
// Shop managers can only edit customer info.
$userdata = get_userdata( $args[0] );
$shop_manager_editable_roles = apply_filters( 'woocommerce_shop_manager_editable_roles', array( 'customer' ) ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
if ( property_exists( $userdata, 'roles' ) && ! empty( $userdata->roles ) && ! array_intersect( $userdata->roles, $shop_manager_editable_roles ) ) {
$caps[] = 'do_not_allow';
} elseif ( wc_current_user_has_role( 'shop_manager' ) ) {
// Shop managers can only edit customer info.
$userdata = get_userdata( $args[0] );
$shop_manager_editable_roles = apply_filters( 'woocommerce_shop_manager_editable_roles', array( 'customer' ) );
if ( property_exists( $userdata, 'roles' ) && ! empty( $userdata->roles ) && ! array_intersect( $userdata->roles, $shop_manager_editable_roles ) ) {
$caps[] = 'do_not_allow';
}
}
}
}
@ -596,7 +677,7 @@ add_filter( 'map_meta_cap', 'wc_modify_map_meta_cap', 10, 4 );
*/
function wc_get_customer_download_permissions( $customer_id ) {
$data_store = WC_Data_Store::load( 'customer-download' );
return apply_filters( 'woocommerce_permission_list', $data_store->get_downloads_for_customer( $customer_id ), $customer_id );
return apply_filters( 'woocommerce_permission_list', $data_store->get_downloads_for_customer( $customer_id ), $customer_id ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
}
/**
@ -655,6 +736,7 @@ function wc_get_customer_available_downloads( $customer_id ) {
}
// Download name will be 'Product Name' for products with a single downloadable file, and 'Product Name - File X' for products with multiple files.
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
$download_name = apply_filters(
'woocommerce_downloadable_product_name',
$download_file['name'],
@ -688,10 +770,11 @@ function wc_get_customer_available_downloads( $customer_id ) {
),
);
$file_number++;
++$file_number;
}
}
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
return apply_filters( 'woocommerce_customer_available_downloads', $downloads, $customer_id );
}
@ -817,7 +900,7 @@ add_action( 'profile_update', 'wc_update_profile_last_update_time', 10, 2 );
* @param mixed $_meta_value Value of the meta that was changed.
*/
function wc_meta_update_last_update_time( $meta_id, $user_id, $meta_key, $_meta_value ) {
$keys_to_track = apply_filters( 'woocommerce_user_last_update_fields', array( 'first_name', 'last_name' ) );
$keys_to_track = apply_filters( 'woocommerce_user_last_update_fields', array( 'first_name', 'last_name' ) ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
$update_time = in_array( $meta_key, $keys_to_track, true ) ? true : false;
$update_time = 'billing_' === substr( $meta_key, 0, 8 ) ? true : $update_time;
@ -848,7 +931,7 @@ function wc_set_user_last_update_time( $user_id ) {
* @return array
*/
function wc_get_customer_saved_methods_list( $customer_id ) {
return apply_filters( 'woocommerce_saved_payment_methods_list', array(), $customer_id );
return apply_filters( 'woocommerce_saved_payment_methods_list', array(), $customer_id ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
}
/**

View File

@ -1,14 +1,12 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Routes\V1;
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
use Automattic\WooCommerce\StoreApi\Payments\PaymentResult;
use Automattic\WooCommerce\StoreApi\Exceptions\InvalidCartException;
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
use Automattic\WooCommerce\StoreApi\Utilities\DraftOrderTrait;
use Automattic\WooCommerce\Checkout\Helpers\ReserveStockException;
use Automattic\WooCommerce\StoreApi\Utilities\CheckoutTrait;
use Automattic\WooCommerce\Tests\Blocks\StoreApi\Routes\AdditionalFields;
use Automattic\WooCommerce\Utilities\RestApiUtil;
/**
@ -588,55 +586,32 @@ class Checkout extends AbstractCartRoute {
* @param \WP_REST_Request $request Request object.
*/
private function process_customer( \WP_REST_Request $request ) {
try {
if ( $this->should_create_customer_account( $request ) ) {
$customer_id = $this->create_customer_account(
$request['billing_address']['email'],
$request['billing_address']['first_name'],
$request['billing_address']['last_name'],
$request['customer_password']
if ( $this->should_create_customer_account( $request ) ) {
$customer_id = wc_create_new_customer(
$request['billing_address']['email'],
'',
$request['customer_password'],
[
'first_name' => $request['billing_address']['first_name'],
'last_name' => $request['billing_address']['last_name'],
'source' => 'store-api',
]
);
if ( is_wp_error( $customer_id ) ) {
throw new RouteException(
esc_html( $customer_id->get_error_code() ),
esc_html( $customer_id->get_error_message() ),
400
);
// Associate customer with the order. This is done before login to ensure the order is associated with
// the correct customer if login fails.
$this->order->set_customer_id( $customer_id );
$this->order->save();
// Log the customer in to WordPress. Doing this inline instead of using `wc_set_customer_auth_cookie`
// because wc_set_customer_auth_cookie forces the use of session cookie.
wp_set_current_user( $customer_id );
wp_set_auth_cookie( $customer_id, true );
// Init session cookie if the session cookie handler exists.
if ( is_callable( [ WC()->session, 'init_session_cookie' ] ) ) {
WC()->session->init_session_cookie();
}
}
} catch ( \Exception $error ) {
switch ( $error->getMessage() ) {
case 'registration-error-invalid-email':
throw new RouteException(
'registration-error-invalid-email',
esc_html__( 'Please provide a valid email address.', 'woocommerce' ),
400
);
case 'registration-error-email-exists':
throw new RouteException(
'registration-error-email-exists',
sprintf(
// Translators: %s Email address.
esc_html__( 'An account is already registered with %s. Please log in or use a different email address.', 'woocommerce' ),
esc_html( $request['billing_address']['email'] )
),
400
);
case 'registration-error-empty-password':
throw new RouteException(
'registration-error-empty-password',
esc_html__( 'Please create a password for your account.', 'woocommerce' ),
400
);
}
// Associate customer with the order.
$this->order->set_customer_id( $customer_id );
$this->order->save();
// Set the customer auth cookie.
wc_set_customer_auth_cookie( $customer_id );
}
// Persist customer address data to account.
@ -673,159 +648,4 @@ class Checkout extends AbstractCartRoute {
return false;
}
/**
* Create a new account for a customer.
*
* The account is created with a generated username. The customer is sent
* an email notifying them about the account and containing a link to set
* their (initial) password.
*
* Intended as a replacement for wc_create_new_customer in WC core.
*
* @throws \Exception If an error is encountered when creating the user account.
*
* @param string $user_email The email address to use for the new account.
* @param string $first_name The first name to use for the new account.
* @param string $last_name The last name to use for the new account.
* @param string $password The password to use for the new account. If empty, a password will be generated.
*
* @return int User id if successful
*/
private function create_customer_account( $user_email, $first_name, $last_name, $password = '' ) {
if ( empty( $user_email ) || ! is_email( $user_email ) ) {
throw new \Exception( 'registration-error-invalid-email' );
}
if ( email_exists( $user_email ) ) {
throw new \Exception( 'registration-error-email-exists' );
}
// Handle password creation if not provided.
if ( empty( $password ) ) {
$password = wp_generate_password();
$password_generated = true;
} else {
$password_generated = false;
}
// This ensures `wp_generate_password` returned something (it is filterable and could be empty string).
if ( empty( $password ) ) {
throw new \Exception( 'registration-error-empty-password' );
}
$username = wc_create_new_customer_username( $user_email );
// Use WP_Error to handle registration errors.
$errors = new \WP_Error();
/**
* Fires before a customer account is registered.
*
* This hook fires before customer accounts are created and passes the form data (username, email) and an array
* of errors.
*
* This could be used to add extra validation logic and append errors to the array.
*
* @since 7.2.0
*
* @internal Matches filter name in WooCommerce core.
*
* @param string $username Customer username.
* @param string $user_email Customer email address.
* @param \WP_Error $errors Error object.
*/
do_action( 'woocommerce_register_post', $username, $user_email, $errors );
/**
* Filters registration errors before a customer account is registered.
*
* This hook filters registration errors. This can be used to manipulate the array of errors before
* they are displayed.
*
* @since 7.2.0
*
* @internal Matches filter name in WooCommerce core.
*
* @param \WP_Error $errors Error object.
* @param string $username Customer username.
* @param string $user_email Customer email address.
* @return \WP_Error
*/
$errors = apply_filters( 'woocommerce_registration_errors', $errors, $username, $user_email );
if ( is_wp_error( $errors ) && $errors->get_error_code() ) {
throw new \Exception( $errors->get_error_code() );
}
/**
* Filters customer data before a customer account is registered.
*
* This hook filters customer data. It allows user data to be changed, for example, username, password, email,
* first name, last name, and role.
*
* @since 7.2.0
*
* @param array $customer_data An array of customer (user) data.
* @return array
*/
$new_customer_data = apply_filters(
'woocommerce_new_customer_data',
array(
'user_login' => $username,
'user_pass' => $password,
'user_email' => $user_email,
'first_name' => $first_name,
'last_name' => $last_name,
'role' => 'customer',
'source' => 'store-api',
)
);
$customer_id = wp_insert_user( $new_customer_data );
if ( is_wp_error( $customer_id ) ) {
throw $this->map_create_account_error( $customer_id );
}
// Set account flag to remind customer to update generated password.
update_user_option( $customer_id, 'default_password_nag', true, true );
/**
* Fires after a customer account has been registered.
*
* This hook fires after customer accounts are created and passes the customer data.
*
* @since 7.2.0
*
* @internal Matches filter name in WooCommerce core.
*
* @param integer $customer_id New customer (user) ID.
* @param array $new_customer_data Array of customer (user) data.
* @param string $password_generated The generated password for the account.
*/
do_action( 'woocommerce_created_customer', $customer_id, $new_customer_data, $password_generated );
return $customer_id;
}
/**
* Convert an account creation error to an exception.
*
* @param \WP_Error $error An error object.
* @return \Exception.
*/
private function map_create_account_error( \WP_Error $error ) {
switch ( $error->get_error_code() ) {
// WordPress core error codes.
case 'empty_username':
case 'invalid_username':
case 'empty_email':
case 'invalid_email':
case 'email_exists':
case 'registerfail':
return new \Exception( 'woocommerce_rest_checkout_create_account_failure' );
}
return new \Exception( 'woocommerce_rest_checkout_create_account_failure' );
}
}

View File

@ -46,7 +46,7 @@ class WC_Tests_Customer_Functions extends WC_Unit_Test_Case {
$id = wc_create_new_customer( 'test@example.com', 'testuser', 'testpassword' );
$this->assertInstanceOf( 'WP_Error', $id );
// Empty username.
// Empty email.
$id = wc_create_new_customer( '', 'testuser', 'testpassword' );
$this->assertInstanceOf( 'WP_Error', $id );
@ -58,20 +58,6 @@ class WC_Tests_Customer_Functions extends WC_Unit_Test_Case {
$id = wc_create_new_customer( 'test2@example.com', 'testuser', 'testpassword' );
$this->assertInstanceOf( 'WP_Error', $id );
// Username with auto-generation.
update_option( 'woocommerce_registration_generate_username', 'yes' );
$id = wc_create_new_customer( 'fred@example.com', '', 'testpassword' );
$userdata = get_userdata( $id );
$this->assertEquals( 'fred', $userdata->user_login );
$id = wc_create_new_customer( 'fred@mail.com', '', 'testpassword' );
$userdata = get_userdata( $id );
$this->assertNotEquals( 'fred', $userdata->user_login );
$this->assertStringContainsString( 'fred', $userdata->user_login );
$id = wc_create_new_customer( 'fred@test.com', '', 'testpassword' );
$userdata = get_userdata( $id );
$this->assertNotEquals( 'fred', $userdata->user_login );
$this->assertStringContainsString( 'fred', $userdata->user_login );
// Test extra arguments to generate display_name.
$id = wc_create_new_customer(
'john.doe@example.com',
@ -85,13 +71,20 @@ class WC_Tests_Customer_Functions extends WC_Unit_Test_Case {
$userdata = get_userdata( $id );
$this->assertEquals( 'John Doe', $userdata->display_name );
// No password.
update_option( 'woocommerce_registration_generate_password', 'no' );
$id = wc_create_new_customer( 'joe@example.com', 'joecustomer', '' );
$this->assertInstanceOf( 'WP_Error', $id );
// Username with auto-generation.
$id = wc_create_new_customer( 'fred@example.com', '', 'testpassword' );
$userdata = get_userdata( $id );
$this->assertEquals( 'fred', $userdata->user_login );
$id = wc_create_new_customer( 'fred@mail.com', '', 'testpassword' );
$userdata = get_userdata( $id );
$this->assertNotEquals( 'fred', $userdata->user_login );
$this->assertStringContainsString( 'fred', $userdata->user_login );
$id = wc_create_new_customer( 'fred@test.com', '', 'testpassword' );
$userdata = get_userdata( $id );
$this->assertNotEquals( 'fred', $userdata->user_login );
$this->assertStringContainsString( 'fred', $userdata->user_login );
// Auto-generated password.
update_option( 'woocommerce_registration_generate_password', 'yes' );
$id = wc_create_new_customer( 'joe@example.com', 'joecustomer', '' );
$this->assertTrue( is_numeric( $id ) && $id > 0 );
}