coryd.dev/api/contact.php

241 lines
7.5 KiB
PHP

<?php
require_once __DIR__.'/../bootstrap.php';
use App\Classes\BaseHandler;
use GuzzleHttp\Client;
class ContactHandler extends BaseHandler
{
protected string $postgrestUrl;
protected string $postgrestApiKey;
private string $forwardEmailApiKey;
private Client $httpClient;
public function __construct(?Client $httpClient = null)
{
parent::__construct();
$this->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 <hi@admin.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);
}