settings = $settings; $this->customers = $customers; $this->logger = $logger; $this->links = $links; } /** * Register the order-completed listener. * * @since 1.0.0 * * @return void */ public function hooks(): void { add_action( 'releasecart_order_completed', array( $this, 'onOrderCompleted' ), 10, 1 ); } /** * Provision or link an account without failing the order. * * @since 1.0.0 * * @param Order $order The completed order. * @return void */ public function onOrderCompleted( Order $order ): void { try { $this->provision( $order ); } catch ( Throwable $failure ) { $this->logger->log( 'error', sprintf( 'Auto-registration failed for order %d: %s', (int) $order->id(), $failure->getMessage() ), array( 'order_id' => (int) $order->id() ), 'order', (int) $order->id() ); } } /** * Skip linked customers, link an existing account or create a new one. * * @since 1.0.0 * * @param Order $order The completed order. * @return void */ private function provision( Order $order ): void { if ( ! $this->settings->autoRegister() ) { return; } $customer = $this->orderCustomer( $order ); // Already linked to a WordPress user (a logged-in buyer, or a prior // provisioning): nothing to do, and nothing to double-provision. if ( null !== $customer && null !== $customer->userId() ) { return; } $email = $order->email(); // A WordPress account already uses this email: link the customer record to // it and never touch the account itself. A guest who typed someone else's // address only links their own order to the mailbox owner's account, never // the reverse. $existing = email_exists( $email ); if ( false !== $existing ) { $this->customers->linkWpUser( (int) $existing, $email ); return; } $this->createUser( $order, $email, $customer ); } /** * The order's linked customer, or null when it carries none. * * @since 1.0.0 * * @param Order $order The order. * @return Customer|null The customer, or null. */ private function orderCustomer( Order $order ): ?Customer { $customerId = $order->customerId(); return null !== $customerId ? $this->customers->get( $customerId ) : null; } /** * Create an account with a unique login and generated password. * * WordPress can send a set-password link. Never email a password. * * @since 1.0.0 * * @param Order $order The completed order. * @param string $email The buyer email. * @param Customer|null $customer The order's customer, or null. * @return void * * @throws RuntimeException When wp_insert_user returns an error. */ private function createUser( Order $order, string $email, ?Customer $customer ): void { $args = array( 'user_login' => $this->uniqueLogin( $email ), 'user_email' => $email, 'user_pass' => wp_generate_password( 24 ), 'role' => AccountRole::resolve( get_option( 'default_role' ) ), ); $name = null !== $customer ? (string) $customer->name() : ''; $first = null !== $customer ? (string) $customer->firstName() : ''; $last = null !== $customer ? (string) $customer->lastName() : ''; if ( '' !== $name ) { $args['display_name'] = $name; } if ( '' !== $first ) { $args['first_name'] = $first; } if ( '' !== $last ) { $args['last_name'] = $last; } /** * Filter wp_insert_user arguments for a completed guest order. * * The role cannot go beyond read. A role that can do more becomes subscriber. * * @since 1.0.0 * * @param array $args The wp_insert_user arguments. * @param Order $order The completed order the account is for. */ $args = apply_filters( 'releasecart_auto_register_user_args', $args, $order ); if ( ! is_array( $args ) ) { return; } $args['role'] = AccountRole::resolve( $args['role'] ?? '' ); $userId = wp_insert_user( $args ); if ( is_wp_error( $userId ) ) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Not HTML output here; escaped where printed. throw new RuntimeException( 'wp_insert_user failed: ' . $userId->get_error_message() ); } $userId = (int) $userId; /** * Filter whether to email a new account holder a set-password link. * * @since 1.0.0 * * @param bool $notify Whether to send the set-password email. * @param int $userId The new user id. * @param Order $order The completed order the account is for. */ if ( (bool) apply_filters( 'releasecart_auto_register_notify', true, $userId, $order ) ) { // The 'user' notification sends the set-password link only; no plaintext // password is ever emailed. $send = static function () use ( $userId ): void { wp_send_new_user_notifications( $userId, 'user' ); }; if ( null !== $this->links ) { $this->links->withBrandedLinks( $send ); } else { $send(); } } } /** * Build a unique login from the email local part. * * Use `user` if sanitisation leaves no usable text. * * @since 1.0.0 * * @param string $email The buyer email. * @return string A login not currently taken. */ private function uniqueLogin( string $email ): string { $parts = explode( '@', $email ); $base = sanitize_user( (string) ( $parts[0] ?? '' ), true ); if ( '' === $base ) { $base = 'user'; } $login = $base; $suffix = 2; while ( username_exists( $login ) ) { $login = $base . $suffix; ++$suffix; } return $login; } }