File: //wordpress/plugins/wp-cloud-client/1.1.5/src/Handler/OneClickLoginHandler.php
<?php
declare(strict_types=1);
namespace VPlugins\WPCloudClient\Handler;
use VPlugins\WPCloudClient\Access\LoginTokenManager;
use VPlugins\WPCloudClient\Multisite\SuperAdminManager;
use VPlugins\WPCloudClient\Support\Logger;
/**
* Handles the `one_click_login` action — issues a short-lived single-use SSO
* token for a resolved WordPress user.
*
* Supported `$params` keys (all optional unless noted):
* - `user_email` (string) Preferred identifier. `@vendasta.com` is rewritten via `wpcc_whitelabel_email()` before lookup.
* - `user_id` (int) Fallback when email is not supplied.
* - `user_login` (string) Fallback when neither email nor id is supplied.
* - `create_user_if_missing` (bool) When true and `user_email` matches no existing user, auto-creates an administrator with the supplied email (since 0.1.10). Opt-in; default `false` preserves the throw-on-unknown-email behavior.
* - `ttl` (int) Token lifetime in seconds; clamped to [60, 600].
* - `redirect_to` (string) Optional post-login destination.
* - `is_super_admin` (bool) Multisite-only; promotes the user before the SSO redirect via `SuperAdminManager`.
*
* Identifier precedence when multiple are supplied: `user_email` → `user_id` → `user_login` → first administrator on the site.
*/
final class OneClickLoginHandler extends AbstractHandler {
private const DEFAULT_TTL = 300;
/**
* Constructor.
*
* @param LoginTokenManager $tokenManager The login token manager instance.
*/
public function __construct(
private readonly LoginTokenManager $tokenManager,
) {}
/**
* Return the action name this handler responds to.
*
* @return string The action identifier.
*/
public function action(): string {
return 'one_click_login';
}
/**
* Execute the one-click login action.
*
* @param array $params The request parameters.
* @return array The login URL and metadata.
* @throws \InvalidArgumentException When user_id or user_login is provided but no matching user exists,
* when user_email is unknown and create_user_if_missing is not set, or when an auto-create email is malformed.
* @throws \RuntimeException When no administrator exists (fallback path) or when user auto-creation fails.
*/
public function execute( array $params ): array {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
error_log(
sprintf(
'WP Cloud Client one_click_login received: user_email=%s user_id=%s user_login=%s create_user_if_missing=%s redirect_to=%s',
$params['user_email'] ?? '(unset)',
isset( $params['user_id'] ) ? (string) $params['user_id'] : '(unset)',
$params['user_login'] ?? '(unset)',
isset( $params['create_user_if_missing'] ) ? ( $params['create_user_if_missing'] ? 'true' : 'false' ) : '(unset)',
$params['redirect_to'] ?? '(unset)'
)
);
// Accept user_email (preferred), user_id, or user_login.
$userId = $this->resolveUserId( $params );
$ttl = (int) ( $params['ttl'] ?? self::DEFAULT_TTL );
$redirectTo = $params['redirect_to'] ?? '';
// Clamp TTL: minimum 60s, maximum 600s
$ttl = max( 60, min( 600, $ttl ) );
// Persist the platform's super admin decision so SuperAdminManager can
// apply it when wp_login fires after the token is consumed.
if ( isset( $params['is_super_admin'] ) && is_multisite() ) {
SuperAdminManager::setFlag( $userId, (bool) $params['is_super_admin'] );
}
$result = $this->tokenManager->create( $userId, $ttl );
$loginUrl = add_query_arg(
[
'wpcc_login_token' => $result['token'],
],
site_url( '/' )
);
if ( '' !== $redirectTo ) {
$loginUrl = add_query_arg( 'redirect_to', rawurlencode( $redirectTo ), $loginUrl );
}
return [
'login_url' => $loginUrl,
'expires_at' => $result['expires_at'],
'ttl' => $ttl,
'user_id' => $userId,
];
}
/**
* Resolve the user ID from the request parameters.
*
* Precedence: `user_email` → `user_id` → `user_login` → first administrator.
* Email is the canonical identifier supplied by the upstream WP Cloud Manager
* integration; `user_id` and `user_login` remain supported for callers that
* already know the WordPress user.
*
* `@vendasta.com` emails are rewritten to their `@support.websitepro.hosting`
* equivalents via `wpcc_whitelabel_email()` before the lookup, so platform
* support staff don't need pre-existing accounts under their real domain.
*
* When `user_email` is supplied but does not match an existing user, the
* caller can opt into auto-creation by setting `create_user_if_missing` to
* `true`. Without the flag, the handler throws so the caller can decide.
*
* @param array $params The request parameters containing user identification.
* @return int The resolved WordPress user ID.
* @throws \InvalidArgumentException When the specified user is not found
* and `create_user_if_missing` is not set, or when an auto-create email is malformed.
* @throws \RuntimeException When no administrator user exists on the site,
* or when user creation fails for a reason other than a benign race.
*/
private function resolveUserId( array $params ): int {
if ( ! empty( $params['user_email'] ) ) {
$email = \wpcc_whitelabel_email( $params['user_email'] );
$user = get_user_by( 'email', $email );
if ( $user ) {
return (int) $user->ID;
}
if ( ! empty( $params['create_user_if_missing'] ) ) {
return $this->createUserFromEmail( $email );
}
throw new \InvalidArgumentException( sprintf( 'User with email "%s" not found.', $email ) );
}
if ( ! empty( $params['user_id'] ) ) {
$userId = (int) $params['user_id'];
$user = get_user_by( 'id', $userId );
if ( ! $user ) {
throw new \InvalidArgumentException( sprintf( 'User ID %d not found.', $userId ) );
}
return $userId;
}
if ( ! empty( $params['user_login'] ) ) {
$user = get_user_by( 'login', $params['user_login'] );
if ( ! $user ) {
throw new \InvalidArgumentException( sprintf( 'User "%s" not found.', $params['user_login'] ) );
}
return $user->ID;
}
// Default: first administrator
$admins = get_users(
[
'role' => 'administrator',
'number' => 1,
'orderby' => 'ID',
'order' => 'ASC',
]
);
if ( empty( $admins ) ) {
throw new \RuntimeException( 'No administrator user found on this site.' );
}
return $admins[0]->ID;
}
/**
* Create a new administrator user with the supplied email.
*
* Username is derived from the email local-part (sanitized) with a random
* suffix appended on collision. Password is random 24-char and is never
* surfaced to the caller — the user authenticates via the SSO token.
*
* Privilege grant is observable: emits a `Logger::info` audit line and
* fires the `wpcc_user_auto_created` action so incident response can
* answer "why is there a new admin on this site?".
*
* @param string $email The email for the new user.
* @return int The WordPress user ID of the newly-created (or race-recovered) user.
* @throws \InvalidArgumentException When the email is malformed.
* @throws \RuntimeException When user creation fails for a reason other than
* a benign race (e.g., database error).
*/
private function createUserFromEmail( string $email ): int {
if ( ! is_email( $email ) ) {
throw new \InvalidArgumentException(
sprintf( 'Cannot create user: "%s" is not a valid email address.', $email )
);
}
$login = $this->deriveLoginFromEmail( $email );
if ( username_exists( $login ) ) {
// 6-char random suffix gives ~2B-namespace per base; single attempt is
// fine because wp_insert_user will reject any leftover collision and
// the race-recovery block below handles concurrent creators.
$login = substr( $login, 0, 53 ) . '_' . wp_generate_password( 6, false );
}
$userId = wp_insert_user(
[
'user_login' => $login,
'user_email' => $email,
'user_pass' => wp_generate_password( 24, true, true ),
// Auto-created users default to administrator because one_click_login is only
// invoked by the trusted platform (via API-key auth) for support access.
'role' => 'administrator',
'display_name' => $email,
]
);
if ( is_wp_error( $userId ) ) {
// Race recovery: another request may have created the user between
// our `get_user_by` check and `wp_insert_user`.
$existing = get_user_by( 'email', $email );
if ( $existing ) {
return (int) $existing->ID;
}
throw new \RuntimeException(
sprintf(
'Failed to create WP user with email "%s" [%s]: %s',
$email,
$userId->get_error_code(),
$userId->get_error_message()
)
);
}
$userId = (int) $userId;
if ( is_multisite() ) {
add_user_to_blog( get_current_blog_id(), $userId, 'administrator' );
}
( new Logger() )->info(
sprintf( 'one_click_login: auto-created administrator "%s" (ID %d) for email "%s"', $login, $userId, $email )
);
do_action( 'wpcc_user_auto_created', $userId, $email, 'one_click_login' );
return $userId;
}
/**
* Derive a sanitized WordPress login from an email address.
*
* Uses the email local-part, lowercased, with disallowed characters
* replaced by underscores. Clamped to 55 characters so a collision suffix
* still fits the VARCHAR(60) `user_login` column. Falls back to
* `wpcc_user` when the derived login is empty.
*
* @param string $email The email to derive a login from.
* @return string A sanitized WP login.
*/
private function deriveLoginFromEmail( string $email ): string {
$at = strpos( $email, '@' );
$local = false !== $at ? substr( $email, 0, $at ) : $email;
$login = strtolower( (string) preg_replace( '/[^a-zA-Z0-9_\-\.]/', '_', $local ) );
$login = '' !== $login ? $login : 'wpcc_user';
// wp_users.user_login is VARCHAR(60); leave room for a 7-char "_xxxxxx" collision suffix.
return substr( $login, 0, 55 );
}
}