Files
sag24-website/public/api/contact.php
striker ac0467290c fix(contact.php): use is_readable() instead of file_exists() for config
When contact-config.php exists but is unreadable by PHP-FPM (e.g. owned by
www-data while pool runs as nginx), file_exists() returns true but
require_once throws a fatal error → 500.

Production was hitting this: contact-config.php was 'www-data:www-data 640'
but the sag24 PHP-FPM pool runs as 'nginx'. Form returned HTTP 500 on every
POST including legitimate ones — contact form effectively dead.

Server side: chown nginx:nginx /opt/www/sag24.ru/contact-config.php (already
applied). This commit makes the script defensive against the same scenario
in the future.
2026-05-06 01:27:12 +03:00

174 lines
7.9 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).
// Use is_readable() not file_exists() — PHP-FPM may lack read permission even when file is present.
$configFile = dirname(__DIR__, 2) . '/contact-config.php';
if (is_readable($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]);