116 lines
3.4 KiB
PHP
116 lines
3.4 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
require __DIR__ . '/../includes/bootstrap.php';
|
|
|
|
// ensure session csrf token exists (parity with frontend)
|
|
if (empty($_SESSION['_csrf']) && !empty($_SESSION['csrf'])) {
|
|
$_SESSION['_csrf'] = $_SESSION['csrf'];
|
|
}
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header('Cache-Control: no-store');
|
|
|
|
function json_out(array $payload, int $code = 200): void {
|
|
http_response_code($code);
|
|
echo json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
json_out(['ok' => false, 'error' => 'method_not_allowed'], 405);
|
|
}
|
|
|
|
$csrf = (string)($_POST['csrf'] ?? '');
|
|
if (!csrf_check($csrf)) {
|
|
json_out(['ok' => false, 'error' => 'Invalid session, refresh and try again.'], 400);
|
|
}
|
|
|
|
// Honeypot to deter bots
|
|
if (!empty($_POST['website'])) {
|
|
json_out(['ok' => true]);
|
|
}
|
|
|
|
$last = (int)($_SESSION['contact_last'] ?? 0);
|
|
if ($last && (time() - $last) < 20) {
|
|
json_out(['ok' => false, 'error' => 'Please wait a moment before sending again.'], 429);
|
|
}
|
|
|
|
$name = trim((string)($_POST['name'] ?? ''));
|
|
$email = trim((string)($_POST['email'] ?? ''));
|
|
$message = trim((string)($_POST['message'] ?? ''));
|
|
|
|
if ($name === '' || mb_strlen($name) > 80) {
|
|
json_out(['ok' => false, 'error' => 'Invalid name.'], 400);
|
|
}
|
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || mb_strlen($email) > 120) {
|
|
json_out(['ok' => false, 'error' => 'Invalid email.'], 400);
|
|
}
|
|
if ($message === '' || mb_strlen($message) > 2000) {
|
|
json_out(['ok' => false, 'error' => 'Invalid message length.'], 400);
|
|
}
|
|
|
|
$name = str_replace(["\r", "\n"], ' ', $name);
|
|
$email = str_replace(["\r", "\n"], '', $email);
|
|
|
|
// Ensure table exists
|
|
try {
|
|
pdo()->exec("
|
|
CREATE TABLE IF NOT EXISTS contact_requests (
|
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
name VARCHAR(80) NOT NULL,
|
|
email VARCHAR(120) NOT NULL,
|
|
message TEXT NOT NULL,
|
|
status ENUM('new','read','archived') NOT NULL DEFAULT 'new',
|
|
created_at DATETIME NOT NULL,
|
|
ip VARCHAR(45) NULL,
|
|
user_agent VARCHAR(255) NULL
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
");
|
|
} catch (Throwable $e) {
|
|
json_out(['ok' => false, 'error' => 'Server error (contact table).'], 500);
|
|
}
|
|
|
|
$ipRaw = (string)($_SERVER['REMOTE_ADDR'] ?? '');
|
|
$ip = substr($ipRaw !== '' ? $ipRaw : '127.0.0.1', 0, 45);
|
|
$ua = substr((string)($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255);
|
|
$now = date('Y-m-d H:i:s');
|
|
$id = null;
|
|
|
|
try {
|
|
$st = pdo()->prepare("INSERT INTO contact_requests (name, email, message, status, created_at, ip, user_agent)
|
|
VALUES (?, ?, ?, 'new', ?, ?, ?)");
|
|
$st->execute([$name, $email, $message, $now, $ip, $ua]);
|
|
$id = (int)pdo()->lastInsertId();
|
|
} catch (Throwable $e) {
|
|
json_out(['ok' => false, 'error' => 'Server error (save failed).'], 500);
|
|
}
|
|
|
|
// Optional email notification if configured
|
|
$delivered = false;
|
|
$to = getenv('CONTACT_TO') ?: '';
|
|
if ($to !== '') {
|
|
$subject = "Portfolio contact from {$name}";
|
|
$body =
|
|
"Name: {$name}\n" .
|
|
"Email: {$email}\n\n" .
|
|
"Message:\n{$message}\n";
|
|
|
|
$headers = [
|
|
'From: Portfolio <no-reply@' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . '>',
|
|
'Reply-To: ' . $email,
|
|
'Content-Type: text/plain; charset=utf-8'
|
|
];
|
|
|
|
$delivered = @mail($to, $subject, $body, implode("\r\n", $headers)) === true;
|
|
}
|
|
|
|
$_SESSION['contact_last'] = time();
|
|
|
|
json_out([
|
|
'ok' => true,
|
|
'id' => $id,
|
|
'delivered' => $delivered,
|
|
'ip' => $ip,
|
|
]);
|