httpClient = $httpClient ?? new Client(); $this->forwardEmailApiKey = $_ENV['FORWARDEMAIL_API_KEY'] ?? getenv('FORWARDEMAIL_API_KEY'); } public function handleRequest(): void { try { $this->validateReferer(); $this->checkRateLimit(); $this->enforceHttps(); $contentType = $_SERVER['CONTENT_TYPE'] ?? ''; $formData = null; if (strpos($contentType, 'application/json') !== false) { $rawBody = file_get_contents('php://input'); $formData = json_decode($rawBody, true); if (! $formData || ! isset($formData['data'])) { throw new \Exception('Invalid JSON payload.'); } $formData = $formData['data']; } elseif ( strpos($contentType, 'application/x-www-form-urlencoded') !== false ) { $formData = $_POST; } else { $this->sendErrorResponse('Unsupported Content-Type. Use application/json or application/x-www-form-urlencoded.', 400); } if (! empty($formData['hp_name'])) { $this->sendErrorResponse('Invalid submission.', 400); } $name = htmlspecialchars( trim($formData['name'] ?? ''), ENT_QUOTES, 'UTF-8' ); $email = filter_var($formData['email'] ?? '', FILTER_VALIDATE_EMAIL); $message = htmlspecialchars( trim($formData['message'] ?? ''), ENT_QUOTES, 'UTF-8' ); if (empty($name)) { $this->sendErrorResponse('Name is required.', 400); } if (! $email) { $this->sendErrorResponse('Valid email is required.', 400); } if (empty($message)) { $this->sendErrorResponse('Message is required.', 400); } if (strlen($name) > 100) { $this->sendErrorResponse('Name is too long. Max 100 characters allowed.', 400); } if (strlen($message) > 1000) { $this->sendErrorResponse('Message is too long. Max 1000 characters allowed.', 400); } if ($this->isBlockedDomain($email)) { $this->sendErrorResponse('Submission from blocked domain.', 400); } $contactData = [ 'name' => $name, 'email' => $email, 'message' => $message, 'replied' => false, ]; $this->saveToDatabase($contactData); $this->sendNotificationEmail($contactData); $this->sendRedirect('/contact/success'); } catch (\Exception $e) { error_log('Error handling contact form submission: '.$e->getMessage()); $this->sendErrorResponse($e->getMessage(), 400); } } private function validateReferer(): void { $referer = $_SERVER['HTTP_REFERER'] ?? ''; $allowedDomain = 'coryd.dev'; if (! str_contains($referer, $allowedDomain)) { throw new \Exception('Invalid submission origin.'); } } private function checkRateLimit(): void { $ipAddress = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; $cacheFile = sys_get_temp_dir().'/rate_limit_'.md5($ipAddress); $rateLimitDuration = 60; $maxRequests = 5; if (file_exists($cacheFile)) { $data = json_decode(file_get_contents($cacheFile), true); if (time() < $data['timestamp'] + $rateLimitDuration && $data['count'] >= $maxRequests) { header('Location: /429', true, 302); exit(); } $data['count']++; } else { $data = ['count' => 1, 'timestamp' => time()]; } file_put_contents($cacheFile, json_encode($data)); } private function enforceHttps(): void { if (empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') { throw new \Exception('Secure connection required. Use HTTPS.'); } } private function isBlockedDomain(string $email): bool { $domain = substr(strrchr($email, '@'), 1); if (! $domain) { return false; } $response = $this->httpClient->get( "{$this->postgrestUrl}/blocked_domains", [ 'headers' => [ 'Content-Type' => 'application/json', 'Authorization' => "Bearer {$this->postgrestApiKey}", ], 'query' => [ 'domain_name' => "eq.{$domain}", 'limit' => 1, ], ] ); $blockedDomains = json_decode($response->getBody(), true); return ! empty($blockedDomains); } private function saveToDatabase(array $contactData): void { $response = $this->httpClient->post("{$this->postgrestUrl}/contacts", [ 'headers' => [ 'Content-Type' => 'application/json', 'Authorization' => "Bearer {$this->postgrestApiKey}", ], 'json' => $contactData, ]); if ($response->getStatusCode() >= 400) { $errorResponse = json_decode($response->getBody(), true); throw new \Exception('PostgREST error: '.($errorResponse['message'] ?? 'Unknown error')); } } private function sendNotificationEmail(array $contactData): void { $authHeader = 'Basic '.base64_encode("{$this->forwardEmailApiKey}:"); $emailSubject = 'Contact form submission'; $emailText = sprintf( "Name: %s\nEmail: %s\nMessage: %s\n", $contactData['name'], $contactData['email'], $contactData['message'] ); $response = $this->httpClient->post( 'https://api.forwardemail.net/v1/emails', [ 'headers' => [ 'Content-Type' => 'application/x-www-form-urlencoded', 'Authorization' => $authHeader, ], 'form_params' => [ 'from' => 'coryd.dev ', 'to' => 'hi@coryd.dev', 'subject' => $emailSubject, 'text' => $emailText, 'replyTo' => $contactData['email'], ], ] ); if ($response->getStatusCode() >= 400) { throw new \Exception('Failed to send email notification.'); } } private function sendRedirect(string $path): void { $protocol = (! empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST']; $redirectUrl = "{$protocol}://{$host}{$path}"; header("Location: $redirectUrl", true, 302); exit(); } } try { $handler = new ContactHandler(); $handler->handleRequest(); } catch (\Exception $e) { error_log('Contact form error: '.$e->getMessage()); echo json_encode(['error' => $e->getMessage()]); http_response_code(500); }