Initial commit
This commit is contained in:
+195
@@ -0,0 +1,195 @@
|
||||
# Formularze SoftSPM - konfiguracja i uruchomienie
|
||||
|
||||
Wszystkie formularze strony (newsletter PL, kontakt EN, zgłoszenie na szkolenie,
|
||||
rozmowa konsultingowa) wysyłają dane przez [send.php](send.php), który dopiero
|
||||
woła `https://api.softspm.pl/send_email_api.php` zgodnie z
|
||||
[doc/manual_send_email.html](../doc/manual_send_email.html).
|
||||
|
||||
**Dlaczego przez pośrednika, a nie prosto z przeglądarki:** endpoint autoryzuje
|
||||
się kluczem API w nagłówku `Authorization`. Klucz wysłany z JavaScriptu byłby
|
||||
widoczny w źródle strony dla każdego, kto naciśnie Ctrl+U - i każdy mógłby nim
|
||||
wysyłać maile na dowolny adres. Klucz zostaje na serwerze; przeglądarka rozmawia
|
||||
wyłącznie z `send.php`.
|
||||
|
||||
Wymagania: **PHP 7.4 lub nowszy**, rozszerzenie `curl` (jest fallback na
|
||||
`file_get_contents`, ale cURL jest zalecany).
|
||||
|
||||
---
|
||||
|
||||
## 1. Co dopisać w `/etc/hydronexis/config.php`
|
||||
|
||||
`api/config.php` czyta ten plik automatycznie. Obsługiwane są dwa zapisy -
|
||||
wybierz ten, który pasuje do tego, co już masz w pliku.
|
||||
|
||||
### Wariant A - plik definiuje stałe (najczęstszy)
|
||||
|
||||
```php
|
||||
// --- formularze softspm.com -------------------------------------------
|
||||
define('SOFTSPM_MAIL_API_KEY', 'tutaj_klucz_do_send_email_api');
|
||||
define('SOFTSPM_FORM_SECRET', 'tutaj_losowy_ciag_min_32_znaki');
|
||||
define('SOFTSPM_FORM_RECIPIENT', 'pzators@go2.pl');
|
||||
```
|
||||
|
||||
### Wariant B - plik zwraca tablicę (`return [...]`)
|
||||
|
||||
```php
|
||||
return [
|
||||
// ... to, co już tam jest ...
|
||||
|
||||
// --- formularze softspm.com ---------------------------------------
|
||||
'softspm_mail_api_key' => 'tutaj_klucz_do_send_email_api',
|
||||
'softspm_form_secret' => 'tutaj_losowy_ciag_min_32_znaki',
|
||||
'softspm_form_recipient' => 'pzators@go2.pl',
|
||||
];
|
||||
```
|
||||
|
||||
### Wpisy opcjonalne
|
||||
|
||||
```php
|
||||
// Domeny, z których wolno wysyłać formularze. Bez tego wpisu obowiązuje
|
||||
// lista z api/config.php (softspm.com, www.softspm.com, localhost).
|
||||
define('SOFTSPM_FORM_HOSTS', ['softspm.com', 'www.softspm.com']);
|
||||
|
||||
// Osobna skrzynka dla wybranego formularza. Identyfikatory formularzy:
|
||||
// newsletter, newsletter-en, szkolenia, konsulting
|
||||
define('SOFTSPM_FORM_RECIPIENTS', ['szkolenia' => 'szkolenia@softspm.com']);
|
||||
```
|
||||
|
||||
### Skąd wziąć `SOFTSPM_FORM_SECRET`
|
||||
|
||||
To nie jest klucz do niczego zewnętrznego - to sekret, którym `send.php`
|
||||
podpisuje własne tokeny antybotowe. Wygeneruj dowolny losowy ciąg:
|
||||
|
||||
```bash
|
||||
php -r "echo bin2hex(random_bytes(32)), PHP_EOL;"
|
||||
```
|
||||
|
||||
Zmiana tej wartości unieważnia formularze otwarte w tej chwili w
|
||||
przeglądarkach; nic poza tym się nie dzieje.
|
||||
|
||||
### Uwagi
|
||||
|
||||
- Plik musi być czytelny dla użytkownika, na którym działa PHP (zwykle
|
||||
`www-data`), i **nieczytelny dla świata**: `chmod 640`, właściciel
|
||||
`root:www-data`.
|
||||
- `api/config.php` robi na tym pliku `require`, więc plik musi być poprawnym
|
||||
PHP. Jeśli wykonuje jakieś operacje przy dołączeniu (łączenie z bazą i tym
|
||||
podobne), wykonają się także przy wysyłce formularza.
|
||||
- Ścieżkę można zmienić zmienną środowiskową `SOFTSPM_SHARED_CONFIG`.
|
||||
|
||||
## 2. Kolejność szukania ustawień
|
||||
|
||||
Wygrywa pierwsze znalezione:
|
||||
|
||||
1. zmienne środowiskowe `SOFTSPM_MAIL_API_KEY`, `SOFTSPM_FORM_SECRET`
|
||||
2. `/etc/hydronexis/config.php` (stałe albo klucze tablicy - patrz wyżej)
|
||||
3. `api/config.local.php` - tylko do pracy lokalnej, wzór w
|
||||
[config.local.sample.php](config.local.sample.php)
|
||||
|
||||
Adresat (`recipient`) i lista domen działają tak samo, z tym że wartość
|
||||
z `/etc/hydronexis/config.php` nadpisuje domyślną z `api/config.php`.
|
||||
|
||||
## 3. Katalog roboczy
|
||||
|
||||
`send.php` zakłada przy pierwszym uruchomieniu `api/storage/` na zużyte tokeny,
|
||||
liczniki adresów IP i log. Katalog dostaje `.htaccess` z `Require all denied`.
|
||||
Na serwerze innym niż Apache **zablokuj ten katalog samodzielnie** - nie
|
||||
powinien być dostępny z zewnątrz. Musi być zapisywalny dla PHP:
|
||||
|
||||
```bash
|
||||
mkdir -p /sciezka/do/strony/api/storage
|
||||
chown www-data:www-data /sciezka/do/strony/api/storage
|
||||
chmod 750 /sciezka/do/strony/api/storage
|
||||
```
|
||||
|
||||
W logu (`api/storage/log/RRRR-MM.log`) zapisujemy tylko fakt wysyłki, nazwę
|
||||
formularza i skrót adresu IP - bez treści zgłoszeń i bez danych osobowych.
|
||||
|
||||
## 4. Format wiadomości: czysty tekst, nie HTML
|
||||
|
||||
`send_email_api.php` **sanityzuje treść** przed osadzeniem jej we własnym
|
||||
szablonie (manual, punkt 7). Znaczniki wysłane z `send.php` docierają więc do
|
||||
skrzynki jako widoczny tekst `<p style="...">`. Dlatego wiadomości budujemy
|
||||
z czystego tekstu i **nie wolno tu wstawiać HTML-a**.
|
||||
|
||||
Układ jest odporny na to, czy szablon po drugiej stronie zamienia znaki nowej
|
||||
linii na `<br>`, czy nie: każda pozycja to osobna linia `Etykieta: wartość`,
|
||||
która zostaje czytelna nawet po sklejeniu w jeden akapit.
|
||||
|
||||
```
|
||||
Nowe zgłoszenie ze strony softspm.com
|
||||
Formularz: Zgłoszenie - szkolenia i webinaria
|
||||
Odpowiedz na: jan.kowalski@pwik.example.pl
|
||||
|
||||
--- DANE Z FORMULARZA ---
|
||||
Imię i nazwisko: Jan Kowalski
|
||||
Przedsiębiorstwo: PWiK Testowo
|
||||
E-mail: jan.kowalski@pwik.example.pl
|
||||
Telefon: 600 100 200
|
||||
Interesujący temat: Prawo wodne i wniosek taryfowy
|
||||
Wiadomość:
|
||||
Sześć osób z działu rozliczeń.
|
||||
Najlepiej pierwszy kwartał.
|
||||
|
||||
--- ZGODA UDZIELONA PRZY WYSYŁCE ---
|
||||
Wyrażam zgodę na przetwarzanie moich danych osobowych przez SoftSPM (...)
|
||||
|
||||
--- METRYKA ---
|
||||
Strona: https://softspm.com/szkolenia.html#zgloszenie
|
||||
Data (UTC): 2026-09-01 09:12:33
|
||||
Adres IP: 203.0.113.7
|
||||
```
|
||||
|
||||
Jeśli w otrzymanym mailu linie nadal są sklejone w jeden akapit, znaczy to, że
|
||||
szablon endpointu nie zamienia `\n` na `<br>`. `send_email_api.php` jest wasz,
|
||||
więc najczystszą poprawką jest tam jedno `nl2br()` na sanityzowanej treści -
|
||||
nic po stronie tej strony nie wymaga wtedy zmian.
|
||||
|
||||
## 5. Zabezpieczenia przed botami
|
||||
|
||||
Suwak sam z siebie niczego nie gwarantuje - jest elementem interfejsu, a nie
|
||||
kryptografią. Blokady, które faktycznie odsiewają automaty, działają na
|
||||
serwerze i składają się na całość:
|
||||
|
||||
| Warstwa | Co robi |
|
||||
|---|---|
|
||||
| Token podpisany HMAC | Wysyłka wymaga tokenu wydanego wcześniej przez `send.php`. Skrypt musi wykonać dwa dodatkowe żądania, nie jeden POST. |
|
||||
| Potwierdzenie suwakiem | Osobne, podpisane potwierdzenie wydawane dopiero po przesunięciu suwaka; musi mieć ten sam identyfikator co token. |
|
||||
| Jednorazowość | Każdy token przechodzi raz. Powtórka tego samego żądania dostaje HTTP 409. |
|
||||
| Bramka czasowa | Minimum 3 sekundy między pierwszym dotknięciem formularza a wysyłką (`min_fill_seconds`). Automat robi to w ułamku sekundy. |
|
||||
| Pole-pułapka | Ukryte pole `firma_www`. Wypełnione = bot. Odpowiadamy sukcesem, żeby nie podpowiadać, co go zdradziło. |
|
||||
| Kontrola pochodzenia | `Origin`/`Referer` musi wskazywać na domenę z listy. |
|
||||
| Limit adresu IP | 5 zgłoszeń na godzinę, 20 na dobę (`rate_limit` w `config.php`). |
|
||||
| Stały adresat | Adres odbiorcy pochodzi wyłącznie z konfiguracji serwera, nigdy z formularza - endpoint nie jest przekaźnikiem spamu. |
|
||||
|
||||
Jeśli mimo to zacznie przechodzić spam, następnym krokiem jest Cloudflare
|
||||
Turnstile albo hCaptcha - obie działają na tej samej zasadzie co obecny token,
|
||||
więc podmiana dotknie tylko `send.php` i `assets/forms.js`.
|
||||
|
||||
## 6. Test po wdrożeniu
|
||||
|
||||
```bash
|
||||
# 1. Token - powinien zwrócić {"ok":true,"token":"t.szkolenia...."}
|
||||
curl -s -H "Referer: https://softspm.com/szkolenia.html" \
|
||||
"https://softspm.com/api/send.php?action=token&form=szkolenia"
|
||||
|
||||
# 2. Bez nagłówka Referer/Origin - powinno być HTTP 403
|
||||
curl -s -o /dev/null -w '%{http_code}\n' \
|
||||
"https://softspm.com/api/send.php?action=token&form=szkolenia"
|
||||
|
||||
# 3. GET na nieznany formularz - HTTP 400
|
||||
curl -s -o /dev/null -w '%{http_code}\n' -H "Referer: https://softspm.com/" \
|
||||
"https://softspm.com/api/send.php?action=token&form=cokolwiek"
|
||||
|
||||
# 4. POST bez tokenu - HTTP 400 (bad_token)
|
||||
curl -s -H "Content-Type: application/json" -H "Origin: https://softspm.com" \
|
||||
-d '{"form":"szkolenia","zgoda":true,"imie":"Test"}' \
|
||||
"https://softspm.com/api/send.php"
|
||||
```
|
||||
|
||||
Jeśli krok 1 zwraca `{"error":"not_configured"}`, PHP nie widzi klucza albo
|
||||
sekretu - sprawdź uprawnienia `/etc/hydronexis/config.php`.
|
||||
|
||||
Test całości: otwórz formularz w przeglądarce, wypełnij, przesuń suwak,
|
||||
zaznacz zgodę i wyślij. Wiadomość powinna dojść na adres z
|
||||
`SOFTSPM_FORM_RECIPIENT`, a w `api/storage/log/` pojawi się wpis `sent`.
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* WZÓR pliku z sekretami - do pracy lokalnej (XAMPP, dev).
|
||||
*
|
||||
* NA PRODUKCJI TEN PLIK NIE JEST POTRZEBNY. Tam klucze bierzemy
|
||||
* z /etc/hydronexis/config.php - patrz komentarz w config.php.
|
||||
*
|
||||
* 1. Skopiuj ten plik jako config.local.php (w tym samym katalogu).
|
||||
* 2. Wpisz klucz API otrzymany do send_email_api.php.
|
||||
* 3. Wygeneruj losowy token_secret, np.:
|
||||
* php -r "echo bin2hex(random_bytes(32)), PHP_EOL;"
|
||||
* 4. Nie dodawaj config.local.php do repozytorium ani do backupu publicznego.
|
||||
*/
|
||||
|
||||
return [
|
||||
'api_key' => 'TUTAJ_KLUCZ_API',
|
||||
'token_secret' => 'TUTAJ_LOSOWY_CIAG_MIN_32_ZNAKI',
|
||||
|
||||
// Adresat zgłoszeń ze wszystkich formularzy
|
||||
'recipient' => 'pzators@go2.pl',
|
||||
|
||||
// Opcjonalnie: osobna skrzynka dla wybranego formularza
|
||||
// Identyfikatory: newsletter, newsletter-en, szkolenia, konsulting
|
||||
// 'recipients' => ['szkolenia' => 'szkolenia@softspm.com'],
|
||||
|
||||
// Domeny, z których wolno wysyłać formularze
|
||||
// 'allowed_hosts' => ['softspm.com', 'www.softspm.com', 'localhost'],
|
||||
];
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Ustawienia integracji formularzy z api.softspm.pl/send_email_api.php
|
||||
*
|
||||
* Sekretów NIE trzymamy w tym pliku ani nigdzie w katalogu strony.
|
||||
* Kolejność szukania klucza API i sekretu podpisu (wygrywa pierwsze znalezione):
|
||||
*
|
||||
* 1. zmienne środowiskowe SOFTSPM_MAIL_API_KEY i SOFTSPM_FORM_SECRET
|
||||
* 2. wspólna konfiguracja serwera: /etc/hydronexis/config.php
|
||||
* - stałe SOFTSPM_MAIL_API_KEY / SOFTSPM_FORM_SECRET, albo
|
||||
* - klucze softspm_mail_api_key / softspm_form_secret w zwracanej tablicy
|
||||
* 3. api/config.local.php (wzór: config.local.sample.php) - na potrzeby dev
|
||||
*
|
||||
* Ścieżkę wspólnej konfiguracji można zmienić zmienną SOFTSPM_SHARED_CONFIG.
|
||||
*/
|
||||
|
||||
$config = [
|
||||
// --- API wysyłki (patrz doc/manual_send_email.html) ---------------------
|
||||
'api_url' => 'https://api.softspm.pl/send_email_api.php',
|
||||
'api_key' => '',
|
||||
'timeout' => 15,
|
||||
|
||||
// --- Sekret do podpisywania tokenów antybotowych (HMAC-SHA256) ---------
|
||||
// Dowolny losowy ciąg, min. 32 znaki. Zmiana unieważnia otwarte formularze.
|
||||
'token_secret' => '',
|
||||
|
||||
// --- Adresat zgłoszeń --------------------------------------------------
|
||||
// Ustawiany wyłącznie po stronie serwera. Nigdy nie pochodzi z formularza -
|
||||
// inaczej endpoint stałby się otwartym przekaźnikiem spamu.
|
||||
// Wartość domyślna; produkcja nadpisuje ją z /etc/hydronexis/config.php.
|
||||
'recipient' => 'pzators@go2.pl',
|
||||
// Opcjonalnie: inny adres dla wybranego formularza, np.
|
||||
// 'recipients' => ['szkolenia' => 'szkolenia@softspm.com'],
|
||||
'recipients' => [],
|
||||
'subject_prefix' => '[softspm.com]',
|
||||
|
||||
// --- Skąd wolno wysyłać (nagłówek Origin / Referer) --------------------
|
||||
'allowed_hosts' => [
|
||||
'softspm.com',
|
||||
'www.softspm.com',
|
||||
'localhost',
|
||||
'127.0.0.1',
|
||||
],
|
||||
|
||||
// --- Zabezpieczenia ----------------------------------------------------
|
||||
// Minimalny czas od pierwszej interakcji z formularzem do wysyłki.
|
||||
// Człowiek czyta, pisze i przesuwa suwak; skrypt robi wszystko w jednej serii.
|
||||
'min_fill_seconds' => 3,
|
||||
// Minimalny odstęp między pobraniem tokenu a potwierdzeniem suwakiem.
|
||||
// 0 = wyłączone. Podnoś ostrożnie: ktoś, kto od razu chwyta suwak, robi
|
||||
// obie rzeczy w tej samej sekundzie i zostałby odrzucony niesłusznie.
|
||||
'min_slide_seconds' => 0,
|
||||
// Po tylu sekundach token formularza wygasa (otwarta karta w tle).
|
||||
'max_token_age' => 7200,
|
||||
// Po tylu sekundach wygasa samo potwierdzenie suwakiem.
|
||||
'max_proof_age' => 1800,
|
||||
// Limit zgłoszeń z jednego adresu IP.
|
||||
'rate_limit' => [
|
||||
'per_hour' => 5,
|
||||
'per_day' => 20,
|
||||
],
|
||||
|
||||
// --- Dane robocze (zużyte tokeny, liczniki, log) ------------------------
|
||||
'storage_dir' => __DIR__ . '/storage',
|
||||
];
|
||||
|
||||
// --- 1. Zmienne środowiskowe ------------------------------------------------
|
||||
$fromEnv = [
|
||||
'api_key' => getenv('SOFTSPM_MAIL_API_KEY'),
|
||||
'token_secret' => getenv('SOFTSPM_FORM_SECRET'),
|
||||
];
|
||||
foreach ($fromEnv as $key => $value) {
|
||||
if (is_string($value) && $value !== '') {
|
||||
$config[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 2. Wspólna konfiguracja serwera ---------------------------------------
|
||||
$sharedPath = getenv('SOFTSPM_SHARED_CONFIG');
|
||||
if (!is_string($sharedPath) || $sharedPath === '') {
|
||||
$sharedPath = '/etc/hydronexis/config.php';
|
||||
}
|
||||
|
||||
if (is_file($sharedPath) && is_readable($sharedPath)) {
|
||||
// Plik wczytujemy w osobnym zasięgu, żeby nie mieszał w zmiennych tego pliku.
|
||||
$shared = (static function (string $file) {
|
||||
/** @psalm-suppress UnresolvableInclude */
|
||||
return require $file;
|
||||
})($sharedPath);
|
||||
|
||||
if (!is_array($shared)) {
|
||||
$shared = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Jedna wartość może przyjść na dwa sposoby: jako stała zdefiniowana
|
||||
* w /etc/hydronexis/config.php albo jako klucz w tablicy zwracanej przez
|
||||
* ten plik. Zmienna środowiskowa ustawiona wcześniej ma pierwszeństwo.
|
||||
*/
|
||||
$pick = static function (string $constant, string $key) use ($shared) {
|
||||
if (defined($constant)) {
|
||||
$value = constant($constant);
|
||||
if (is_string($value) && $value !== '') {
|
||||
return $value;
|
||||
}
|
||||
if (is_array($value) && $value !== []) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
if (isset($shared[$key]) && (is_string($shared[$key]) || is_array($shared[$key])) && $shared[$key] !== '') {
|
||||
return $shared[$key];
|
||||
}
|
||||
if (isset($shared[$constant]) && (is_string($shared[$constant]) || is_array($shared[$constant])) && $shared[$constant] !== '') {
|
||||
return $shared[$constant];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Sekrety - tylko jeśli nie przyszły ze zmiennych środowiskowych.
|
||||
if ($config['api_key'] === '') {
|
||||
$value = $pick('SOFTSPM_MAIL_API_KEY', 'softspm_mail_api_key');
|
||||
if (is_string($value)) {
|
||||
$config['api_key'] = $value;
|
||||
}
|
||||
}
|
||||
if ($config['token_secret'] === '') {
|
||||
$value = $pick('SOFTSPM_FORM_SECRET', 'softspm_form_secret');
|
||||
if (is_string($value)) {
|
||||
$config['token_secret'] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// Adresat zgłoszeń - jeden dla wszystkich formularzy.
|
||||
$value = $pick('SOFTSPM_FORM_RECIPIENT', 'softspm_form_recipient');
|
||||
if (is_string($value)) {
|
||||
$config['recipient'] = $value;
|
||||
}
|
||||
|
||||
// Opcjonalnie: adresaci per formularz, tablica id_formularza => adres.
|
||||
$value = $pick('SOFTSPM_FORM_RECIPIENTS', 'softspm_form_recipients');
|
||||
if (is_array($value)) {
|
||||
$config['recipients'] = $value;
|
||||
}
|
||||
|
||||
// Opcjonalnie: domeny, z których wolno wysyłać.
|
||||
$value = $pick('SOFTSPM_FORM_HOSTS', 'softspm_form_hosts');
|
||||
if (is_array($value) && $value !== []) {
|
||||
$config['allowed_hosts'] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3. Konfiguracja lokalna (dev) ------------------------------------------
|
||||
$local = __DIR__ . '/config.local.php';
|
||||
if (is_file($local)) {
|
||||
$override = require $local;
|
||||
if (is_array($override)) {
|
||||
$config = array_replace($config, $override);
|
||||
}
|
||||
}
|
||||
|
||||
return $config;
|
||||
+632
@@ -0,0 +1,632 @@
|
||||
<?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.',
|
||||
]);
|
||||
Reference in New Issue
Block a user