# 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', ]; ``` ### Akademia Wodociągowca (harmonogram w sekcji 04) Tabela terminów na stronie głównej i na `szkolenia.html` pobiera webinary i kursy z publicznego API Akademii. Dopisz klucz wygenerowany w panelu Akademii (Ustawienia -> Klucze API - pokazywany tylko raz): ```php // wariant ze stałymi define('SOFTSPM_AKADEMIA_API_KEY', 'tutaj_klucz_z_panelu_akademii'); // wariant z tablicą 'softspm_akademia_api_key' => 'tutaj_klucz_z_panelu_akademii', ``` Adres API domyślnie to `https://akademiawodociagowca.pl/api/public/v1`. Zmieniasz go tylko wtedy, gdy instancja stoi pod innym adresem: ```php define('SOFTSPM_AKADEMIA_API_URL', 'https://inna-domena/api/public/v1'); ``` Bez tego klucza nic się nie psuje: `api/schedule.php` zwraca `source: "error"`, a w tabeli zostają statyczne wiersze wpisane w HTML. ### 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 `
`. 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 `
`, 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 `
`. `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. Harmonogram z Akademii Wodociągowca
[schedule.php](schedule.php) pobiera `GET /webinars` i `GET /courses` z API
Akademii (dokumentacja: `akademiawodociagowca/docs/api_public_v1.html`), scala
je w jedną listę i oddaje stronie jako JSON. Klucz `X-API-Key` zostaje na
serwerze - punkt 9 dokumentacji Akademii wprost tego wymaga.
- **Cache:** odpowiedź leży w `api/storage/cache/schedule.json` przez
`akademia_cache_ttl` (domyślnie 300 s, zgodnie z zaleceniem 1-5 minut).
Pusty wynik i błąd trzymane są krócej, maksymalnie minutę.
- **Kolejność:** webinary z terminem rosnąco, potem kursy na żądanie.
- **Linki:** akceptujemy wyłącznie adresy prowadzące do domeny Akademii -
gdyby API zwróciło cokolwiek innego, link jest pomijany.
- **Limit:** `?limit=` (1-40). Strona główna prosi o 8 pozycji,
`szkolenia.html` o 12.
Odpowiedź ma pole `source`:
| `source` | Znaczenie | Co widzi użytkownik |
|---|---|---|
| `api` | Są pozycje | Tabela, nagłówki i notatka podmienione na dane z Akademii; kolumna wraca z „Termin" na „Data" |
| `empty` | API odpowiedziało pustą tablicą | Statyczne wiersze z HTML-a zostają nietknięte |
| `error` | Brak klucza albo API nie odpowiada | Jak wyżej |
Statyczne wiersze to pełnoprawny plan awaryjny: działają bez JavaScriptu,
bez klucza i przy niedostępnym API. Strona nigdy nie pokazuje pustej tabeli.
Są w `index.html` (8 pozycji) i `szkolenia.html` (11 pozycji) - jeśli chcesz
zmienić te tematy zastępcze, edytujesz je wprost w HTML.
Test:
```bash
curl -s "https://softspm.com/api/schedule.php?limit=5" | head -c 400
```
Oczekiwane: `{"ok":true,"source":"api",...}` z tablicą `items`.
## 6. 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`.
## 7. 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`.