'AI Training', 'workflow-automation' => 'Workflow Automation', 'software-development' => 'Software Development', 'website-design' => 'Website Design', 'fractional-cto' => 'Fractional CTO', 'other' => 'Other', ]; public function submit(): void { // 5 submissions per 10 minutes per IP. $this->guardPublic('contact_submit', 5, 600); if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { $this->json(null, 405, ['code' => 'method_not_allowed', 'message' => 'POST only']); } $in = $this->readBody(); // Honeypot: a field hidden from humans. A bot fills everything, so any // value here is spam. Answer 200 so the bot cannot tell it was caught, // but write nothing. if (trim((string) ($in['company'] ?? '')) !== '') { $this->json(['received' => true]); } $name = trim((string) ($in['name'] ?? '')); $email = trim((string) ($in['email'] ?? '')); $subject = trim((string) ($in['subject'] ?? 'other')); $message = trim((string) ($in['message'] ?? '')); $errors = []; if ($name === '' || mb_strlen($name) > 160) { $errors['name'] = 'Please enter your name.'; } if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL) || mb_strlen($email) > 255) { $errors['email'] = 'Please enter a valid email address.'; } if (!isset(self::SUBJECTS[$subject])) { // Unknown subject is coerced rather than rejected — never trust the // client's list, but do not fail a real person over it either. $subject = 'other'; } if (mb_strlen($message) < 10) { $errors['message'] = 'Please add a little more detail (10 characters or more).'; } if (mb_strlen($message) > 5000) { $errors['message'] = 'That message is a bit long — please keep it under 5000 characters.'; } if ($errors) { $this->json(null, 422, [ 'code' => 'validation_failed', 'message' => 'Please check the highlighted fields.', 'fields' => $errors, ]); } \Db::insert('cja_contact', [ 'name' => $name, 'email' => $email, 'subject' => $subject, 'message' => $message, 'status' => 'new', 'ip' => $_SERVER['REMOTE_ADDR'] ?? null, 'user_agent' => mb_substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255), 'referrer' => mb_substr((string) ($_SERVER['HTTP_REFERER'] ?? ''), 0, 255), 'created_at' => date('Y-m-d H:i:s'), ]); // TODO: notify. A mailer helper exists (app/Helpers/Mailer.php, Brevo); // wire it once a from-address and API key are configured. The row is // saved either way, so a mail outage never loses a lead. $this->json([ 'received' => true, 'message' => "Thanks — I'll get back to you shortly.", ]); } /** Read a JSON body, falling back to form-encoded POST. */ private function readBody(): array { $raw = file_get_contents('php://input'); if ($raw !== '' && $raw !== false) { $decoded = json_decode($raw, true); if (is_array($decoded)) { return $decoded; } } return $_POST ?: []; } }