633 lines
24 KiB
PHP
633 lines
24 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* SoftSPM - obsługa formularzy strony.
|
|
*
|
|
* Pośrednik między formularzami a api.softspm.pl/send_email_api.php.
|
|
* Klucz API zostaje na serwerze; przeglądarka nigdy go nie widzi. Adresat
|
|
* jest stały (config.php), więc endpointu nie da się użyć do wysyłki
|
|
* dowolnej wiadomości pod dowolny adres.
|
|
*
|
|
* Tryby pracy:
|
|
* GET ?action=token&form=<id> -> wydaje podpisany token formularza
|
|
* GET ?action=confirm&form=<id>&token=.. -> potwierdza przesunięcie suwaka
|
|
* POST (JSON) -> waliduje i wysyła zgłoszenie
|
|
*/
|
|
|
|
$config = require __DIR__ . '/config.php';
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header('X-Content-Type-Options: nosniff');
|
|
header('Cache-Control: no-store');
|
|
header('Referrer-Policy: same-origin');
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Definicje formularzy. Serwer przyjmuje wyłącznie te pola - cokolwiek innego
|
|
// przyjdzie w JSON-ie, jest ignorowane.
|
|
// ---------------------------------------------------------------------------
|
|
function spm_forms(): array
|
|
{
|
|
return [
|
|
'newsletter' => [
|
|
'lang' => 'pl',
|
|
'label' => 'Newsletter',
|
|
'subject' => 'Zapis do newslettera',
|
|
'fields' => [
|
|
'email' => ['label' => 'E-mail', 'type' => 'email', 'required' => true, 'max' => 190],
|
|
],
|
|
],
|
|
'newsletter-en' => [
|
|
'lang' => 'en',
|
|
'label' => 'Newsletter (EN)',
|
|
'subject' => 'Kontakt z wersji angielskiej',
|
|
'fields' => [
|
|
'email' => ['label' => 'E-mail', 'type' => 'email', 'required' => true, 'max' => 190],
|
|
],
|
|
],
|
|
'szkolenia' => [
|
|
'lang' => 'pl',
|
|
'label' => 'Zgłoszenie - szkolenia i webinaria',
|
|
'subject' => 'Zapytanie o szkolenie',
|
|
'fields' => [
|
|
'imie' => ['label' => 'Imię i nazwisko', 'type' => 'text', 'required' => true, 'max' => 120],
|
|
'firma' => ['label' => 'Przedsiębiorstwo', 'type' => 'text', 'required' => true, 'max' => 160],
|
|
'email' => ['label' => 'E-mail', 'type' => 'email', 'required' => true, 'max' => 190],
|
|
'telefon' => ['label' => 'Telefon', 'type' => 'text', 'required' => false, 'max' => 40],
|
|
'temat' => ['label' => 'Interesujący temat', 'type' => 'text', 'required' => false, 'max' => 160],
|
|
'wiadomosc' => ['label' => 'Wiadomość', 'type' => 'textarea', 'required' => false, 'max' => 4000],
|
|
],
|
|
],
|
|
'konsulting' => [
|
|
'lang' => 'pl',
|
|
'label' => 'Zgłoszenie - konsulting',
|
|
'subject' => 'Prośba o rozmowę wstępną',
|
|
'fields' => [
|
|
'imie' => ['label' => 'Imię i nazwisko', 'type' => 'text', 'required' => true, 'max' => 120],
|
|
'firma' => ['label' => 'Przedsiębiorstwo', 'type' => 'text', 'required' => true, 'max' => 160],
|
|
'email' => ['label' => 'E-mail', 'type' => 'email', 'required' => true, 'max' => 190],
|
|
'telefon' => ['label' => 'Telefon', 'type' => 'text', 'required' => false, 'max' => 40],
|
|
'zakres' => ['label' => 'Interesujący zakres', 'type' => 'text', 'required' => false, 'max' => 160],
|
|
'wiadomosc' => ['label' => 'Opis sytuacji', 'type' => 'textarea', 'required' => false, 'max' => 4000],
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
// Treść zgody utrwalana razem ze zgłoszeniem - dowód, na co konkretnie
|
|
// zgodził się nadawca w dniu wysyłki.
|
|
function spm_consent_text(string $lang): string
|
|
{
|
|
if ($lang === 'en') {
|
|
return 'I consent to SoftSPM processing my personal data in order to be contacted '
|
|
. 'about commercial and marketing matters, including offers of training, '
|
|
. 'consulting and software. I may withdraw this consent at any time '
|
|
. 'by writing to info@softspm.com.';
|
|
}
|
|
|
|
return 'Wyrażam zgodę na przetwarzanie moich danych osobowych przez SoftSPM w celu '
|
|
. 'kontaktu handlowego i marketingowego, w tym przedstawienia oferty szkoleń, '
|
|
. 'konsultingu i aplikacji. Zgodę mogę wycofać w każdej chwili, pisząc na '
|
|
. 'info@softspm.com.';
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Narzędzia
|
|
// ---------------------------------------------------------------------------
|
|
function spm_respond(int $status, array $payload): void
|
|
{
|
|
http_response_code($status);
|
|
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
exit;
|
|
}
|
|
|
|
function spm_fail(int $status, string $code, string $message): void
|
|
{
|
|
spm_respond($status, ['ok' => false, 'error' => $code, 'message' => $message]);
|
|
}
|
|
|
|
function spm_b64u(string $bin): string
|
|
{
|
|
return rtrim(strtr(base64_encode($bin), '+/', '-_'), '=');
|
|
}
|
|
|
|
function spm_sign(string $payload, string $secret): string
|
|
{
|
|
return spm_b64u(hash_hmac('sha256', $payload, $secret, true));
|
|
}
|
|
|
|
/** Token ma postać: rodzaj.formularz.nonce.czas.podpis */
|
|
function spm_issue_token(string $kind, string $form, string $nonce, string $secret): string
|
|
{
|
|
$payload = $kind . '.' . $form . '.' . $nonce . '.' . time();
|
|
|
|
return $payload . '.' . spm_sign($payload, $secret);
|
|
}
|
|
|
|
/** @return array{nonce:string, ts:int}|null */
|
|
function spm_read_token(string $token, string $kind, string $form, string $secret): ?array
|
|
{
|
|
$parts = explode('.', $token);
|
|
if (count($parts) !== 5) {
|
|
return null;
|
|
}
|
|
[$gotKind, $gotForm, $nonce, $ts, $sig] = $parts;
|
|
if ($gotKind !== $kind || $gotForm !== $form || !ctype_digit($ts)) {
|
|
return null;
|
|
}
|
|
if (preg_match('/^[A-Za-z0-9_-]{8,64}$/', $nonce) !== 1) {
|
|
return null;
|
|
}
|
|
$payload = $gotKind . '.' . $gotForm . '.' . $nonce . '.' . $ts;
|
|
if (!hash_equals(spm_sign($payload, $secret), $sig)) {
|
|
return null;
|
|
}
|
|
|
|
return ['nonce' => $nonce, 'ts' => (int) $ts];
|
|
}
|
|
|
|
function spm_storage(array $config, string $sub): string
|
|
{
|
|
$dir = rtrim($config['storage_dir'], '/\\') . '/' . $sub;
|
|
if (!is_dir($dir)) {
|
|
@mkdir($dir, 0770, true);
|
|
}
|
|
// Katalog roboczy nie ma prawa być czytany przez przeglądarkę.
|
|
$guard = rtrim($config['storage_dir'], '/\\') . '/.htaccess';
|
|
if (!is_file($guard)) {
|
|
@file_put_contents($guard, "Require all denied\n<IfModule !mod_authz_core.c>\nDeny from all\n</IfModule>\n");
|
|
}
|
|
|
|
return $dir;
|
|
}
|
|
|
|
/** Jednorazowość tokenu: nonce raz zużyty nie przechodzi drugi raz. */
|
|
function spm_nonce_used(array $config, string $nonce): bool
|
|
{
|
|
$dir = spm_storage($config, 'nonce');
|
|
$file = $dir . '/' . hash('sha256', $nonce) . '.used';
|
|
if (is_file($file)) {
|
|
return true;
|
|
}
|
|
@file_put_contents($file, (string) time());
|
|
|
|
// Sprzątanie: co jakiś czas kasujemy wpisy starsze niż czas życia tokenu.
|
|
if (random_int(1, 25) === 1) {
|
|
$limit = time() - (int) $config['max_token_age'] - 3600;
|
|
foreach ((glob($dir . '/*.used') ?: []) as $old) {
|
|
if (@filemtime($old) < $limit) {
|
|
@unlink($old);
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function spm_client_ip(): string
|
|
{
|
|
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
|
|
|
return is_string($ip) ? $ip : '0.0.0.0';
|
|
}
|
|
|
|
/** Limit zgłoszeń z jednego adresu IP - godzinowy i dobowy. */
|
|
function spm_rate_limited(array $config, string $ip): bool
|
|
{
|
|
$dir = spm_storage($config, 'rate');
|
|
$file = $dir . '/' . hash('sha256', $ip . '|' . $config['token_secret']) . '.json';
|
|
$now = time();
|
|
|
|
$stamps = [];
|
|
if (is_file($file)) {
|
|
$raw = @file_get_contents($file);
|
|
if (is_string($raw) && $raw !== '') {
|
|
$decoded = json_decode($raw, true);
|
|
if (is_array($decoded)) {
|
|
$stamps = array_values(array_filter($decoded, static fn($t) => is_int($t) && $t > $now - 86400));
|
|
}
|
|
}
|
|
}
|
|
|
|
$lastHour = count(array_filter($stamps, static fn(int $t) => $t > $now - 3600));
|
|
if ($lastHour >= (int) $config['rate_limit']['per_hour'] || count($stamps) >= (int) $config['rate_limit']['per_day']) {
|
|
return true;
|
|
}
|
|
|
|
$stamps[] = $now;
|
|
@file_put_contents($file, json_encode($stamps), LOCK_EX);
|
|
|
|
return false;
|
|
}
|
|
|
|
/** Zgłoszenie musi pochodzić ze strony SoftSPM, nie z obcego skryptu. */
|
|
function spm_origin_ok(array $config): bool
|
|
{
|
|
$candidates = [];
|
|
foreach (['HTTP_ORIGIN', 'HTTP_REFERER'] as $header) {
|
|
if (!empty($_SERVER[$header]) && is_string($_SERVER[$header])) {
|
|
$host = parse_url($_SERVER[$header], PHP_URL_HOST);
|
|
if (is_string($host) && $host !== '') {
|
|
$candidates[] = strtolower($host);
|
|
}
|
|
}
|
|
}
|
|
if ($candidates === []) {
|
|
return false;
|
|
}
|
|
$allowed = array_map('strtolower', (array) $config['allowed_hosts']);
|
|
foreach ($candidates as $host) {
|
|
if (in_array($host, $allowed, true)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function spm_log(array $config, string $line): void
|
|
{
|
|
$dir = spm_storage($config, 'log');
|
|
@file_put_contents(
|
|
$dir . '/' . gmdate('Y-m') . '.log',
|
|
gmdate('c') . ' ' . $line . "\n",
|
|
FILE_APPEND | LOCK_EX
|
|
);
|
|
}
|
|
|
|
/** Pojedyncza linia tekstu: bez znaków sterujących, bez łamania wiersza. */
|
|
function spm_clean_line(string $value): string
|
|
{
|
|
$value = str_replace(["\r", "\n", "\t"], ' ', $value);
|
|
$value = preg_replace('/[\x00-\x1F\x7F]/u', '', $value) ?? '';
|
|
|
|
return trim(preg_replace('/\s{2,}/u', ' ', $value) ?? '');
|
|
}
|
|
|
|
function spm_clean_text(string $value): string
|
|
{
|
|
$value = str_replace("\r\n", "\n", $value);
|
|
$value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $value) ?? '';
|
|
|
|
return trim($value);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Start obsługi żądania
|
|
// ---------------------------------------------------------------------------
|
|
$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
|
|
|
|
if ($method === 'OPTIONS') {
|
|
http_response_code(204);
|
|
exit;
|
|
}
|
|
|
|
if ($config['token_secret'] === '' || $config['api_key'] === '') {
|
|
spm_log($config, 'config-missing');
|
|
spm_fail(503, 'not_configured', 'Formularz nie jest jeszcze skonfigurowany. Napisz na info@softspm.com.');
|
|
}
|
|
|
|
$forms = spm_forms();
|
|
|
|
// --- GET: wydanie tokenu i potwierdzenie suwaka ----------------------------
|
|
if ($method === 'GET') {
|
|
$action = isset($_GET['action']) && is_string($_GET['action']) ? $_GET['action'] : '';
|
|
$form = isset($_GET['form']) && is_string($_GET['form']) ? $_GET['form'] : '';
|
|
|
|
if (!isset($forms[$form])) {
|
|
spm_fail(400, 'unknown_form', 'Nieznany formularz.');
|
|
}
|
|
if (!spm_origin_ok($config)) {
|
|
spm_fail(403, 'bad_origin', 'Żądanie spoza strony SoftSPM.');
|
|
}
|
|
|
|
if ($action === 'token') {
|
|
$nonce = spm_b64u(random_bytes(12));
|
|
spm_respond(200, [
|
|
'ok' => true,
|
|
'token' => spm_issue_token('t', $form, $nonce, $config['token_secret']),
|
|
]);
|
|
}
|
|
|
|
if ($action === 'confirm') {
|
|
$token = isset($_GET['token']) && is_string($_GET['token']) ? $_GET['token'] : '';
|
|
$data = spm_read_token($token, 't', $form, $config['token_secret']);
|
|
if ($data === null) {
|
|
spm_fail(400, 'bad_token', 'Token formularza jest nieprawidłowy. Odśwież stronę.');
|
|
}
|
|
if (time() - $data['ts'] > (int) $config['max_token_age']) {
|
|
spm_fail(400, 'expired', 'Formularz był otwarty zbyt długo. Odśwież stronę.');
|
|
}
|
|
spm_respond(200, [
|
|
'ok' => true,
|
|
'proof' => spm_issue_token('c', $form, $data['nonce'], $config['token_secret']),
|
|
]);
|
|
}
|
|
|
|
spm_fail(400, 'bad_action', 'Nieznana operacja.');
|
|
}
|
|
|
|
if ($method !== 'POST') {
|
|
spm_fail(405, 'method_not_allowed', 'Dozwolone są wyłącznie żądania POST.');
|
|
}
|
|
|
|
// --- POST: właściwa wysyłka -------------------------------------------------
|
|
|
|
// Formularz wysłany bez JavaScriptu leci tu jako zwykły POST przeglądarki.
|
|
// Zamiast JSON-a z błędem pokazujemy stronę, którą da się przeczytać.
|
|
$contentType = strtolower((string) ($_SERVER['CONTENT_TYPE'] ?? ''));
|
|
if (strpos($contentType, 'application/json') === false) {
|
|
http_response_code(400);
|
|
header('Content-Type: text/html; charset=utf-8');
|
|
echo '<!doctype html><html lang="pl"><head><meta charset="utf-8">'
|
|
. '<title>Formularz SoftSPM</title></head><body style="font:16px/1.5 system-ui,sans-serif;padding:2rem;max-width:40rem">'
|
|
. '<h1 style="font-size:1.25rem">Formularz wymaga włączonego JavaScriptu</h1>'
|
|
. '<p>Napisz do nas bezpośrednio na <a href="mailto:info@softspm.com">info@softspm.com</a>. '
|
|
. 'Przepraszamy za kłopot.</p>'
|
|
. '<p><a href="javascript:history.back()">Wróć do formularza</a></p>'
|
|
. '</body></html>';
|
|
exit;
|
|
}
|
|
|
|
$raw = file_get_contents('php://input');
|
|
if (!is_string($raw) || trim($raw) === '') {
|
|
spm_fail(400, 'empty_payload', 'Puste żądanie.');
|
|
}
|
|
|
|
$input = json_decode($raw, true);
|
|
if (!is_array($input)) {
|
|
spm_fail(400, 'invalid_json', 'Nieprawidłowy format danych.');
|
|
}
|
|
|
|
$form = isset($input['form']) && is_string($input['form']) ? $input['form'] : '';
|
|
if (!isset($forms[$form])) {
|
|
spm_fail(400, 'unknown_form', 'Nieznany formularz.');
|
|
}
|
|
$def = $forms[$form];
|
|
$lang = $def['lang'];
|
|
$isEn = $lang === 'en';
|
|
|
|
if (!spm_origin_ok($config)) {
|
|
spm_log($config, 'reject origin form=' . $form);
|
|
spm_fail(403, 'bad_origin', $isEn ? 'Request did not come from the SoftSPM site.' : 'Żądanie spoza strony SoftSPM.');
|
|
}
|
|
|
|
// Pułapka na boty: pole ukryte przed człowiekiem. Wypełnione = automat.
|
|
// Odpowiadamy sukcesem, żeby nie podpowiadać botowi, co go zdradziło.
|
|
$trap = isset($input['firma_www']) && is_string($input['firma_www']) ? trim($input['firma_www']) : '';
|
|
if ($trap !== '') {
|
|
spm_log($config, 'trap form=' . $form . ' ip=' . hash('sha256', spm_client_ip()));
|
|
spm_respond(200, ['ok' => true, 'message' => $isEn ? 'Message sent.' : 'Wiadomość wysłana.']);
|
|
}
|
|
|
|
// Token formularza + potwierdzenie suwakiem.
|
|
$token = isset($input['token']) && is_string($input['token']) ? $input['token'] : '';
|
|
$proof = isset($input['proof']) && is_string($input['proof']) ? $input['proof'] : '';
|
|
$tokenData = spm_read_token($token, 't', $form, $config['token_secret']);
|
|
$proofData = spm_read_token($proof, 'c', $form, $config['token_secret']);
|
|
|
|
if ($tokenData === null || $proofData === null || !hash_equals($tokenData['nonce'], $proofData['nonce'])) {
|
|
spm_log($config, 'reject token form=' . $form);
|
|
spm_fail(400, 'bad_token', $isEn
|
|
? 'Verification failed. Refresh the page and slide the control again.'
|
|
: 'Weryfikacja nie powiodła się. Odśwież stronę i przesuń suwak ponownie.');
|
|
}
|
|
|
|
$now = time();
|
|
if ($now - $tokenData['ts'] > (int) $config['max_token_age'] || $now - $proofData['ts'] > (int) $config['max_proof_age']) {
|
|
spm_fail(400, 'expired', $isEn
|
|
? 'The form has expired. Refresh the page and try again.'
|
|
: 'Formularz stracił ważność. Odśwież stronę i spróbuj ponownie.');
|
|
}
|
|
$minSlide = (int) $config['min_slide_seconds'];
|
|
if ($now - $tokenData['ts'] < (int) $config['min_fill_seconds']
|
|
|| $proofData['ts'] < $tokenData['ts']
|
|
|| ($minSlide > 0 && $proofData['ts'] - $tokenData['ts'] < $minSlide)) {
|
|
spm_log($config, 'reject speed form=' . $form);
|
|
spm_fail(429, 'too_fast', $isEn
|
|
? 'That was too quick. Try again in a moment.'
|
|
: 'Zgłoszenie wysłano zbyt szybko. Spróbuj ponownie za chwilę.');
|
|
}
|
|
if (spm_nonce_used($config, $tokenData['nonce'])) {
|
|
spm_fail(409, 'already_sent', $isEn
|
|
? 'This form has already been sent. Refresh the page to send another message.'
|
|
: 'To zgłoszenie zostało już wysłane. Odśwież stronę, aby wysłać kolejne.');
|
|
}
|
|
|
|
// Zgoda marketingowa - wymagana, sprawdzana także po stronie serwera.
|
|
$consent = $input['zgoda'] ?? false;
|
|
if ($consent !== true && $consent !== 'true' && $consent !== 1 && $consent !== '1' && $consent !== 'on') {
|
|
spm_fail(400, 'consent_required', $isEn
|
|
? 'Consent to be contacted is required.'
|
|
: 'Zgoda na kontakt jest wymagana.');
|
|
}
|
|
|
|
if (spm_rate_limited($config, spm_client_ip())) {
|
|
spm_log($config, 'reject rate form=' . $form . ' ip=' . hash('sha256', spm_client_ip()));
|
|
spm_fail(429, 'rate_limited', $isEn
|
|
? 'Too many messages from this address. Please write to info@softspm.com.'
|
|
: 'Zbyt wiele zgłoszeń z tego adresu. Napisz na info@softspm.com.');
|
|
}
|
|
|
|
// --- Walidacja pól ----------------------------------------------------------
|
|
$values = [];
|
|
$replyTo = '';
|
|
foreach ($def['fields'] as $name => $rule) {
|
|
$value = $input[$name] ?? '';
|
|
if (!is_string($value)) {
|
|
$value = '';
|
|
}
|
|
$value = $rule['type'] === 'textarea' ? spm_clean_text($value) : spm_clean_line($value);
|
|
|
|
if ($value === '') {
|
|
if (!empty($rule['required'])) {
|
|
spm_fail(400, 'missing_field', $isEn
|
|
? sprintf('Field "%s" is required.', $rule['label'])
|
|
: sprintf('Pole „%s" jest wymagane.', $rule['label']));
|
|
}
|
|
$values[$name] = '';
|
|
continue;
|
|
}
|
|
|
|
if (mb_strlen($value) > (int) $rule['max']) {
|
|
spm_fail(400, 'field_too_long', $isEn
|
|
? sprintf('Field "%s" is too long.', $rule['label'])
|
|
: sprintf('Pole „%s" jest za długie.', $rule['label']));
|
|
}
|
|
|
|
if ($rule['type'] === 'email') {
|
|
if (filter_var($value, FILTER_VALIDATE_EMAIL) === false) {
|
|
spm_fail(400, 'invalid_email', $isEn
|
|
? 'The e-mail address is not valid.'
|
|
: 'Adres e-mail jest nieprawidłowy.');
|
|
}
|
|
$replyTo = $value;
|
|
}
|
|
|
|
$values[$name] = $value;
|
|
}
|
|
|
|
// --- Budowa wiadomości ------------------------------------------------------
|
|
// UWAGA: send_email_api.php sanityzuje treść przed osadzeniem jej we własnym
|
|
// szablonie HTML (manual, punkt 7). Znaczniki wysłane stąd dotarłyby do skrzynki
|
|
// jako widoczny tekst "<p style=...>", więc wysyłamy czysty tekst.
|
|
//
|
|
// Nie wiemy, czy szablon po drugiej stronie zamienia znaki nowej linii na <br>,
|
|
// dlatego układ jest odporny na oba warianty: każda pozycja to osobna linia
|
|
// "Etykieta: wartość", która pozostaje czytelna nawet po sklejeniu w akapit.
|
|
$topic = '';
|
|
foreach (['temat', 'zakres'] as $key) {
|
|
if (!empty($values[$key])) {
|
|
$topic = $values[$key];
|
|
break;
|
|
}
|
|
}
|
|
|
|
$subject = trim($config['subject_prefix'] . ' ' . $def['subject']);
|
|
if ($topic !== '') {
|
|
$subject .= ' - ' . $topic;
|
|
}
|
|
if (mb_strlen($subject) > 250) {
|
|
$subject = mb_substr($subject, 0, 247) . '...';
|
|
}
|
|
|
|
$lines = [];
|
|
$lines[] = $isEn ? 'New message from softspm.com' : 'Nowe zgłoszenie ze strony softspm.com';
|
|
$lines[] = ($isEn ? 'Form: ' : 'Formularz: ') . $def['label'];
|
|
if ($replyTo !== '') {
|
|
$lines[] = ($isEn ? 'Reply to: ' : 'Odpowiedz na: ') . $replyTo;
|
|
}
|
|
$lines[] = '';
|
|
$lines[] = $isEn ? '--- DETAILS ---' : '--- DANE Z FORMULARZA ---';
|
|
|
|
foreach ($def['fields'] as $name => $rule) {
|
|
$value = $values[$name] ?? '';
|
|
if ($value === '') {
|
|
continue;
|
|
}
|
|
if ($rule['type'] === 'textarea') {
|
|
// Treść wieloliniowa dostaje własny blok, każdy wiersz z wcięciem,
|
|
// żeby po sklejeniu akapitu dało się odróżnić ją od pozostałych pól.
|
|
$lines[] = $rule['label'] . ':';
|
|
foreach (explode("\n", $value) as $row) {
|
|
$lines[] = ' ' . $row;
|
|
}
|
|
continue;
|
|
}
|
|
$lines[] = $rule['label'] . ': ' . $value;
|
|
}
|
|
|
|
$lines[] = '';
|
|
$lines[] = $isEn ? '--- CONSENT GIVEN ON SUBMISSION ---' : '--- ZGODA UDZIELONA PRZY WYSYŁCE ---';
|
|
$lines[] = spm_consent_text($lang);
|
|
|
|
$lines[] = '';
|
|
$lines[] = $isEn ? '--- METADATA ---' : '--- METRYKA ---';
|
|
$lines[] = ($isEn ? 'Page: ' : 'Strona: ')
|
|
. (isset($input['page']) && is_string($input['page']) ? spm_clean_line(mb_substr($input['page'], 0, 300)) : '-');
|
|
$lines[] = ($isEn ? 'Date (UTC): ' : 'Data (UTC): ') . gmdate('Y-m-d H:i:s');
|
|
$lines[] = ($isEn ? 'IP address: ' : 'Adres IP: ') . spm_client_ip();
|
|
|
|
$message = implode("\n", $lines);
|
|
|
|
// Zostawiamy zapas na szablon po stronie API (limit endpointu to 20000 znaków).
|
|
if (mb_strlen($message) > 15000) {
|
|
$message = mb_substr($message, 0, 15000) . "\n[...]";
|
|
}
|
|
|
|
// --- Wysyłka przez api.softspm.pl ------------------------------------------
|
|
// Adresat pochodzi wyłącznie z konfiguracji serwera. Formularz może mieć
|
|
// własną skrzynkę (config['recipients']), inaczej idzie na adres domyślny.
|
|
$recipient = (string) $config['recipient'];
|
|
if (isset($config['recipients'][$form]) && is_string($config['recipients'][$form]) && $config['recipients'][$form] !== '') {
|
|
$recipient = $config['recipients'][$form];
|
|
}
|
|
if (filter_var($recipient, FILTER_VALIDATE_EMAIL) === false) {
|
|
spm_log($config, 'config-bad-recipient form=' . $form);
|
|
spm_fail(503, 'not_configured', $isEn
|
|
? 'The form is not configured yet. Please write to info@softspm.com.'
|
|
: 'Formularz nie jest jeszcze skonfigurowany. Napisz na info@softspm.com.');
|
|
}
|
|
|
|
$payload = json_encode([
|
|
'to' => $recipient,
|
|
'subject' => $subject,
|
|
'message' => $message,
|
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
|
|
if ($payload === false) {
|
|
spm_fail(500, 'encode_failed', $isEn ? 'Could not prepare the message.' : 'Nie udało się przygotować wiadomości.');
|
|
}
|
|
|
|
$headers = [
|
|
'Content-Type: application/json',
|
|
'Authorization: Bearer ' . $config['api_key'],
|
|
];
|
|
|
|
$status = 0;
|
|
$response = '';
|
|
$netError = '';
|
|
|
|
if (function_exists('curl_init')) {
|
|
$ch = curl_init($config['api_url']);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => $payload,
|
|
CURLOPT_HTTPHEADER => $headers,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_CONNECTTIMEOUT => 8,
|
|
CURLOPT_TIMEOUT => (int) $config['timeout'],
|
|
]);
|
|
$body = curl_exec($ch);
|
|
if ($body === false) {
|
|
$netError = curl_error($ch);
|
|
} else {
|
|
$response = (string) $body;
|
|
}
|
|
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
|
curl_close($ch);
|
|
} else {
|
|
$context = stream_context_create([
|
|
'http' => [
|
|
'method' => 'POST',
|
|
'header' => implode("\r\n", $headers),
|
|
'content' => $payload,
|
|
'timeout' => (int) $config['timeout'],
|
|
'ignore_errors' => true,
|
|
],
|
|
]);
|
|
$body = @file_get_contents($config['api_url'], false, $context);
|
|
if ($body === false) {
|
|
$netError = 'stream request failed';
|
|
} else {
|
|
$response = (string) $body;
|
|
foreach ($http_response_header ?? [] as $line) {
|
|
if (preg_match('#^HTTP/\S+\s+(\d{3})#', $line, $m) === 1) {
|
|
$status = (int) $m[1];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($status !== 200) {
|
|
// Szczegóły zostają w logu serwera, klient dostaje komunikat bez detali.
|
|
spm_log($config, sprintf(
|
|
'send-failed form=%s http=%d err=%s resp=%s',
|
|
$form,
|
|
$status,
|
|
$netError !== '' ? $netError : '-',
|
|
substr($response, 0, 300)
|
|
));
|
|
spm_fail(502, 'send_failed', $isEn
|
|
? 'The message could not be sent. Please write to info@softspm.com.'
|
|
: 'Nie udało się wysłać wiadomości. Napisz na info@softspm.com.');
|
|
}
|
|
|
|
spm_log($config, 'sent form=' . $form . ' ip=' . hash('sha256', spm_client_ip()));
|
|
spm_respond(200, [
|
|
'ok' => true,
|
|
'message' => $isEn
|
|
? 'Thank you - your message has been sent. We reply within one working day.'
|
|
: 'Dziękujemy - wiadomość została wysłana. Odpowiadamy w ciągu jednego dnia roboczego.',
|
|
]);
|