# 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`.