228 lines
6.7 KiB
PHP
228 lines
6.7 KiB
PHP
<?php
|
|
|
|
require __DIR__ . "/Classes/BaseHandler.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)
|
|
{
|
|
$this->httpClient = $httpClient ?? new Client();
|
|
$this->loadEnvironment();
|
|
}
|
|
|
|
private function loadEnvironment(): void
|
|
{
|
|
$this->postgrestUrl = $_ENV["POSTGREST_URL"] ?? getenv("POSTGREST_URL");
|
|
$this->postgrestApiKey =
|
|
$_ENV["POSTGREST_API_KEY"] ?? getenv("POSTGREST_API_KEY");
|
|
$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 (
|
|
$data["timestamp"] + $rateLimitDuration > time() &&
|
|
$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);
|
|
}
|