allowAttempt('verify-ip', $_SERVER['REMOTE_ADDR'] ?? '', 10, 3600) || !$auth->allowAttempt('verify-account', $identifier, 3, 3600)) return false; $user = $auth->findUser($identifier); if (!$user || $user['user_status'] !== 'active' || $user['user_email_verified_at']) return false; $url = rtrim((string) $this->getSettings()->ApplicationUrl, '/'); if (!filter_var($url, FILTER_VALIDATE_URL) || parse_url($url, PHP_URL_SCHEME) !== 'https' || parse_url($url, PHP_URL_QUERY) !== null || parse_url($url, PHP_URL_FRAGMENT) !== null || parse_url($url, PHP_URL_USER) !== null || parse_url($url, PHP_URL_PASS) !== null) throw new RuntimeException('Configure a trusted HTTPS application URL.'); $token = bin2hex(random_bytes(32)); $db = $this->connect(); $db->beginTransaction(); try { $stmt = $db->prepare('SELECT * FROM users WHERE user_serial=? FOR UPDATE'); $stmt->execute([$user['user_serial']]); $current = $stmt->fetch(); if (!$current || $current['user_status'] !== 'active' || $current['user_email_verified_at'] || $current['user_email'] !== $user['user_email'] || (int) $current['user_auth_version'] !== (int) $user['user_auth_version']) { $db->rollBack(); return false; } $stmt = $db->prepare('INSERT INTO email_verifications(token_hash,user_serial,email,auth_version,expires_at) VALUES(?,?,?,?,DATE_ADD(UTC_TIMESTAMP(),INTERVAL 24 HOUR))'); $stmt->execute([hash('sha256', $token), $user['user_serial'], $user['user_email'], $user['user_auth_version']]); $db->commit(); } catch (Throwable $e) { if ($db->inTransaction()) $db->rollBack();throw $e; } if (!$this->deliver($user, $url . '/main.php?Xaction=verifyemail&token=' . $token)) { $stmt = $db->prepare('DELETE FROM email_verifications WHERE token_hash=?'); $stmt->execute([hash('sha256', $token)]); throw new RuntimeException('Verification email could not be delivered.'); } return true; } protected function deliver(array $user, string $url): bool { $h = static fn($v) => htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8'); return (new Email())->sendEmail($user['user_email'], self::APPLICATION_NAME . ' - Verify your email', '

Verify the email address for account ' . $h($user['user_name']) . '.

Verify email

The link expires in 24 hours. You must also enter the account password. If you did not create this account, ignore this message or use password reset to secure it.

'); } public function confirm(string $token, string $password): bool { if (!preg_match('/\A[a-f0-9]{64}\z/', $token)) return false; $db = $this->connect(); $db->beginTransaction(); try { $stmt = $db->prepare('SELECT user_serial FROM email_verifications WHERE token_hash=?'); $stmt->execute([hash('sha256', $token)]); $id = $stmt->fetchColumn(); if (!$id) { $db->rollBack(); return false; } $stmt = $db->prepare('SELECT * FROM users WHERE user_serial=? FOR UPDATE'); $stmt->execute([$id]); $user = $stmt->fetch(); $stmt = $db->prepare('SELECT * FROM email_verifications WHERE token_hash=? AND expires_at>UTC_TIMESTAMP() FOR UPDATE'); $stmt->execute([hash('sha256', $token)]); $row = $stmt->fetch(); if (!$row || !$user || $user['user_status'] !== 'active' || $row['email'] !== $user['user_email'] || (int) $row['auth_version'] !== (int) $user['user_auth_version'] || !password_verify($password, $user['user_password_hash'])) { $db->rollBack(); return false; } $stmt = $db->prepare('UPDATE users SET user_email_verified_at=UTC_TIMESTAMP() WHERE user_serial=?'); $stmt->execute([$id]); $stmt = $db->prepare('DELETE FROM email_verifications WHERE user_serial=?'); $stmt->execute([$id]); Audit::required($db, (int) $id, 'verified email ownership'); $db->commit(); return true; } catch (Throwable $e) { if ($db->inTransaction()) $db->rollBack();throw $e; } } public function page(): void { $token = $_POST['token'] ?? $_GET['token'] ?? ''; $message = ''; $complete = false; if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') { $this->requireCsrf(); if (($_POST['step'] ?? '') === 'confirm') { $allowed = (new AuthService())->allowAttempt('verify-submit-ip', $_SERVER['REMOTE_ADDR'] ?? '', 20, 900); $complete = $allowed && $this->confirm($token, $_POST['user_password'] ?? ''); $message = $complete ? 'Email verified. You can now be assigned to a club.' : 'Unable to verify. Check the password or request a fresh link. You can also reset your password to prove mailbox ownership.'; } else { try { $this->request(trim($_POST['user_email'] ?? '')); } catch (Throwable $e) { error_log('Email verification failed: ' . $e->getMessage()); } $message = 'If an active account needs verification, a link will be sent. You can also reset your password to verify its email.'; } } $h = static fn($v) => htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8'); echo 'Verify email

Verify your email

' . $h($message) . '

'; if (!$complete) { echo '
'; if ($token) echo ''; else echo ''; echo '
'; } echo '

Return to application · Request another link · Reset password

'; exit; } }