241 lines
7.5 KiB
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);
|
|
}
|