'', 'display_name' => '', 'email' => '']; if ($_SERVER['REQUEST_METHOD'] === 'POST') { csrf_verify(); // Honeypot + timing trap: a decoy field real users never see/fill, and a // minimum time between the form being shown and submitted. Either one // tripping means this almost certainly isn't a human — pretend success // without creating an account, so a bot doesn't learn it was caught and // adapt its script. $honeypot = trim($_POST['website'] ?? ''); $shown_at = (int)($_SESSION['reg_form_shown_at'] ?? 0); $bot_detected = ($honeypot !== '') || (time() - $shown_at < 3); $username = trim($_POST['username'] ?? ''); $display_name = trim($_POST['display_name'] ?? ''); $email = trim($_POST['email'] ?? ''); $password = $_POST['password'] ?? ''; $password_confirm = $_POST['password_confirm'] ?? ''; $fields = compact('username', 'display_name', 'email'); if ($bot_detected) { $success = true; $auto_confirmed = false; } else { // reCAPTCHA v3: unlike the honeypot/timing trap above, a failure here // gets a real, visible error — a legitimate low-score user deserves // an explicit retry rather than a silently-discarded submission. if (recaptcha_enabled() && !verify_recaptcha($_POST['recaptcha_token'] ?? '')) { $errors[] = 'We could not verify your submission. Please try again.'; } // Validate username if (!preg_match('/^[a-zA-Z0-9_]{3,30}$/', $username)) { $errors[] = 'Username must be 3-30 characters and contain only letters, numbers, and underscores.'; } // Validate email if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { $errors[] = 'Please enter a valid email address.'; } // Validate password if (strlen($password) < 8) { $errors[] = 'Password must be at least 8 characters.'; } if ($password !== $password_confirm) { $errors[] = 'Passwords do not match.'; } if (empty($errors)) { $pdo = get_pdo(); // Check uniqueness $chk = $pdo->prepare('SELECT id FROM users WHERE username = ? OR email = ?'); $chk->execute([$username, $email]); $existing = $chk->fetchAll(); if (!empty($existing)) { $chk_u = $pdo->prepare('SELECT id FROM users WHERE username = ?'); $chk_u->execute([$username]); if ($chk_u->fetch()) $errors[] = 'That username is already taken.'; $chk_e = $pdo->prepare('SELECT id FROM users WHERE email = ?'); $chk_e->execute([$email]); if ($chk_e->fetch()) $errors[] = 'That email address is already registered.'; } } if (empty($errors)) { $pdo = get_pdo(); $smtp_host = get_setting('smtp_host'); $auto_confirm = ($smtp_host === ''); // No SMTP = skip email confirmation $hash = password_hash($password, PASSWORD_BCRYPT); $token = $auto_confirm ? null : bin2hex(random_bytes(32)); $pdo->prepare(" INSERT INTO users (username, email, password_hash, display_name, role, rosary_limit, email_confirmed, confirm_token) VALUES (?, ?, ?, ?, 'user', 1, ?, ?) ")->execute([$username, $email, $hash, $display_name ?: $username, $auto_confirm ? 1 : 0, $token]); if (!$auto_confirm && $token) { $site_url = rtrim(get_setting('site_url'), '/'); $link = $site_url . '/confirm?token=' . urlencode($token); $site_name = get_setting('site_name', APP_NAME); $body_html = "

Confirm your email

Hello, " . htmlspecialchars($display_name ?: $username) . "!

Thank you for registering with {$site_name}. Click the button below to confirm your email address:

Confirm Email

Or copy this link: " . htmlspecialchars($link) . "

If you did not register, ignore this email.

"; $html = email_template('Confirm your email — ' . $site_name, $body_html); send_email($email, $display_name ?: $username, 'Confirm your email — ' . $site_name, $html); } $success = true; $auto_confirmed = $auto_confirm; } } } // Refresh the timing-trap timestamp whenever the form is about to be (re)shown. if (!$success) { $_SESSION['reg_form_shown_at'] = time(); } ?> Register — <?= htmlspecialchars(get_setting('site_name', APP_NAME)) ?>

Create Account

Account created! Sign in now.
Account created! Please check your email to confirm your address before logging in.

3-30 characters. Letters, numbers, underscores only.

Optional. Shown publicly.

At least 8 characters.

Already have an account? Sign in