207 lines
6.6 KiB
PHP
207 lines
6.6 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
header("Content-Type: application/json; charset=UTF-8");
|
|
|
|
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
|
|
http_response_code(405);
|
|
echo json_encode([
|
|
"ok" => false,
|
|
"message" => "Метод не поддерживается",
|
|
], JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
|
|
function respondError(string $message, int $code = 400): void
|
|
{
|
|
http_response_code($code);
|
|
echo json_encode([
|
|
"ok" => false,
|
|
"message" => $message,
|
|
], JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
|
|
function sanitizeInput(string $value): string
|
|
{
|
|
$clean = trim($value);
|
|
return preg_replace("/[\\x00-\\x1F\\x7F]/u", "", $clean) ?? "";
|
|
}
|
|
|
|
function respondSuccess(): void
|
|
{
|
|
echo json_encode([
|
|
"ok" => true,
|
|
], JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
}
|
|
|
|
function loadRateLimitState(string $filePath): array
|
|
{
|
|
if (!is_file($filePath)) {
|
|
return [];
|
|
}
|
|
$raw = file_get_contents($filePath);
|
|
if ($raw === false || $raw === "") {
|
|
return [];
|
|
}
|
|
$data = json_decode($raw, true);
|
|
return is_array($data) ? $data : [];
|
|
}
|
|
|
|
function storeRateLimitState(string $filePath, array $state): void
|
|
{
|
|
$encoded = json_encode($state, JSON_UNESCAPED_UNICODE);
|
|
if ($encoded === false) {
|
|
return;
|
|
}
|
|
@file_put_contents($filePath, $encoded, LOCK_EX);
|
|
}
|
|
|
|
function isRateLimited(string $ip, int $nowTs): bool
|
|
{
|
|
$limitWindowSeconds = 10 * 60; // 10 минут
|
|
$limitAttempts = 8;
|
|
$rateFile = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . "frieze_spb_rate_limit.json";
|
|
|
|
$state = loadRateLimitState($rateFile);
|
|
$freshState = [];
|
|
|
|
foreach ($state as $storedIp => $timestamps) {
|
|
if (!is_array($timestamps)) {
|
|
continue;
|
|
}
|
|
$validTs = [];
|
|
foreach ($timestamps as $ts) {
|
|
$intTs = (int)$ts;
|
|
if ($intTs >= $nowTs - $limitWindowSeconds) {
|
|
$validTs[] = $intTs;
|
|
}
|
|
}
|
|
if ($validTs) {
|
|
$freshState[$storedIp] = $validTs;
|
|
}
|
|
}
|
|
|
|
$currentIpHits = $freshState[$ip] ?? [];
|
|
if (count($currentIpHits) >= $limitAttempts) {
|
|
storeRateLimitState($rateFile, $freshState);
|
|
return true;
|
|
}
|
|
|
|
$currentIpHits[] = $nowTs;
|
|
$freshState[$ip] = $currentIpHits;
|
|
storeRateLimitState($rateFile, $freshState);
|
|
return false;
|
|
}
|
|
|
|
$name = sanitizeInput((string)($_POST["name"] ?? ""));
|
|
$phoneRaw = sanitizeInput((string)($_POST["phone"] ?? ""));
|
|
$consent = (string)($_POST["consent"] ?? "");
|
|
$honeypot = sanitizeInput((string)($_POST["contact_fax"] ?? ($_POST["website"] ?? "")));
|
|
$startedAtRaw = sanitizeInput((string)($_POST["started_at"] ?? ""));
|
|
|
|
if ($honeypot !== "") {
|
|
respondError("Не удалось отправить заявку. Пожалуйста, попробуйте еще раз.", 400);
|
|
}
|
|
|
|
$nowTs = time();
|
|
$formMinFillSeconds = 0.4;
|
|
$startedAtMs = (int)$startedAtRaw;
|
|
if ($startedAtMs > 0) {
|
|
$elapsedSeconds = ((int)floor(microtime(true) * 1000) - $startedAtMs) / 1000;
|
|
if ($elapsedSeconds < $formMinFillSeconds) {
|
|
respondError("Слишком быстрая отправка формы. Попробуйте еще раз.", 429);
|
|
}
|
|
}
|
|
|
|
$clientIp = (string)($_SERVER["REMOTE_ADDR"] ?? "unknown");
|
|
if (isRateLimited($clientIp, $nowTs)) {
|
|
respondError("Слишком много заявок за короткое время. Повторите попытку позже.", 429);
|
|
}
|
|
|
|
if ($name === "") {
|
|
respondError("Укажите имя");
|
|
}
|
|
|
|
// Нормализуем имя: убираем лишние пробелы внутри.
|
|
$name = preg_replace("/\\s+/u", " ", $name) ?? $name;
|
|
|
|
if (mb_strlen($name) < 2 || mb_strlen($name) > 80) {
|
|
respondError("Имя должно быть от 2 до 80 символов");
|
|
}
|
|
|
|
if (!preg_match("/^[\\p{L}\\s-]+$/u", $name)) {
|
|
respondError("Имя может содержать только буквы, пробелы и дефис");
|
|
}
|
|
|
|
$phoneDigits = preg_replace("/\\D+/", "", $phoneRaw) ?? "";
|
|
if (strlen($phoneDigits) !== 11 || strpos($phoneDigits, "7") !== 0) {
|
|
respondError("Укажите корректный телефон");
|
|
}
|
|
|
|
if ($consent !== "1") {
|
|
respondError("Необходимо согласие на обработку данных");
|
|
}
|
|
|
|
// Укажите email в файле submit-config.php (предпочтительно для REG.RU)
|
|
// или в переменной окружения FORM_RECIPIENT_EMAIL.
|
|
$recipientEmail = "";
|
|
$senderEmail = "";
|
|
$configFile = __DIR__ . "/submit-config.php";
|
|
if (is_file($configFile)) {
|
|
require $configFile;
|
|
}
|
|
|
|
if ($recipientEmail === "") {
|
|
$recipientEmail = getenv("FORM_RECIPIENT_EMAIL") ?: "you@example.com";
|
|
}
|
|
|
|
if (substr($recipientEmail, -11) === "@example.com") {
|
|
respondError("Не настроен email получателя заявок. Укажите адрес в submit-config.php.", 500);
|
|
}
|
|
|
|
$host = strtolower((string)($_SERVER["HTTP_HOST"] ?? "frieze-spb.ru"));
|
|
$host = preg_replace("/:\\d+$/", "", $host) ?? "frieze-spb.ru";
|
|
$host = preg_replace("/[^a-z0-9\\.-]/", "", $host) ?? "frieze-spb.ru";
|
|
if ($host === "") {
|
|
$host = "frieze-spb.ru";
|
|
}
|
|
$defaultSenderEmail = "noreply@" . $host;
|
|
$fromEmail = $senderEmail !== "" ? $senderEmail : $defaultSenderEmail;
|
|
if (!filter_var($fromEmail, FILTER_VALIDATE_EMAIL)) {
|
|
$fromEmail = $defaultSenderEmail;
|
|
}
|
|
|
|
$subject = "Новая заявка с сайта frieze-spb.ru";
|
|
$encodedSubject = "=?UTF-8?B?" . base64_encode($subject) . "?=";
|
|
|
|
$messageLines = [
|
|
"Новая заявка с сайта",
|
|
"",
|
|
"Имя: " . $name,
|
|
"Телефон: " . $phoneRaw,
|
|
"Телефон (цифры): +" . $phoneDigits,
|
|
"Дата/время: " . date("d.m.Y H:i:s"),
|
|
"IP: " . (string)($_SERVER["REMOTE_ADDR"] ?? "не определен"),
|
|
"User-Agent: " . (string)($_SERVER["HTTP_USER_AGENT"] ?? "не определен"),
|
|
];
|
|
|
|
$body = implode("\r\n", $messageLines);
|
|
|
|
$headers = [
|
|
"MIME-Version: 1.0",
|
|
"Content-Type: text/plain; charset=UTF-8",
|
|
"From: Frieze Site <" . $fromEmail . ">",
|
|
"Reply-To: " . $recipientEmail,
|
|
"X-Mailer: PHP/" . PHP_VERSION,
|
|
];
|
|
|
|
$sent = mail($recipientEmail, $encodedSubject, $body, implode("\r\n", $headers), "-f" . $fromEmail);
|
|
|
|
if (!$sent) {
|
|
respondError("Не удалось отправить заявку. Попробуйте позже или позвоните по телефону.", 500);
|
|
}
|
|
|
|
respondSuccess();
|