Files
sag24-website/public/api/contact.php
striker 4e33aebe55 fix: contact.php — move secrets to include file, HTML email
- Replace hardcoded BOT_TOKEN, CHAT_ID, TURNSTILE_SECRET, smtp_pass
  with require_once from /opt/www/sag24.ru/contact-config.php (outside webroot)
- Convert email from plain text to HTML (text/html Content-Type)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 21:09:30 +03:00

173 lines
7.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://sag24.ru');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
// Load secrets from outside webroot (not in git)
$configFile = dirname(__DIR__, 2) . '/contact-config.php';
if (file_exists($configFile)) {
require_once $configFile;
} else {
// Fallback values for local development (override via contact-config.php on server)
$BOT_TOKEN = getenv('TELEGRAM_BOT_TOKEN') ?: '';
$CHAT_ID = getenv('TELEGRAM_CHAT_ID') ?: '';
$TURNSTILE_SECRET = getenv('TURNSTILE_SECRET_KEY') ?: '';
$smtp_pass = getenv('SMTP_PASS') ?: '';
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
exit;
}
// ─── Rate limiting (5 requests per minute per IP) ────────────────────────────
$ip = $_SERVER['HTTP_X_FORWARDED_FOR']
? explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]
: ($_SERVER['REMOTE_ADDR'] ?? 'unknown');
$ip = trim($ip);
$rate_file = sys_get_temp_dir() . '/rl_' . md5($ip) . '.json';
$now = time();
$window = 60;
$max_req = 5;
$rate_data = file_exists($rate_file) ? json_decode(file_get_contents($rate_file), true) : [];
$rate_data = array_filter($rate_data ?? [], fn($t) => $t > $now - $window);
if (count($rate_data) >= $max_req) {
http_response_code(429);
echo json_encode(['error' => 'Too many requests. Please try again later.']);
exit;
}
$rate_data[] = $now;
file_put_contents($rate_file, json_encode(array_values($rate_data)));
$data = json_decode(file_get_contents('php://input'), true);
if (!$data) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON']);
exit;
}
$name = htmlspecialchars(trim($data['name'] ?? ''), ENT_QUOTES);
$company = htmlspecialchars(trim($data['company'] ?? ''), ENT_QUOTES);
$phone = htmlspecialchars(trim($data['phone'] ?? ''), ENT_QUOTES);
$email = htmlspecialchars(trim($data['email'] ?? ''), ENT_QUOTES);
$message = htmlspecialchars(trim($data['message'] ?? ''), ENT_QUOTES);
$turnstileToken = trim($data['turnstileToken'] ?? '');
if (!$name || !$message) {
http_response_code(400);
echo json_encode(['error' => 'Missing required fields']);
exit;
}
// ─── Validation ───────────────────────────────────────────────────────────────
if ($email && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid email format']);
exit;
}
if (mb_strlen($name) > 100) { http_response_code(400); echo json_encode(['error' => 'Name too long']); exit; }
if (mb_strlen($company) > 200) { http_response_code(400); echo json_encode(['error' => 'Company too long']); exit; }
if (mb_strlen($email) > 254) { http_response_code(400); echo json_encode(['error' => 'Email too long']); exit; }
if (mb_strlen($phone) > 30) { http_response_code(400); echo json_encode(['error' => 'Phone too long']); exit; }
if (mb_strlen($message) > 5000) { http_response_code(400); echo json_encode(['error' => 'Message too long']); exit; }
// ─── Cloudflare Turnstile verification ───────────────────────────────────────
if ($TURNSTILE_SECRET) {
if (!$turnstileToken) {
http_response_code(400);
echo json_encode(['error' => 'Bot verification required.']);
exit;
}
$ch = curl_init('https://challenges.cloudflare.com/turnstile/v0/siteverify');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query(['secret' => $TURNSTILE_SECRET, 'response' => $turnstileToken, 'remoteip' => $ip]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$tsResult = json_decode(curl_exec($ch), true);
curl_close($ch);
if (($tsResult['success'] ?? false) !== true) {
http_response_code(400);
echo json_encode(['error' => 'Bot verification failed. Please try again.']);
exit;
}
}
// ─── Telegram (best-effort, not fatal) ───────────────────────────────────────
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
$referer = $_SERVER['HTTP_REFERER'] ?? '';
$country = $_SERVER['HTTP_CF_IPCOUNTRY'] ?? '';
$text = "🔔 <b>Заявка с sag24.ru</b>\n\n";
$text .= "👤 <b>Имя:</b> {$name}\n";
if ($company) $text .= "🏢 <b>Компания:</b> {$company}\n";
if ($email) $text .= "📧 <b>Email:</b> {$email}\n";
if ($phone) $text .= "📱 <b>Телефон:</b> {$phone}\n";
$text .= "\n💬 <b>Сообщение:</b>\n{$message}\n";
$text .= "\n📊 <b>Метаданные:</b>\n";
$text .= "🌍 IP: {$ip}\n";
if ($country) $text .= "🌐 Страна: {$country}\n";
if ($referer) $text .= "🔗 Источник: {$referer}\n";
$text .= "" . date('d.m.Y H:i', time() + 3 * 3600) . " MSK";
$ch = curl_init("https://tg-relay.it-resheniya-2018.workers.dev/bot{$BOT_TOKEN}/sendMessage");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['chat_id' => $CHAT_ID, 'text' => $text, 'parse_mode' => 'HTML']),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
curl_exec($ch);
curl_close($ch);
// ─── Email via SMTP (noreply@sag24.ru → info@sag24.ru) ───────────────────────
$smtp_host = 'mx.hhivp.com';
$smtp_port = 587;
$smtp_user = 'noreply@sag24.ru';
$mail_to = 'info@sag24.ru';
$subj_enc = "=?UTF-8?B?" . base64_encode("Заявка с sag24.ru от {$name}") . "?=";
$html_body = "<h2>Заявка с sag24.ru</h2>";
$html_body .= "<p><strong>Имя:</strong> {$name}</p>";
if ($company) $html_body .= "<p><strong>Компания:</strong> {$company}</p>";
if ($email) $html_body .= "<p><strong>Email:</strong> <a href=\"mailto:{$email}\">{$email}</a></p>";
if ($phone) $html_body .= "<p><strong>Телефон:</strong> {$phone}</p>";
$msg_html = nl2br($message);
$html_body .= "<p><strong>Сообщение:</strong><br>{$msg_html}</p>";
$html_body .= "<hr><p><small>IP: {$ip}";
if ($country) $html_body .= " | Страна: {$country}";
$html_body .= " | " . date('d.m.Y H:i', time() + 3 * 3600) . " MSK</small></p>";
try {
$smtp = fsockopen($smtp_host, $smtp_port, $errno, $errstr, 10);
if ($smtp) {
fgets($smtp, 512);
fwrite($smtp, "EHLO sag24.ru\r\n"); $caps = '';
while ($line = fgets($smtp, 512)) { $caps .= $line; if ($line[3] == ' ') break; }
fwrite($smtp, "STARTTLS\r\n"); fgets($smtp, 512);
stream_socket_enable_crypto($smtp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
fwrite($smtp, "EHLO sag24.ru\r\n");
while ($line = fgets($smtp, 512)) { if ($line[3] == ' ') break; }
fwrite($smtp, "AUTH LOGIN\r\n"); fgets($smtp, 512);
fwrite($smtp, base64_encode($smtp_user) . "\r\n"); fgets($smtp, 512);
fwrite($smtp, base64_encode($smtp_pass) . "\r\n"); fgets($smtp, 512);
fwrite($smtp, "MAIL FROM:<{$smtp_user}>\r\n"); fgets($smtp, 512);
fwrite($smtp, "RCPT TO:<{$mail_to}>\r\n"); fgets($smtp, 512);
fwrite($smtp, "DATA\r\n"); fgets($smtp, 512);
fwrite($smtp, "From: noreply@sag24.ru\r\nTo: {$mail_to}\r\nSubject: {$subj_enc}\r\n");
fwrite($smtp, "MIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n");
fwrite($smtp, $html_body . "\r\n.\r\n"); fgets($smtp, 512);
fwrite($smtp, "QUIT\r\n");
fclose($smtp);
}
} catch (Throwable $e) {
// email failure is non-fatal
}
echo json_encode(['success' => true]);