chore(*.php): use pint for php formatting
This commit is contained in:
parent
029caaaa9e
commit
cf1ee4c97f
40 changed files with 2261 additions and 1900 deletions
399
api/contact.php
399
api/contact.php
|
@ -1,214 +1,241 @@
|
|||
<?php
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.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;
|
||||
protected string $postgrestUrl;
|
||||
|
||||
public function __construct(?Client $httpClient = null)
|
||||
{
|
||||
parent::__construct();
|
||||
protected string $postgrestApiKey;
|
||||
|
||||
$this->httpClient = $httpClient ?? new Client();
|
||||
$this->forwardEmailApiKey = $_ENV["FORWARDEMAIL_API_KEY"] ?? getenv("FORWARDEMAIL_API_KEY");
|
||||
}
|
||||
private string $forwardEmailApiKey;
|
||||
|
||||
public function handleRequest(): void
|
||||
{
|
||||
try {
|
||||
$this->validateReferer();
|
||||
$this->checkRateLimit();
|
||||
$this->enforceHttps();
|
||||
private Client $httpClient;
|
||||
|
||||
$contentType = $_SERVER["CONTENT_TYPE"] ?? "";
|
||||
$formData = null;
|
||||
public function __construct(?Client $httpClient = null)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
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);
|
||||
$this->httpClient = $httpClient ?? new Client();
|
||||
$this->forwardEmailApiKey = $_ENV['FORWARDEMAIL_API_KEY'] ?? getenv('FORWARDEMAIL_API_KEY');
|
||||
}
|
||||
}
|
||||
|
||||
private function validateReferer(): void
|
||||
{
|
||||
$referer = $_SERVER["HTTP_REFERER"] ?? "";
|
||||
$allowedDomain = "coryd.dev";
|
||||
public function handleRequest(): void
|
||||
{
|
||||
try {
|
||||
$this->validateReferer();
|
||||
$this->checkRateLimit();
|
||||
$this->enforceHttps();
|
||||
|
||||
if (!str_contains($referer, $allowedDomain)) throw new \Exception("Invalid submission origin.");
|
||||
}
|
||||
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
|
||||
$formData = null;
|
||||
|
||||
private function checkRateLimit(): void
|
||||
{
|
||||
$ipAddress = $_SERVER["REMOTE_ADDR"] ?? "unknown";
|
||||
$cacheFile = sys_get_temp_dir() . "/rate_limit_" . md5($ipAddress);
|
||||
$rateLimitDuration = 60;
|
||||
$maxRequests = 5;
|
||||
if (strpos($contentType, 'application/json') !== false) {
|
||||
$rawBody = file_get_contents('php://input');
|
||||
$formData = json_decode($rawBody, true);
|
||||
|
||||
if (file_exists($cacheFile)) {
|
||||
$data = json_decode(file_get_contents($cacheFile), 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);
|
||||
|
||||
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();
|
||||
$handler = new ContactHandler();
|
||||
$handler->handleRequest();
|
||||
} catch (\Exception $e) {
|
||||
error_log("Contact form error: " . $e->getMessage());
|
||||
error_log('Contact form error: '.$e->getMessage());
|
||||
|
||||
echo json_encode(["error" => $e->getMessage()]);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
|
||||
http_response_code(500);
|
||||
http_response_code(500);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue