feat(138): security and legacy hardening

Phase 138 complete:

- Harden SMTP mailbox TLS verification with local/dev override

- Block unknown email/SMS template variables through shared catalog

- Centralize session access and replace targeted view require/FQCN patterns

Verification:

- php -l on touched files

- rg checks for raw session, targeted require, and inline App references

- git diff --check

- PHPUnit and Sonar gaps documented because local tools are unavailable
This commit is contained in:
2026-05-17 18:39:04 +02:00
parent bdf415501f
commit 9f2b5e5f3b
33 changed files with 1086 additions and 229 deletions

View File

@@ -24,6 +24,8 @@ DB_CHARSET=utf8mb4
# Windows XAMPP: C:/xampp/php/extras/ssl/cacert.pem
# Linux: /etc/ssl/certs/ca-certificates.crt
CURL_CA_BUNDLE_PATH=
# Tylko lokalnie/dev: pozwala testowi SMTP zaakceptowac self-signed/unverified certyfikaty.
SMTP_ALLOW_SELF_SIGNED_DEV=false
# Allegro User-Agent — wymagany od 01.07.2026 (art. 3.4.c Regulaminu REST API)
# Format: {APP_NAME}/{APP_VERSION} (+{ALLEGRO_USER_AGENT_URL})

View File

@@ -13,8 +13,8 @@ Sprzedawca moĹĽe obsĹugiwać zamĂłwienia ze wszystkich kanaĹĂłw
| Attribute | Value |
|-----------|-------|
| Version | 3.9.0-dev |
| Status | v3.9 Stabilizacja i splata dlugu technicznego in progress - Phase 136 Fakturownia invoice idempotency complete; Phase 137 ready to plan |
| Last Updated | 2026-05-17 (Phase 136 closed) |
| Status | v3.9 Stabilizacja i splata dlugu technicznego in progress - Phase 138 Security and Legacy Hardening complete; Phase 139 ready to plan |
| Last Updated | 2026-05-17 (Phase 138 closed) |
## Requirements
@@ -136,6 +136,8 @@ Sprzedawca moĹĽe obsĹugiwać zamĂłwienia ze wszystkich kanaĹĂłw
- [x] Backlog Reality Check: `.paul/codebase/todo.md` i `.paul/codebase/concerns.md` sklasyfikowane przeciw aktualnemu kodowi/docs, z dowodami w `BACKLOG-AUDIT.md` i routingiem do faz 135-142 — Phase 134
- [x] Accounting Net Correctness: nowe paragony zapisuja VAT-aware `receipts.total_net`, a statystyki dzienne preferuja source-level net, potem `order_items` VAT fallback, z gross `/1.23` tylko jako legacy fallback — Phase 135
- [x] Fakturownia Invoice Idempotency: delegowane faktury uzywaja stabilnego `oid=orders.internal_order_number`, lookup-first `GET /invoices.json?oid=...`, lokalnego stanu `pending_external`/`failed_retryable` i auto-attach po timeoutach — Phase 136
- [x] Delivery Status Backlog Verification: `DELIVERY-STATUS-MGMT` zamkniete jako wdrozone; runtime korzysta z DB-driven statusow, a read-only DB check nie wykazal starych ani niepoprawnych kluczy automatyzacji — Phase 137
- [x] Security and Legacy Hardening: test SMTP ma strict TLS by default z lokalnym `SMTP_ALLOW_SELF_SIGNED_DEV`, szablony e-mail/SMS blokuja nieznane placeholdery, raw `$_SESSION` jest izolowany w `Session`, a wskazane widoki uzywaja `$component()` zamiast hard `require` — Phase 138
- [x] Integracja polkurier.pl (fundament): pojedyncza globalna konfiguracja w `/settings/integrations/polkurier`, szyfrowany Token API + login, karta w hubie integracji obok Apaczki i realny test polaczenia przez `apimetod=test_auth_api` zweryfikowany na zywym koncie operatora; `ShipmentProviderRegistry` netkniety — `PolkurierShipmentService/TrackingService` w kolejnych fazach — Phase 127
- [x] polkurier ShipmentService + TrackingService + UI prepare panel: pelen kontrakt API (createShipment/getLabel/getStatus/cancelOrder/getAvailableCarriers), `PolkurierShipmentService` implementujacy `ShipmentProviderInterface` z normalizacja shipmenttype (lowercase) i splitem ulicy na street/housenumber/flatnumber, `PolkurierTrackingService` mapujacy statusy O/P/A/WP/D/Z/W na znormalizowane, panel "polkurier" w `prepare.php` z dynamiczna lista uslug z `available_carriers`, seed migracja `delivery_status_mappings(provider='polkurier')` z 7 wpisami z PDF v1.11; live test na #114/#115 zakonczony sukcesem po 4 iteracjach (ReferenceError → uppercase shipmenttype → orderno parsing → A4/A6); rozmiar etykiety sterowany w panelu klienta polkurier.pl (Ustawienia konta → Preferencje etykiet), NIE przez API — Phase 128
- [x] Order User Notes module (Phase 129): pelen CRUD notatek autorskich operatora per zamowienie. Reuse `order_notes` przez nowy `note_type='user'` z `user_id` (FK→users SET NULL) + `author_name` (snapshot) + indeks `idx_order_notes_type_order`. `OrderNotesService` z autoryzacja DB-level (`WHERE user_id = :user_id`, rowCount=0 ⇒ 403). Sekcja `#notes` w "Wiadomosci i zalaczniki" w `/orders/{id}` z inline edit form + delete przez `OrderProAlerts.confirm`. Badge `[N]` (indigo neutralny) przy nr zamowienia na `/orders/list` (subquery `user_notes_count` w paginate). Brak admin override (brak systemu rol w aplikacji) — edit/delete tylko dla autora — Phase 129
@@ -148,7 +150,7 @@ Sprzedawca moĹĽe obsĹugiwać zamĂłwienia ze wszystkich kanaĹĂłw
### Active (In Progress)
- [ ] v3.9 Stabilizacja i splata dlugu technicznego — Phase 137 Delivery Status Backlog Verification ready to plan after Phase 136.
- [ ] v3.9 Stabilizacja i splata dlugu technicznego — Phase 139 Sonar Critical/Major Cleanup ready to plan after Phase 138.
### Planned (Next)
@@ -267,6 +269,9 @@ PHP (XAMPP/Laravel), integracje z API marketplace'Ăłw (Allegro, Erli) oraz API
| v3.9 debt phases start from evidence-backed backlog audit | Phase 134 rozdzielil wpisy aktywne, wdrozone, stale i decyzyjne; kolejne fazy 135-142 maja naprawiac tylko potwierdzone problemy | 2026-05-16 | Active |
| Existing receipt `total_net` rows are not backfilled | Operator wybral zakres Phase 135 tylko dla nowych paragonow; historia pozostaje bez migracji/UPDATE | 2026-05-16 | Active |
| Accounting net fallbacks prefer explicit source data before assumptions | Phase 135: source-level net > item net/gross+VAT > legacy gross `/1.23`; dostawa fallback jako 23% VAT | 2026-05-16 | Active |
| SMTP mailbox TLS is strict by default | Phase 138: `ssl` and STARTTLS verify peer and host name; self-signed/unverified certificates require `SMTP_ALLOW_SELF_SIGNED_DEV=true` and local/dev/testing env. | 2026-05-17 | Active |
| Unknown e-mail/SMS template placeholders are blocked on save | Phase 138: `TemplateVariableCatalog` is the shared catalog; create/edit rejects unknown `{{group.variable}}` keys while existing DB rows are not migrated. | 2026-05-17 | Active |
| Raw session access belongs only in `App\Core\Support\Session` | Phase 138 moved auth, CSRF, flash and Allegro OAuth state access behind `Session::get/set/has/forget/pull`. | 2026-05-17 | Active |
| polkurier startuje jako jedna globalna konfiguracja (single-instance, mirror Apaczka/HostedSMS/SMSPLANET) z realnym testowym wywolaniem `apimetod=test_auth_api` | Operator ma jedno konto polkurier; fundament musi byc zweryfikowany na zywym API zanim dolozymy `PolkurierShipmentService` | 2026-05-14 | Active |
| polkurier wymaga `login + token` razem w body `authorization` (nie samego tokena) | Zweryfikowane w SDK polkurier-sdk (`Auth.php`/`Request.php`); kolumna `login VARCHAR(190)` w `polkurier_integration_settings` mimo ze PLAN tego nie wymagal — kontrakt API to dyktuje | 2026-05-14 | Active |
| polkurier API: top-level `status` === `'success'` (nie `'ok'`), tresc bledu w polu `response` envelope'a | `ResponseStatus::SUCCESS = 'success'` z `src/Type/ResponseStatus.php` SDK; bledy rzucane przez `ErrorException($response->get('response'))` w `PolkurierWebService.php`. Pattern dla wszystkich przyszlych metod polkurier API (`createShipment`, `getLabel`, `getStatus`, `cancelOrder`, etc.) | 2026-05-14 | Active |
@@ -314,6 +319,6 @@ Quick Reference:
---
*PROJECT.md — Updated when requirements or context change*
*Last updated: 2026-05-17 after Phase 136 (Fakturownia Invoice Idempotency) closure*
*Last updated: 2026-05-17 after Phase 138 (Security and Legacy Hardening) closure*

View File

@@ -12,16 +12,16 @@ Milestone porzadkujacy zbudowany z `.paul/codebase/todo.md` i `.paul/codebase/co
Rule for every phase/plan: przed implementacja sprawdzic w kodzie i dokumentacji, czy wpis nadal jest aktualny i czy nie zostal juz wdrozony; nastepnie przedstawic krotki plan operatorowi i zapytac o potwierdzenie. Dopiero po akceptacji wolno wprowadzac zmiany i uruchamiac testy. Jezeli wpis jest nieaktualny albo juz zrealizowany, faza/planu ma zamknac go dokumentacyjnie bez niepotrzebnej zmiany kodu.
Progress: 3 of 9 phases complete (33%).
Progress: 5 of 9 phases complete (56%).
| Phase | Name | Plans | Status |
|-------|------|-------|--------|
| 134 | Backlog Reality Check | 1/1 | Complete (2026-05-16; documentation-only audit, Sonar CLI gap documented) |
| 135 | Accounting Net Correctness | 1/1 | Complete (2026-05-16; VAT-aware receipt/stat net, PHPUnit/Sonar env gaps documented) |
| 136 | Fakturownia Invoice Idempotency | 1/1 | Complete (2026-05-17; Fakturownia oid idempotency, migration/PHPUnit/Sonar env gaps documented) |
| 137 | Delivery Status Backlog Verification | TBD | Not started |
| 138 | Security and Legacy Hardening | TBD | Not started |
| 139 | Sonar Critical/Major Cleanup | TBD | Not started |
| 137 | Delivery Status Backlog Verification | 1/1 | Complete (2026-05-17; verification-only closure, no stale automation keys found) |
| 138 | Security and Legacy Hardening | 1/1 | Complete (2026-05-17; SMTP TLS/template/session/view hardening, PHPUnit/Sonar env gaps documented) |
| 139 | Sonar Critical/Major Cleanup | TBD | Ready to plan |
| 140 | Performance Safeguards | TBD | Not started |
| 141 | God Classes and Duplication Refactor | TBD | Not started |
| 142 | Architecture Guardrails | TBD | Not started |
@@ -44,12 +44,12 @@ Plans: 136-01 (complete; `.paul/phases/136-fakturownia-invoice-idempotency/136-0
### Phase 137: Delivery Status Backlog Verification
Focus: Zweryfikowac wpis `DELIVERY-STATUS-MGMT` z todo oraz breaking changes po Phase 108: statusy DB-driven, stare klucze grup statusow, usuniecie `SHIPMENT_STATUS_OPTION_MAP` i realny wplyw na reguly automatyzacji. Jezeli funkcjonalnosc jest juz wdrozona, zamknac/oczyscic backlog i zostawic tylko potwierdzone luki.
Plans: TBD (defined during $paul-plan)
Plans: 137-01 (complete; `.paul/phases/137-delivery-status-backlog-verification/137-01-SUMMARY.md`)
### Phase 138: Security and Legacy Hardening
Focus: Sprawdzic i naprawic po potwierdzeniu: szyfrowanie `print_api_keys.api_key`, `fsockopen('ssl://...')` w tescie skrzynki e-mail, injection przez zmienne szablonow, brakujacy import `RuntimeException`, stare `require` w widokach, raw `$_SESSION` i pozostale legacy patterns wskazane w concerns.
Plans: TBD (defined during $paul-plan)
Plans: 138-01 (complete; `.paul/phases/138-security-and-legacy-hardening/138-01-SUMMARY.md`)
### Phase 139: Sonar Critical/Major Cleanup
@@ -633,4 +633,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md`
---
*Roadmap created: 2026-03-12*
*Last updated: 2026-05-17 - Phase 136 closed; Phase 137 ready to plan*
*Last updated: 2026-05-17 - Phase 138 closed; Phase 139 ready to plan*

View File

@@ -5,19 +5,19 @@
See: .paul/PROJECT.md (updated 2026-05-17)
**Core value:** Sprzedawca moze obslugiwac zamowienia ze wszystkich kanalow sprzedazy i nadawac przesylki bez przelaczania sie miedzy platformami.
**Current focus:** v3.9 Stabilizacja i splata dlugu technicznego; Phase 136 complete, Phase 137 Delivery Status Backlog Verification ready to plan.
**Current focus:** v3.9 Stabilizacja i splata dlugu technicznego; Phase 138 Security and Legacy Hardening complete, Phase 139 Sonar Critical/Major Cleanup ready to plan.
## Current Position
Milestone: v3.9 Stabilizacja i splata dlugu technicznego
Phase: 137 of 142 (Delivery Status Backlog Verification) - Ready to plan
Phase: 139 of 142 (Sonar Critical/Major Cleanup) - Ready to plan
Plan: Not started
Status: Ready for next PLAN
Last activity: 2026-05-17 17:36 - Phase 136 complete, transitioned to Phase 137
Last activity: 2026-05-17 18:35 - Phase 138 complete, transitioned to Phase 139
Progress:
- Milestone v3.9: [###-------] 33% (3 of 9 phases complete)
- Phase 137: [----------] 0% (not started)
- Milestone v3.9: [######----] 56% (5 of 9 phases complete)
- Phase 139: [----------] 0% (not started)
## Loop Position
@@ -29,18 +29,18 @@ PLAN -> APPLY -> UNIFY
## Session Continuity
Last session: 2026-05-17 17:36
Stopped at: Phase 136 complete
Next action: Run $paul-plan for Phase 137 (Delivery Status Backlog Verification)
Resume file: .paul/phases/136-fakturownia-invoice-idempotency/136-01-SUMMARY.md
Last session: 2026-05-17 18:35
Stopped at: Phase 138 complete
Next action: Run $paul-plan for Phase 139 (Sonar Critical/Major Cleanup)
Resume file: .paul/phases/138-security-and-legacy-hardening/138-01-SUMMARY.md
## Pending parallel work
- None — Phase 118, 121, 122 wszystkie zacommitowane (8f14851, 360eef1).
## Git State
Last phase commit: HEAD feat(136): fakturownia invoice idempotency
Previous: feat(135): accounting net correctness
Last phase commit: HEAD feat(138): security and legacy hardening
Previous: feat(136): fakturownia invoice idempotency
Branch: main
### Skill Audit (Phase 129)
@@ -91,6 +91,12 @@ Branch: main
|----------|---------|-------|
| `sonar-scanner` | gap documented | Attempted after APPLY with `sonar-scanner --version`; CLI is not available in PATH. |
### Skill Audit (Phase 138)
| Expected | Invoked | Notes |
|----------|---------|-------|
| `sonar-scanner` | gap documented | Attempted after APPLY with `sonar-scanner --version`; CLI is not available in PATH. |
## Accumulated Context
### Recent Decisions
@@ -102,6 +108,10 @@ Branch: main
- Phase 135 delivery net fallback uses 23% VAT when no source-level delivery VAT exists.
- Phase 139 must refresh Sonar before cleanup because the current concern counts are a stale baseline.
- Phase 136 resolved `INVOICE-IDEMP-115`: Fakturownia delegated invoices use `orders.internal_order_number` as stable `oid`; retry flow is lookup-first by `GET /invoices.json?oid=...`, persists `pending_external`/`failed_retryable` state, and auto-attaches remote invoices found after timeout.
- Phase 137 closed `DELIVERY-STATUS-MGMT` as implemented and verified: runtime code is DB-driven, old group maps are not present in source, and read-only remote DB check found 0 old/invalid shipment-status automation keys.
- Phase 138 hardened SMTP mailbox tests: TLS certificate and peer-name verification are strict by default; `SMTP_ALLOW_SELF_SIGNED_DEV=true` works only in local/dev/development/testing.
- Phase 138 blocks newly saved e-mail/SMS templates that contain unknown `{{group.variable}}` placeholders via the shared `TemplateVariableCatalog`.
- Phase 138 centralized raw `$_SESSION` access in `Session` and replaced targeted hard view `require`/inline `\App\...` patterns.
### Blockers / Concerns
@@ -109,6 +119,7 @@ Branch: main
- Phase 135: `vendor/bin/phpunit` and `sonar-scanner` are unavailable in PATH/checkout; syntax checks and ad-hoc SQLite/runtime smoke passed.
- Phase 136: Fakturownia idempotency strategy implemented and UNIFY complete; runtime migration still needs local MySQL online.
- Phase 136 APPLY: `php bin/migrate.php` could not run because local MySQL refused connection; `vendor/bin/phpunit` is missing; `sonar-scanner` is unavailable in PATH. PHP lint, documentation grep, git diff check and ad-hoc SQLite repository smoke passed.
- Phase 138 APPLY: `vendor/bin/phpunit` is missing, so new unit tests were linted but not run; `sonar-scanner` is unavailable in PATH. PHP lint, targeted `rg` checks and `git diff --check` passed.
- Phase 140: deferred indexes should be applied only after operator confirms dataset size/prod timing.
### Deferred Issues
@@ -117,6 +128,9 @@ Branch: main
## Pending Actions
- Phase 138 follow-up: run `vendor/bin/phpunit tests/Unit/SmtpSecurityContextFactoryTest.php tests/Unit/TemplateVariableCatalogTest.php` after dependencies are installed.
- Phase 138 follow-up: run SonarQube scan after `sonar-scanner` is installed or added to PATH.
- Phase 138 manual smoke: test a real SMTP SSL/STARTTLS mailbox in strict mode; test invalid and valid e-mail/SMS template saves in UI.
- Manualne testy AC-1..AC-7 dla Phase 112 na zywej bazie (XAMPP online).
- Backfill zamowienia #882 - operator robi recznie po wdrozeniu (poza zakresem planu).
- Uruchom migracje gdy XAMPP online: `php bin/migrate.php` (delivery_statuses).
@@ -175,4 +189,4 @@ Branch: main
## Skill Requirements
- `sonar-scanner` required after APPLY; Phase 116, Phase 117, Phase 121, Phase 122, Phase 128, Phase 129, Phase 130, Phase 131, Phase 132, Phase 133, Phase 134, Phase 135 and Phase 136 gaps documented because CLI was not available in PATH.
- `sonar-scanner` required after APPLY; Phase 116, Phase 117, Phase 121, Phase 122, Phase 128, Phase 129, Phase 130, Phase 131, Phase 132, Phase 133, Phase 134, Phase 135, Phase 136 and Phase 138 gaps documented because CLI was not available in PATH.

View File

@@ -5,6 +5,9 @@
- [Phase 136, Plan 136-01] Domknieto `INVOICE-IDEMP-115`: delegowane faktury Fakturowni uzywaja stabilnego `oid`, lookup-first retry i lokalnego stanu `pending_external`/`failed_retryable`.
- Dodano migracje idempotencji faktur delegowanych, obsluge repozytorium, refaktor `InvoiceService`, `findInvoiceByOid()` w kliencie Fakturowni oraz testy jednostkowe retry/auto-attach.
- Udokumentowano kontrakt `oid`, nowe kolumny `invoices` i luki weryfikacyjne: migracja wymaga dzialajacego MySQL, PHPUnit nie ma w checkoutcie, `sonar-scanner` nie jest w PATH.
- [Phase 138, Plan 138-01] Domknieto Security and Legacy Hardening: strict SMTP TLS, jawny local/dev self-signed override, walidacja zmiennych szablonow, centralizacja sesji i targeted view cleanup.
- Dodano `SmtpSecurityContextFactory`, `TemplateVariableCatalog`, testy jednostkowe dla obu polityk oraz helper `$component()` w `Template`.
- Udokumentowano brak zmian DB oraz luki srodowiskowe: `vendor/bin/phpunit` i `sonar-scanner` nie sa dostepne lokalnie.
## Zmienione pliki
@@ -26,3 +29,28 @@
- `src/Modules/Accounting/InvoiceService.php`
- `src/Modules/Settings/FakturowniaApiClient.php`
- `tests/Unit/FakturowniaInvoiceIdempotencyTest.php`
- `.env.example`
- `AGENTS.md`
- `.paul/codebase/concerns.md`
- `.paul/codebase/tech_changelog.md`
- `.paul/phases/138-security-and-legacy-hardening/138-01-PLAN.md`
- `.paul/phases/138-security-and-legacy-hardening/138-01-SUMMARY.md`
- `config/app.php`
- `routes/web.php`
- `src/Core/Security/Csrf.php`
- `src/Core/Support/Flash.php`
- `src/Core/Support/Session.php`
- `src/Core/View/Template.php`
- `src/Modules/Auth/AuthService.php`
- `src/Modules/Settings/AllegroIntegrationController.php`
- `src/Modules/Settings/EmailMailboxController.php`
- `src/Modules/Settings/EmailTemplateController.php`
- `src/Modules/Settings/SmsTemplateController.php`
- `src/Modules/Settings/SmtpSecurityContextFactory.php`
- `src/Modules/Settings/TemplateVariableCatalog.php`
- `resources/views/accounting/index.php`
- `resources/views/orders/list.php`
- `resources/views/orders/show.php`
- `resources/views/users/index.php`
- `tests/Unit/SmtpSecurityContextFactoryTest.php`
- `tests/Unit/TemplateVariableCatalogTest.php`

View File

@@ -8,17 +8,17 @@ Szczegoly i dowody: `.paul/phases/134-backlog-reality-check/BACKLOG-AUDIT.md`.
|--------------|-------------------|----------------|
| God Classes | **Active** | Klasy nadal sa duze; stare LOC/method counts sa nieaktualne, ale Phase 141 pozostaje zasadny. |
| SonarQube Issues | **Stale baseline / active patterns** | Liczby wymagaja nowego skanu; lokalnie nadal widac wzorce do sprzatania. `RuntimeException` import jest juz naprawiony. |
| Breaking: delivery status group keys | **Active operational follow-up** | DB-driven statusy sa wdrozone, ale stare reguly automation moga wymagac odtworzenia przez operatora. |
| Breaking: delivery status group keys | **Closed in Phase 137** | DB-driven statusy sa wdrozone, a read-only DB check nie znalazl starych ani niepoprawnych kluczy automatyzacji. |
| Breaking: `SHIPMENT_STATUS_OPTION_MAP` | **Implemented / stale** | Symbol nie wystepuje juz w runtime source. |
| Breaking: `_csrf_token` -> `_token` | **Implemented / stale** | Formularze/kontrolery uzywaja `_token`; wewnetrzny session key w `Csrf` nie jest problemem formularzy. |
| Known Bugs: `STAT-NET` | **Resolved in Phase 135** | Runtime statystyk liczy net z source-level net albo item-level VAT; `RECEIPT-NET-FIX` naprawiony dla nowych paragonow bez backfillu historii. |
| Deferred Indexes | **Active / deferred** | Indeksy nadal nie sa w migracjach; wykonac po decyzji operatora w Phase 140. |
| Security: print API keys | **Implemented / stale** | Przechowywany jest hash i prefix, nie raw `api_key`. |
| Security: mailbox TLS | **Active, wording stale** | Nie ma juz `fsockopen`, ale `stream_socket_client()` ma `verify_peer=false`. |
| Security: template variables | **Needs operator/security decision** | Resolver dopuszcza tylko znane mapy, ale trzeba zdecydowac polityke dla nieznanych `{{var}}`. |
| Security: mailbox TLS | **Resolved in Phase 138** | Test SMTP uzywa strict peer/name verification dla `ssl` i STARTTLS; self-signed/unverified tylko przez lokalny `SMTP_ALLOW_SELF_SIGNED_DEV`. |
| Security: template variables | **Resolved in Phase 138** | Nowe/edytowane szablony e-mail/SMS blokuja nieznane `{{grupa.zmienna}}` przez wspolny `TemplateVariableCatalog`. |
| Architecture Concerns | **Active / low impact** | Zostawic do decyzji w Phase 142. |
| Duplication Areas | **Mixed** | `SslCertificateResolver` i `RedirectPathResolver` sa czesciowo wdrozone; reszta wymaga selektywnej decyzji. |
| Legacy patterns | **Mixed** | View `include/require` nadal wystepuja; raw `$_SESSION` glownie w warstwach Auth/Flash/Csrf/OAuth. |
| Legacy patterns | **Partly resolved in Phase 138** | Raw `$_SESSION` jest izolowany w `Session`; wskazane hard `require` i inline FQCN w widokach sa usuniete. Alert includes pozostaja zaakceptowanym patternem Phase 120. |
| Performance Risks | **Active / needs profiling** | Return-risk indexes i cron backoff aktywne; `findDetails()` najpierw profilowac. |
## God Classes (Priority Refactor Targets)
@@ -46,7 +46,7 @@ Szczegoly i dowody: `.paul/phases/134-backlog-reality-check/BACKLOG-AUDIT.md`.
| `php:S3776` — Cognitive complexity > 15 | 9+ | CRITICAL | `ShopproOrderMapper::initOrderFromArray()` (28), `ShipmentTrackingHandler` (27) |
| `php:S1448` — Class too large | 6+ | MAJOR | See god classes above |
| `php:S1172` — Unused parameters | 11+ | MAJOR | `$request` params in handlers, unused payload params |
| `php:S4423` — Weak TLS protocol | 1 | **CRITICAL** | `EmailMailboxController::testConnection()` line ~223: `fsockopen('ssl://...')` — deprecated |
| `php:S4423` — Weak TLS protocol | stale | **CRITICAL** | Resolved in Phase 138: `EmailMailboxController::testConnection()` uzywa strict SSL context i STARTTLS |
| `php:S5911` — Missing import | 1 | **BLOCKER** | `AllegroOrderImportService``RuntimeException` not imported |
| `php:S4833` — Use namespace import | 2 | MAJOR | `resources/views/accounting/index.php:31`, `orders/show.php:780` |
| `Web:S6827` — Anchors without accessible text | 9+ | MINOR | Icon-only buttons need `aria-label` |
@@ -58,7 +58,7 @@ Szczegoly i dowody: `.paul/phases/134-backlog-reality-check/BACKLOG-AUDIT.md`.
| Change | Phase | Impact | Migration |
|--------|-------|--------|-----------|
| Delivery status group keys przeniesione do DB | Phase 108 (2026-04-27) | Stare reguły automation z kluczami `registered`, `courier_pickup`, `dropped_at_point`, `unclaimed`, `picked_up` przestają działać | Operatorzy muszą ręcznie odtworzyć reguły w UI |
| Delivery status group keys przeniesione do DB | Phase 108 (2026-04-27), verified Phase 137 (2026-05-17) | Stare reguly automation z kluczami `registered`, `courier_pickup`, `dropped_at_point`, `unclaimed`, `picked_up_return` byly ryzykiem po breaking change | Zamkniete: read-only DB check znalazl 0 starych i 0 niepoprawnych kluczy |
| `SHIPMENT_STATUS_OPTION_MAP` usunięty | Phase 108 (2026-04-27) | `AutomationService` porównuje klucze statusów bezpośrednio z DB | Brak wpływu po odtworzeniu reguł |
| `_csrf_token``_token` | Phase 105 (2026-04-19) | Stare nazwy pól formularzy | Sprawdzić czy nie ma starych referencji `_csrf_token` w widokach |
@@ -87,8 +87,8 @@ These support the correlated subquery in `OrdersRepository` used for return-risk
| Item | Risk | Action |
|------|------|--------|
| `print_api_keys.api_key` encryption | MEDIUM | Verify column is encrypted (same as `integrations.api_key_encrypted`) |
| `fsockopen('ssl://...')` in `EmailMailboxController::testConnection()` | MEDIUM | Replace with `stream_socket_client()` + `stream_context_create(['ssl' => ['verify_peer' => true]])` |
| Email variable injection via `{{var}}` templates | LOW | Only predefined variables allowed — verify automation rule creation doesn't accept arbitrary variable names |
| SMTP TLS in `EmailMailboxController::testConnection()` | MEDIUM | Resolved in Phase 138: strict certificate verification by default; local dev override via `SMTP_ALLOW_SELF_SIGNED_DEV`. |
| Email/SMS variable injection via `{{var}}` templates | LOW | Resolved in Phase 138: `TemplateVariableCatalog` blocks unknown placeholders on save. |
## Architecture Concerns
@@ -111,9 +111,9 @@ These support the correlated subquery in `OrdersRepository` used for return-risk
| Pattern | Location | Status |
|---------|----------|--------|
| `fsockopen('ssl://')` | `EmailMailboxController::testConnection()` | Deprecated PHP TLS approach — fix when touching that method |
| `require` in views | `resources/views/accounting/index.php:31` | Should use namespace `use` — minor |
| Raw `$_SESSION` access | Some older controllers | Should use `Session::get()` / `set()` helpers |
| `fsockopen('ssl://')` / weak SMTP TLS | `EmailMailboxController::testConnection()` | Resolved in Phase 138; strict stream context + STARTTLS, local dev override only. |
| `require` in targeted views | `resources/views/accounting/index.php`, `orders/list.php`, `orders/show.php`, `users/index.php` | Resolved in Phase 138 through `$component()` helper. Other alert includes remain accepted Phase 120 pattern. |
| Raw `$_SESSION` access | Auth/Flash/Csrf/OAuth before Phase 138 | Resolved in Phase 138; raw access is isolated in `App\Core\Support\Session`. |
## Performance Risks

View File

@@ -1,5 +1,35 @@
# Technical Changelog
## 2026-05-17 - Phase 138 Plan 01: Security and Legacy Hardening
**Co zrobiono:**
- `EmailMailboxController::testConnection()` uzywa strict TLS verification dla implicit SSL i STARTTLS.
- `SMTP_ALLOW_SELF_SIGNED_DEV` pozwala na self-signed/unverified certyfikaty tylko lokalnie/dev/testing.
- `TemplateVariableCatalog` centralizuje zmienne e-mail/SMS i blokuje zapis szablonow z nieznanymi placeholderami.
- `Session` dostal helpery `get/set/has/forget/pull`; raw `$_SESSION` przeniesiono do tej warstwy.
- `Template` dostal `$component()` helper, a wskazane widoki przestaly uzywac hard `require` dla komponentow/partiali.
**Dlaczego:**
- Phase 134 potwierdzil aktywne ryzyka security/legacy: weak SMTP TLS, niespojna polityka zmiennych, raw session access i view include debt.
**BREAKING / migracja:**
- Brak migracji DB. Nowe/edytowane szablony z nieznanymi zmiennymi sa odrzucane.
## 2026-05-17 - Phase 137 Plan 01: Delivery Status Backlog Verification
**Co zrobiono:**
- Zweryfikowano runtime code dla `DELIVERY-STATUS-MGMT`: `SHIPMENT_STATUS_OPTIONS` i `SHIPMENT_STATUS_OPTION_MAP` nie istnieja juz w source poza historyczna dokumentacja.
- Potwierdzono, ze `AutomationController` uzywa `DeliveryStatus::getAllOptions()` / `getAllStatuses()` dla dropdownow i walidacji.
- Potwierdzono, ze `AutomationService` porownuje `shipment_status` bezposrednio po znormalizowanych kluczach DB.
- Lokalny MySQL byl niedostepny, wiec wykonano manualny read-only check przez `DB_HOST_REMOTE` bez zmiany runtime config.
- Read-only DB check: `delivery_statuses=11`, `shipment_status` conditions=3, `update_shipment_status` actions=0, stare klucze=0, invalid keys=0.
**Dlaczego:**
- Phase 134 oznaczyl `DELIVERY-STATUS-MGMT` jako wdrozone w kodzie, ale wymagajace operator verification po breaking change z Phase 108. Phase 137 potwierdzila, ze w aktualnych danych nie ma reguly wymagajacej recznej migracji.
**BREAKING / migracja:**
- Brak zmian runtime i brak migracji. Phase 137 jest verification-only.
## 2026-05-17 - Phase 136 Plan 01: Fakturownia Invoice Idempotency
**Co zrobiono:**

View File

@@ -78,6 +78,9 @@
### Status audytu Phase 134 (2026-05-16)
- **Implemented; operator verification remains** - core DB-driven statusy, CRUD i dropdowny automatyzacji sa wdrozone po Phase 108. Phase 137 powinien zweryfikowac tylko stare reguly automatyzacji/operator migration i zamknac nieaktualny wpis. Dowody: `.paul/phases/134-backlog-reality-check/BACKLOG-AUDIT.md`.
### Status Phase 137 (2026-05-17)
- **Closed / verified** - runtime code nie zawiera juz `SHIPMENT_STATUS_OPTIONS` ani `SHIPMENT_STATUS_OPTION_MAP`; `AutomationController` i `AutomationService` uzywaja `DeliveryStatus::getAllStatuses()` / `getAllOptions()`. Read-only check przez `DB_HOST_REMOTE` wykazal 11 statusow w `delivery_statuses`, 3 warunki `shipment_status`, 0 akcji `update_shipment_status`, 0 starych kluczy (`registered`, `courier_pickup`, `dropped_at_point`, `unclaimed`, `picked_up_return`) i 0 kluczy spoza `delivery_statuses`. Dowody: `.paul/phases/137-delivery-status-backlog-verification/137-01-SUMMARY.md`.
### Kontekst
- Aktualnie statusy znormalizowane (`created`, `confirmed`, `picked_up`, `in_transit`, itd.) sa stalymi w kodzie (`DeliveryStatus.php`).
- Dodanie nowego statusu wymaga zmiany kodu + deploymentu.

View File

@@ -0,0 +1,76 @@
---
phase: 137-delivery-status-backlog-verification
plan: 01
subsystem: backlog, automation, delivery-statuses
tags: [verification, delivery-status, automation, backlog]
runtime-code-changed: false
database-changed: false
completed: 2026-05-17
---
# Phase 137 Plan 01: Delivery Status Backlog Verification
Phase 137 zamknela wpis `DELIVERY-STATUS-MGMT` jako wdrozony i zweryfikowany. Nie bylo potrzeby dodawania nowej funkcji, migracji ani refaktoru runtime.
## What Was Checked
- Runtime code no longer defines `SHIPMENT_STATUS_OPTIONS` or `SHIPMENT_STATUS_OPTION_MAP`.
- `AutomationController` builds shipment status dropdowns from `DeliveryStatus::getAllOptions()` and validates submitted status keys through `DeliveryStatus::getAllStatuses()`.
- `AutomationService` evaluates `shipment_status` conditions by direct normalized DB status keys and resolves `update_shipment_status` actions through `DeliveryStatus::getAllStatuses()`.
- `DeliveryStatusRepository` provides DB-backed status CRUD/cache and `DeliveryStatus::setRepository()` remains the runtime bridge.
- Documentation/backlog references to the old symbols are historical notes only.
## Database Verification
Local `DB_HOST=localhost` was unavailable during verification:
```text
SQLSTATE[HY000] [2002] Nie można nawiązać połączenia, ponieważ komputer docelowy aktywnie go odmawia
```
Per project rule, `DB_HOST_REMOTE` was used only for this manual read-only verification. Runtime configuration was not changed.
Read-only remote results:
| Check | Result |
|-------|--------|
| `delivery_statuses` rows | 11 |
| `shipment_status` automation conditions | 3 |
| `update_shipment_status` automation actions | 0 |
| old group keys found | 0 |
| invalid status keys outside `delivery_statuses` | 0 |
Active shipment-status rules found:
| Rule ID | Name | Status keys |
|---------|------|-------------|
| 8 | Zmień status zamówienia na zrealizowane | `delivered` |
| 14 | Zmień status zamówienia na zwrot | `returned` |
| 18 | Zmień status zamówienia na wysłane | `picked_up`, `in_transit`, `out_for_delivery` |
Checked old keys:
```text
registered
courier_pickup
dropped_at_point
unclaimed
picked_up_return
```
None are present in `automation_conditions.condition_value` or `automation_actions.action_config`.
## Conclusion
`DELIVERY-STATUS-MGMT` is complete:
- DB-driven normalized delivery statuses exist.
- Automation dropdowns and validation use the DB-backed status list.
- The old hardcoded group maps are absent from runtime source.
- The current production-like data has no stale shipment-status automation keys requiring manual migration.
## Follow-Up
- None for Phase 137.
- Phase 138 can proceed with Security and Legacy Hardening.

View File

@@ -0,0 +1,250 @@
---
phase: 138-security-and-legacy-hardening
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- .env.example
- config/app.php
- routes/web.php
- src/Core/View/Template.php
- src/Core/Support/Session.php
- src/Core/Security/Csrf.php
- src/Core/Support/Flash.php
- src/Modules/Auth/AuthService.php
- src/Modules/Settings/AllegroIntegrationController.php
- src/Modules/Settings/EmailMailboxController.php
- src/Modules/Settings/SmtpSecurityContextFactory.php
- src/Modules/Settings/EmailTemplateController.php
- src/Modules/Settings/SmsTemplateController.php
- src/Modules/Settings/TemplateVariableCatalog.php
- resources/views/accounting/index.php
- resources/views/orders/list.php
- resources/views/orders/show.php
- resources/views/users/index.php
- tests/Unit/SmtpSecurityContextFactoryTest.php
- tests/Unit/TemplateVariableCatalogTest.php
- DOCS/ARCHITECTURE.md
- DOCS/TECH_CHANGELOG.md
- .paul/codebase/concerns.md
autonomous: true
delegation: off
---
<objective>
## Goal
Domknac Phase 138 jako pelny hardening potwierdzonych security/legacy items: SMTP TLS verification, polityka zmiennych szablonow, raw session access i selektywny cleanup legacy view patterns.
## Purpose
Faza zmniejsza realne ryzyko security bez naruszania obecnych przeplywow zamowien, integracji i dokumentow. Najwazniejszy runtime risk to test skrzynki e-mail z wylaczona weryfikacja certyfikatu; pozostale prace maja usunac niespojne lub historycznie mylace wzorce przed Phase 139/141.
## Output
Kod aplikacji z bezpiecznym domyslnym SMTP TLS, jawnym lokalnym dev override dla self-signed/unverified certow, walidacja nieznanych zmiennych e-mail/SMS przy zapisie, ograniczony raw `$_SESSION`, uporzadkowane wskazane widoki oraz zaktualizowane dokumenty.
</objective>
<context>
<clarifications>
- **Zakres** - Jaki zakres ma miec pierwszy plan Phase 138?
-> Odpowiedz: B - Pelny hardening: TLS, zmienne, raw `$_SESSION`, include/require.
- **Zmienne** - Jak traktowac nieznane zmienne typu `{{grupa.zmienna}}` w szablonach e-mail/SMS?
-> Odpowiedz: A - Blokowac zapis z czytelnym bledem.
- **TLS** - Czy test SMTP po naprawie ma dopuszczac certyfikaty samopodpisane lub niezweryfikowane?
-> Odpowiedz: B - Dodac jawny tryb dev dla self-signed/unverified.
</clarifications>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
@AGENTS.md
@DOCS/ARCHITECTURE.md
@DOCS/DB_SCHEMA.md
## Prior Work
@.paul/phases/134-backlog-reality-check/BACKLOG-AUDIT.md
@.paul/phases/137-delivery-status-backlog-verification/137-01-SUMMARY.md
## Source Files
@src/Modules/Settings/EmailMailboxController.php
@src/Modules/Settings/EmailMailboxRepository.php
@src/Modules/Settings/EmailTemplateController.php
@src/Modules/Settings/SmsTemplateController.php
@src/Modules/Sms/SmsVariableResolver.php
@src/Modules/Email/VariableResolver.php
@src/Core/Support/Session.php
@src/Core/Security/Csrf.php
@src/Core/Support/Flash.php
@src/Modules/Auth/AuthService.php
@src/Modules/Settings/AllegroIntegrationController.php
@src/Core/View/Template.php
@resources/views/accounting/index.php
@resources/views/orders/list.php
@resources/views/orders/show.php
@resources/views/users/index.php
@config/app.php
@routes/web.php
@.env.example
</context>
<skills>
## Required Skills (from SPECIAL-FLOWS.md)
| Skill | Priority | When to Invoke | Loaded? |
|-------|----------|----------------|---------|
| `sonar-scanner` | required | After APPLY, before UNIFY | o |
| /code-review | optional | After APPLY, before UNIFY for security-sensitive diff | o |
**BLOCKING:** `sonar-scanner` must be attempted after APPLY. If unavailable in PATH, record the gap in SUMMARY/STATE as in prior phases.
</skills>
<acceptance_criteria>
## AC-1: SMTP TLS verifies certificates by default
```gherkin
Given an operator tests an SMTP mailbox with encryption `ssl` or `tls`
When the test connection opens or upgrades the socket
Then peer and host-name verification are enabled by default and certificate failures return a clear JSON error instead of silently accepting the connection
```
## AC-2: SMTP dev override is explicit and local-only
```gherkin
Given `SMTP_ALLOW_SELF_SIGNED_DEV=true` and the app environment is local/dev/testing
When the SMTP test uses `ssl` or `tls`
Then the code may allow self-signed or unverified certificates only for that dev mode and the default production behavior remains strict
```
## AC-3: Unknown template variables are blocked on save
```gherkin
Given an e-mail or SMS template contains an unknown placeholder such as `{{foo.bar}}`
When the operator saves the template
Then the save is rejected with a readable validation message listing the unknown placeholders and the old template content is not overwritten
```
## AC-4: Valid template variables remain compatible
```gherkin
Given an e-mail or SMS template uses only the documented variables from the shared catalog
When the operator saves, previews, or sends from an order
Then the template continues to save and resolve variables using the existing order/company/shipment data contracts
```
## AC-5: Session access is centralized
```gherkin
Given code outside the low-level session helpers needs to read or write session state
When auth and Allegro OAuth state are updated
Then they use `App\Core\Support\Session` helper methods, and raw `$_SESSION` remains only in the session abstraction internals or documented compatibility wrappers
```
## AC-6: Legacy view patterns are reduced without a broad redesign
```gherkin
Given the targeted order/accounting/user views render shared components or reference application classes
When those views are rendered
Then output remains functionally equivalent, hard `require` component calls in the targeted files are replaced with the project view/component helper, and `orders/show.php` no longer uses fully-qualified `\App\...` class names inline
```
## AC-7: Documentation reflects the security decisions
```gherkin
Given Phase 138 changes runtime security and accepted legacy boundaries
When implementation is complete
Then `DOCS/ARCHITECTURE.md`, `DOCS/TECH_CHANGELOG.md`, and `.paul/codebase/concerns.md` document the SMTP dev override, variable policy, session boundary, and resolved/deferred concern status
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Harden SMTP mailbox test TLS</name>
<files>.env.example, config/app.php, routes/web.php, src/Modules/Settings/EmailMailboxController.php, src/Modules/Settings/SmtpSecurityContextFactory.php, tests/Unit/SmtpSecurityContextFactoryTest.php</files>
<action>
Add a small `SmtpSecurityContextFactory` responsible for building stream context options for SMTP tests.
- Add `SMTP_ALLOW_SELF_SIGNED_DEV=false` to `.env.example` with a comment that it is local/dev only.
- Add `app.smtp.allow_self_signed_dev` config derived from `SMTP_ALLOW_SELF_SIGNED_DEV` and allowed only for `APP_ENV` values `local`, `dev`, `development`, or `testing`.
- Inject the resolved boolean into `EmailMailboxController` from `routes/web.php`.
- For `ssl`, open an implicit TLS socket with strict verification unless the dev override is active.
- For `tls`, connect plain, require STARTTLS, then enable crypto with the same verification policy.
- For `none`, keep the existing no-encryption behavior without SSL context.
- Return clear JSON failures for connection, greeting, STARTTLS, crypto, and auth errors.
- Avoid global insecure defaults and do not change stored mailbox schema.
</action>
<verify>`C:\xampp\php\php.exe -l src/Modules/Settings/EmailMailboxController.php`; `C:\xampp\php\php.exe -l src/Modules/Settings/SmtpSecurityContextFactory.php`; `vendor/bin/phpunit tests/Unit/SmtpSecurityContextFactoryTest.php` if dependencies are installed</verify>
<done>AC-1 and AC-2 satisfied; strict TLS is default and dev override is explicit/local-only.</done>
</task>
<task type="auto">
<name>Task 2: Add shared template variable policy</name>
<files>src/Modules/Settings/TemplateVariableCatalog.php, src/Modules/Settings/EmailTemplateController.php, src/Modules/Settings/SmsTemplateController.php, src/Modules/Sms/SmsVariableResolver.php, tests/Unit/TemplateVariableCatalogTest.php</files>
<action>
Create a shared variable catalog for e-mail and SMS templates.
- Move the documented groups/sample keys from duplicated controller constants into `TemplateVariableCatalog`.
- Add `findUnknownPlaceholders()` that scans `{{group.var}}` placeholders and returns unique unknown keys while allowing only the catalog keys.
- In `EmailTemplateController::save()`, validate both subject and body HTML; reject unknown placeholders with a readable Polish flash message and redirect back to the edit/create form.
- In `SmsTemplateController::save()`, validate body; reject unknown placeholders analogically.
- Make e-mail preview use the same catalog/sample data so preview and save policy match.
- Preserve current runtime resolver behavior for already-existing legacy records; this plan blocks newly saved invalid templates rather than adding a DB migration.
</action>
<verify>`C:\xampp\php\php.exe -l src/Modules/Settings/TemplateVariableCatalog.php`; `C:\xampp\php\php.exe -l src/Modules/Settings/EmailTemplateController.php`; `C:\xampp\php\php.exe -l src/Modules/Settings/SmsTemplateController.php`; `vendor/bin/phpunit tests/Unit/TemplateVariableCatalogTest.php` if dependencies are installed</verify>
<done>AC-3 and AC-4 satisfied; invalid placeholders cannot be saved and valid variables remain compatible.</done>
</task>
<task type="auto">
<name>Task 3: Encapsulate session access and reduce targeted legacy view patterns</name>
<files>src/Core/View/Template.php, src/Core/Support/Session.php, src/Core/Security/Csrf.php, src/Core/Support/Flash.php, src/Modules/Auth/AuthService.php, src/Modules/Settings/AllegroIntegrationController.php, resources/views/accounting/index.php, resources/views/orders/list.php, resources/views/orders/show.php, resources/views/users/index.php, DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md, .paul/codebase/concerns.md</files>
<action>
Keep this cleanup selective and behavior-preserving.
- Add `Session::get()`, `set()`, `has()`, `forget()`, and `pull()` helpers.
- Migrate `AuthService`, `Csrf`, `Flash`, and Allegro OAuth state access to those helpers so raw `$_SESSION` is isolated in `Session`.
- Add a component/view helper in `Template::renderFile()` for targeted view component rendering.
- Replace hard `require` calls in the targeted order/accounting/user views with that helper where they load shared components or partials.
- In `orders/show.php`, import `StringHelper` and `DeliveryStatus` at the top and replace inline fully-qualified `\App\...` references.
- Do not rewrite all alert component includes from Phase 120; document that as accepted current pattern unless it becomes a separate component-rendering refactor.
- Update architecture/changelog/concerns docs with the resolved items and explicit remaining boundaries.
</action>
<verify>`rg -n "\$_SESSION" src`; `rg -n "\brequire\b" resources/views/accounting/index.php resources/views/orders/list.php resources/views/orders/show.php resources/views/users/index.php`; `rg -n "\\App\\" resources/views/orders/show.php`; PHP lint on all touched PHP files; `git diff --check`</verify>
<done>AC-5, AC-6, and AC-7 satisfied; raw session and targeted view legacy patterns are reduced without broad UI redesign.</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- No database migrations or schema changes in this plan.
- Do not use `DB_HOST_REMOTE`; this plan has no DB operation.
- Do not alter order import, invoice issuance, shipment creation, status sync, or SMS sending business flows except template-save validation.
- Do not add native `alert()` / `confirm()` calls in views.
- Do not move CSS into views.
## SCOPE LIMITS
- Existing invalid templates in the database are not migrated or backfilled; validation applies when templates are created or edited.
- SMTP live verification against real external servers is a manual/operator smoke test if credentials are available.
- Full view component refactor across every alert include is out of scope; only targeted legacy hardening files are included.
- Sonar broad cleanup is Phase 139, not this plan.
</boundaries>
<verification>
Before declaring plan complete:
- [ ] `C:\xampp\php\php.exe -l` passes for every touched PHP file.
- [ ] `vendor/bin/phpunit tests/Unit/SmtpSecurityContextFactoryTest.php tests/Unit/TemplateVariableCatalogTest.php` passes, or the missing vendor/PHPUnit gap is documented.
- [ ] `rg -n "\$_SESSION" src` shows raw session access isolated to `src/Core/Support/Session.php` or explicitly documented wrappers if unavoidable.
- [ ] `rg -n "\brequire\b" resources/views/accounting/index.php resources/views/orders/list.php resources/views/orders/show.php resources/views/users/index.php` has no targeted hard component requires left.
- [ ] `rg -n "\\App\\" resources/views/orders/show.php` returns no inline fully-qualified view class calls.
- [ ] `git diff --check` passes.
- [ ] `sonar-scanner` is attempted; if unavailable, gap is recorded.
- [ ] `DOCS/ARCHITECTURE.md`, `DOCS/TECH_CHANGELOG.md`, and `.paul/codebase/concerns.md` are updated.
</verification>
<success_criteria>
- SMTP mailbox test no longer disables certificate checks by default.
- Local/dev self-signed SMTP behavior is possible only through an explicit env/config switch.
- New or edited e-mail/SMS templates cannot save unknown `{{group.var}}` placeholders.
- Raw `$_SESSION` access is centralized behind `Session` helpers.
- Targeted hard `require`/FQCN view issues are removed or explicitly documented as accepted component pattern.
- No schema/runtime business-flow regressions are introduced.
- All verification checks pass or environment gaps are documented.
</success_criteria>
<output>
After completion, create `.paul/phases/138-security-and-legacy-hardening/138-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,165 @@
---
phase: 138-security-and-legacy-hardening
plan: 01
subsystem: security, settings, templates, views, session
tags: [security, smtp, templates, session, legacy-cleanup]
requires:
- phase: 134-backlog-reality-check
provides: evidence-backed security and legacy backlog
- phase: 137-delivery-status-backlog-verification
provides: closed delivery-status backlog risk
provides:
- strict SMTP mailbox TLS verification by default
- local/dev-only SMTP self-signed override
- shared e-mail/SMS template variable catalog
- unknown template placeholder save validation
- centralized session helper access
- targeted view component rendering cleanup
affects: [phase-139-sonar-cleanup, phase-141-refactor, phase-142-architecture-guardrails]
tech-stack:
added: []
patterns: [SmtpSecurityContextFactory, TemplateVariableCatalog, Template component helper, Session get-set-pull helpers]
key-files:
created:
- src/Modules/Settings/SmtpSecurityContextFactory.php
- src/Modules/Settings/TemplateVariableCatalog.php
- tests/Unit/SmtpSecurityContextFactoryTest.php
- tests/Unit/TemplateVariableCatalogTest.php
modified:
- src/Modules/Settings/EmailMailboxController.php
- src/Modules/Settings/EmailTemplateController.php
- src/Modules/Settings/SmsTemplateController.php
- src/Core/Support/Session.php
- src/Core/View/Template.php
key-decisions:
- "SMTP TLS verification is strict by default; self-signed/unverified certs require SMTP_ALLOW_SELF_SIGNED_DEV in local/dev/testing."
- "Unknown e-mail/SMS placeholders are blocked on save; existing stored records are not migrated."
- "Raw session access is allowed only inside App\\Core\\Support\\Session."
patterns-established:
- "Settings controllers should use TemplateVariableCatalog for documented template variables."
- "Targeted reusable PHP partials can be rendered through Template's $component() helper."
runtime-code-changed: true
database-changed: false
duration: ~30min
started: 2026-05-17T18:05:00+02:00
completed: 2026-05-17T18:35:00+02:00
---
# Phase 138 Plan 01: Security and Legacy Hardening Summary
SMTP mailbox tests now verify TLS by default, template saves reject unknown placeholders, raw session access is isolated, and the targeted legacy view patterns were reduced without schema changes.
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~30 minutes |
| Started | 2026-05-17T18:05:00+02:00 |
| Completed | 2026-05-17T18:35:00+02:00 |
| Tasks | 3 completed |
| Files created | 4 |
| Runtime/schema | Runtime changed; no DB schema change |
## Acceptance Criteria Results
| Criterion | Status | Evidence |
|-----------|--------|----------|
| AC-1: SMTP TLS verifies certificates by default | Pass | `SmtpSecurityContextFactory` enables `verify_peer` and `verify_peer_name`; `EmailMailboxController` uses strict context for `ssl` and STARTTLS. |
| AC-2: SMTP dev override is explicit and local-only | Pass | `config/app.php` enables `app.smtp.allow_self_signed_dev` only when `SMTP_ALLOW_SELF_SIGNED_DEV=true` and `APP_ENV` is local/dev/development/testing. |
| AC-3: Unknown template variables are blocked on save | Pass | `EmailTemplateController::save()` and `SmsTemplateController::save()` reject unknown placeholders returned by `TemplateVariableCatalog::findUnknownPlaceholders()`. |
| AC-4: Valid template variables remain compatible | Pass | Catalog keys match existing order/buyer/address/company/shipment resolver contract; e-mail preview now uses the same sample data catalog. |
| AC-5: Session access is centralized | Pass | `rg -n '\$_SESSION' src` shows raw access only in `src/Core/Support/Session.php`. |
| AC-6: Legacy view patterns are reduced | Pass | Targeted `require` calls were replaced with `$component()`; `orders/show.php` no longer has inline `\App\...` FQCN references. |
| AC-7: Documentation reflects security decisions | Pass | `AGENTS.md`, `.env.example`, `DOCS/ARCHITECTURE.md`, `DOCS/TECH_CHANGELOG.md`, `.paul/codebase/concerns.md`, and `.paul/codebase/tech_changelog.md` updated. |
## Accomplishments
- SMTP tests no longer silently accept unverified TLS certificates by default.
- STARTTLS now connects plain, requires server STARTTLS support, enables crypto, then repeats EHLO after upgrade.
- `SMTP_ALLOW_SELF_SIGNED_DEV` documents and gates the only insecure certificate path, and production-like environments stay strict.
- E-mail and SMS templates share one variable catalog and reject unknown new placeholders before repository save.
- Session read/write behavior is centralized behind `Session::get()`, `set()`, `has()`, `forget()`, and `pull()`.
- Targeted accounting/order/user views use `$component()` for shared partial rendering.
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `.env.example` | Modified | Documents `SMTP_ALLOW_SELF_SIGNED_DEV=false`. |
| `AGENTS.md` | Modified | Persists SMTP TLS project rule. |
| `config/app.php` | Modified | Adds local/dev-only SMTP self-signed config. |
| `routes/web.php` | Modified | Injects `SmtpSecurityContextFactory` into mailbox controller. |
| `src/Modules/Settings/SmtpSecurityContextFactory.php` | Created | Builds strict/dev SMTP stream contexts. |
| `src/Modules/Settings/EmailMailboxController.php` | Modified | Uses strict SSL/STARTTLS handling and clear TLS failures. |
| `src/Modules/Settings/TemplateVariableCatalog.php` | Created | Shared e-mail/SMS placeholder catalog and validation. |
| `src/Modules/Settings/EmailTemplateController.php` | Modified | Blocks unknown variables and reuses shared preview data. |
| `src/Modules/Settings/SmsTemplateController.php` | Modified | Blocks unknown variables and reuses shared catalog. |
| `src/Core/Support/Session.php` | Modified | Adds session helper API. |
| `src/Core/Security/Csrf.php` | Modified | Uses `Session` helper. |
| `src/Core/Support/Flash.php` | Modified | Uses `Session` helper. |
| `src/Modules/Auth/AuthService.php` | Modified | Uses `Session` helper. |
| `src/Modules/Settings/AllegroIntegrationController.php` | Modified | Uses `Session` helper for OAuth state. |
| `src/Core/View/Template.php` | Modified | Adds `$component()` helper. |
| `resources/views/accounting/index.php` | Modified | Uses `$component('components/table-list')`. |
| `resources/views/orders/list.php` | Modified | Uses `$component()` for status panel, table list, preview modal. |
| `resources/views/orders/show.php` | Modified | Imports classes and uses `$component()` for targeted partials. |
| `resources/views/users/index.php` | Modified | Uses `$component('components/table-list')`. |
| `tests/Unit/SmtpSecurityContextFactoryTest.php` | Created | Covers strict/dev SMTP context options. |
| `tests/Unit/TemplateVariableCatalogTest.php` | Created | Covers known and unknown placeholders. |
| `DOCS/ARCHITECTURE.md` | Modified | Documents Phase 138 security/session/view boundaries. |
| `DOCS/TECH_CHANGELOG.md` | Modified | Documents Phase 138 technical changes and breaking note. |
| `.paul/codebase/concerns.md` | Modified | Marks Phase 138 concerns resolved/partly resolved. |
| `.paul/codebase/tech_changelog.md` | Modified | Adds PAUL technical changelog entry. |
## Verification Results
| Check | Result |
|-------|--------|
| `C:\xampp\php\php.exe -l` on all touched PHP source, view, config, route and test files | Pass |
| `rg -n '\$_SESSION' src` | Pass - raw session access only in `src/Core/Support/Session.php` |
| `rg -n '\brequire\b'` on targeted views | Pass - no targeted hard component requires left |
| `rg -n '\\App\\' resources/views/orders/show.php` | Pass - no inline fully-qualified view class calls |
| `git diff --check` | Pass |
| `vendor/bin/phpunit tests/Unit/SmtpSecurityContextFactoryTest.php tests/Unit/TemplateVariableCatalogTest.php` | Not run - `vendor/bin/phpunit` missing in checkout |
| `sonar-scanner --version` | Attempted; not run - `sonar-scanner` missing in PATH |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Strict TLS by default, local/dev-only override for self-signed SMTP | Security default must be safe; dev servers may still need self-signed smoke tests. | Production mailbox tests fail closed on invalid certificates. |
| Block unknown placeholders only on create/edit | Avoids silent template mistakes without mutating existing stored templates. | Existing legacy rows continue resolving at send time until edited. |
| Keep runtime resolvers compatible | Send flows should not change beyond save-time validation. | Low regression risk for existing order email/SMS flows. |
| Component helper is selective | Phase 138 targets confirmed legacy view patterns, not a broad alert include rewrite. | Existing Phase 120 alert include pattern remains accepted. |
## Deviations From Plan
| Type | Count | Impact |
|------|-------|--------|
| Environment gap | 2 | PHPUnit and Sonar could not run because tools are unavailable locally; gaps are documented. |
| Scope additions | 1 | `AGENTS.md` was updated to persist the SMTP/TLS rule required by project policy. |
| Deferred | 0 | No new implementation work was deferred beyond manual smoke/tooling follow-ups. |
## Issues Encountered
| Issue | Resolution |
|-------|------------|
| `vendor/bin/phpunit` missing | New tests were linted and follow-up was recorded for dependency-installed environment. |
| `sonar-scanner` missing in PATH | Required scan was attempted and gap documented in SUMMARY/STATE. |
## Next Phase Readiness
Ready:
- Phase 139 can focus on refreshed Sonar critical/major cleanup with SMTP TLS and raw session false positives reduced.
- Phase 141/142 can build on `Session`, `TemplateVariableCatalog`, and `$component()` instead of repeating legacy patterns.
Concerns:
- Live SMTP and template UI smoke tests remain manual because no credentials/browser session were available during APPLY.
- Sonar baseline remains stale until `sonar-scanner` is available.
Blockers:
- None for moving to Phase 139 planning.
---
*Phase: 138-security-and-legacy-hardening, Plan: 01*
*Completed: 2026-05-17*

View File

@@ -5,6 +5,10 @@
- Nie podpinaj `DB_HOST_REMOTE` do runtime aplikacji.
- Runtime aplikacji ma korzystac standardowo z `DB_HOST`.
## SMTP i TLS
- Test skrzynki e-mail ma domyslnie weryfikowac certyfikat TLS (`verify_peer` i `verify_peer_name`).
- `SMTP_ALLOW_SELF_SIGNED_DEV=true` wolno traktowac tylko jako lokalny/dev override dla self-signed/unverified certyfikatow; nie uzywac w produkcji.
## Zasady pisania kodu
- Kod ma być czytelny „dla obcego”: jasne nazwy, mało magii
- Brak „skrótów na szybko” typu logika w widokach, copy-paste, losowe helpery bez spójności

View File

@@ -56,6 +56,26 @@ HTTP Request
- `AutomationRepository::listOrderIntegrations()` uses the registry integration types so order automation conditions include active Erli integrations together with Allegro/shopPRO.
- `OrdersStatisticsRepository` uses the registry for marketplace source filtering. Statistics channels always include Allegro and Erli; shopPRO remains split per integration as `shoppro:{integration_id}`.
## Phase 138 Security and Legacy Hardening
### SMTP mailbox test TLS policy
- `SmtpSecurityContextFactory` (`src/Modules/Settings/SmtpSecurityContextFactory.php`) builds SSL stream context options for `/settings/email-mailboxes/test`.
- `EmailMailboxController::testConnection()` treats `ssl` as implicit TLS and `tls` as STARTTLS. Both verify peer and host name by default.
- `SMTP_ALLOW_SELF_SIGNED_DEV=true` is honored only when `APP_ENV` is `local`, `dev`, `development`, or `testing`; production-like envs stay strict.
- `CURL_CA_BUNDLE_PATH` is reused through `SslCertificateResolver` when strict verification needs an explicit CA bundle.
### Template variable policy
- `TemplateVariableCatalog` (`src/Modules/Settings/TemplateVariableCatalog.php`) is the shared e-mail/SMS catalog for documented `{{group.variable}}` placeholders and preview sample data.
- `EmailTemplateController::save()` validates e-mail subject and body before writing; `SmsTemplateController::save()` validates SMS body.
- Unknown placeholders are rejected with a readable flash message. Existing stored templates are not migrated; validation applies when a template is created or edited.
- Runtime resolvers remain compatible: e-mail and SMS sending still resolve values from order, company and latest shipment data.
### Session and view boundaries
- `Session` centralizes session read/write helpers (`get`, `set`, `has`, `forget`, `pull`) while preserving `start()` and `regenerate()`.
- `AuthService`, `Csrf`, `Flash` and Allegro OAuth state use the session helper; raw `$_SESSION` is isolated to `Session`.
- `Template::renderFile()` exposes a `$component($view, $params = [])` helper for reusable PHP components and partials.
- Phase 138 migrated targeted hard `require` usages in order/accounting/user views to `$component()`. Alert component includes from Phase 120 remain an accepted current pattern unless a later broad component-rendering refactor is planned.
## Frontend Enhancement Modules
### Checkbox Multiselect (`public/assets/js/modules/checkbox-multiselect.js`)

View File

@@ -1,5 +1,36 @@
# Technical Changelog
## 2026-05-17 - Phase 138 Plan 01: Security and Legacy Hardening
**Co zrobiono:**
- Utwardzono test skrzynki SMTP: `ssl` uzywa implicit TLS, `tls` wykonuje STARTTLS, a oba tryby domyslnie weryfikuja peer i host name.
- Dodano `SmtpSecurityContextFactory` oraz jawny lokalny override `SMTP_ALLOW_SELF_SIGNED_DEV` dzialajacy tylko w `APP_ENV=local/dev/development/testing`.
- Dodano `TemplateVariableCatalog` jako wspolny katalog zmiennych e-mail/SMS i walidacje zapisu szablonow. Nieznane `{{grupa.zmienna}}` blokuja zapis z czytelnym komunikatem.
- Rozszerzono `Session` o helpery `get/set/has/forget/pull` i przepieto `AuthService`, `Csrf`, `Flash` oraz Allegro OAuth state z raw `$_SESSION`.
- Dodano helper `$component()` w `Template` i zmigrowano wskazane `require` w widokach zamowien, ksiegowosci i uzytkownikow; `orders/show.php` nie uzywa juz inline `\App\...` FQCN.
- Dodano testy jednostkowe dla polityki SMTP context oraz katalogu zmiennych.
**Dlaczego:**
- Phase 134 potwierdzil aktywne ryzyko: test SMTP akceptowal certyfikaty bez weryfikacji. Dodatkowo polityka zmiennych szablonow byla niespojna, a legacy session/view patterns utrudnialy dalsze porzadkowanie.
**BREAKING / migracja:**
- Brak migracji DB.
- Nowe lub edytowane szablony e-mail/SMS z nieznanymi placeholderami nie zapisza sie; istniejace rekordy nie sa backfillowane.
- Self-signed/unverified certyfikaty SMTP wymagaja jawnego lokalnego `SMTP_ALLOW_SELF_SIGNED_DEV=true`; w produkcji pozostaje strict TLS.
## 2026-05-17 - Phase 137 Plan 01: Delivery Status Backlog Verification
**Co zrobiono:**
- Zweryfikowano, ze `DELIVERY-STATUS-MGMT` jest wdrozone: statusy dostawy sa DB-driven, a automatyzacje uzywaja `DeliveryStatus::getAllOptions()` / `getAllStatuses()`.
- Potwierdzono, ze `SHIPMENT_STATUS_OPTIONS` i `SHIPMENT_STATUS_OPTION_MAP` nie wystepuja juz w runtime source.
- Wykonano read-only check danych automatyzacji przez `DB_HOST_REMOTE` (lokalny MySQL byl offline): 3 warunki `shipment_status`, 0 akcji `update_shipment_status`, 0 starych kluczy i 0 kluczy spoza `delivery_statuses`.
**Dlaczego:**
- Phase 108 wprowadzila breaking change dla starych grupowych kluczy statusow przesylek. Phase 137 zamyka operacyjne ryzyko: aktualne reguly nie wymagaja recznej migracji.
**BREAKING / migracja:**
- Brak zmian kodu runtime i brak migracji.
## 2026-05-17 - Phase 136 Plan 01: Fakturownia Invoice Idempotency
**Co zrobiono:**

View File

@@ -16,6 +16,10 @@ return [
'integrations' => [
'secret' => Env::get('INTEGRATIONS_SECRET', ''),
],
'smtp' => [
'allow_self_signed_dev' => Env::bool('SMTP_ALLOW_SELF_SIGNED_DEV', false)
&& in_array(Env::get('APP_ENV', 'production'), ['local', 'dev', 'development', 'testing'], true),
],
'cron' => [
'run_on_web_default' => Env::bool('CRON_RUN_ON_WEB', false),
'web_limit_default' => max(1, min(100, (int) Env::get('CRON_WEB_LIMIT', '5'))),

View File

@@ -28,7 +28,7 @@ $queryData = is_array($tableList['query'] ?? null) ? $tableList['query'] : [];
<div id="exportSelectedIds"></div>
</form>
<?php require __DIR__ . '/../components/table-list.php'; ?>
<?php $component('components/table-list'); ?>
<script>
(function() {

View File

@@ -2,7 +2,10 @@
<?php $statusPanelTitle = 'Statusy'; ?>
<section class="order-show-layout">
<?php require __DIR__ . '/../components/order-status-panel.php'; ?>
<?php $component('components/order-status-panel', [
'statusPanelList' => $statusPanelList,
'statusPanelTitle' => $statusPanelTitle,
]); ?>
<div class="order-show-main">
<section class="card orders-list-page">
@@ -17,11 +20,11 @@
<?php endif; ?>
</section>
<?php require __DIR__ . '/../components/table-list.php'; ?>
<?php $component('components/table-list'); ?>
</div>
</section>
<?php require __DIR__ . '/partials/preview-modal.php'; ?>
<?php $component('orders/partials/preview-modal'); ?>
<script type="application/json" id="js-inline-status-config"><?= json_encode([
'allStatuses' => is_array($allStatuses ?? null) ? $allStatuses : [],

View File

@@ -1,4 +1,7 @@
<?php
use App\Core\Support\StringHelper;
use App\Modules\Shipments\DeliveryStatus;
$orderRow = is_array($order ?? null) ? $order : [];
$itemsList = is_array($items ?? null) ? $items : [];
$addressesList = is_array($addresses ?? null) ? $addresses : [];
@@ -46,7 +49,10 @@ foreach ($addressesList as $address) {
?>
<section class="order-show-layout">
<?php require __DIR__ . '/../components/order-status-panel.php'; ?>
<?php $component('components/order-status-panel', [
'statusPanelList' => $statusPanelList,
'statusPanelTitle' => $statusPanelTitle,
]); ?>
<div class="order-show-main">
<section class="card order-details-page">
@@ -289,7 +295,7 @@ foreach ($addressesList as $address) {
$deliveryMethodValue = trim((string) ($orderRow['delivery_method'] ?? ''));
$paymentMethodValue = trim((string) ($orderRow['payment_method'] ?? ''));
$paymentTypeCurrent = strtoupper(trim((string) ($orderRow['external_payment_type_id'] ?? '')));
$isCodCurrent = \App\Core\Support\StringHelper::isCodPayment($paymentTypeCurrent);
$isCodCurrent = StringHelper::isCodPayment($paymentTypeCurrent);
?>
<form method="POST" action="/orders/<?= (int) $orderId ?>/details/update" class="order-details-edit-form js-details-edit-form" style="display:none">
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
@@ -360,7 +366,7 @@ foreach ($addressesList as $address) {
];
$paymentTypeLabel = $paymentTypeLabels[$paymentTypeRaw] ?? ($paymentTypeRaw !== '' ? $paymentTypeRaw : '-');
?>
<?php if (\App\Core\Support\StringHelper::isCodPayment($paymentTypeRaw)): ?>
<?php if (StringHelper::isCodPayment($paymentTypeRaw)): ?>
<span class="order-tag is-cod"><?= $e($paymentTypeLabel) ?></span>
<?php else: ?>
<?= $e($paymentTypeLabel) ?>
@@ -383,14 +389,14 @@ foreach ($addressesList as $address) {
}
if ($latestDeliveryPkg !== null):
$ldStatus = (string) ($latestDeliveryPkg['delivery_status'] ?? 'unknown');
$ldLabel = \App\Modules\Shipments\DeliveryStatus::label($ldStatus);
$ldLabel = DeliveryStatus::label($ldStatus);
$ldDate = trim((string) ($latestDeliveryPkg['delivery_status_updated_at'] ?? ''));
?>
<dt>Status dostawy</dt>
<dd>
<?php
$ldIsSystem = in_array($ldStatus, \App\Modules\Shipments\DeliveryStatus::ALL_STATUSES, true);
$ldColor = $ldIsSystem ? '' : \App\Modules\Shipments\DeliveryStatus::getColor($ldStatus);
$ldIsSystem = in_array($ldStatus, DeliveryStatus::ALL_STATUSES, true);
$ldColor = $ldIsSystem ? '' : DeliveryStatus::getColor($ldStatus);
?>
<span class="delivery-badge<?= $ldIsSystem ? ' delivery-badge--' . $e($ldStatus) : ' delivery-badge--custom' ?>"<?= $ldColor !== '' ? ' style="--status-color: ' . $e($ldColor) . '"' : '' ?>><?= $e($ldLabel) ?></span>
<?php if ($ldDate !== ''): ?><small class="muted" style="margin-left:4px"><?= $e($ldDate) ?></small><?php endif; ?>
@@ -672,19 +678,19 @@ foreach ($addressesList as $address) {
<?php
$pkgDeliveryStatus = (string) ($pkg['delivery_status'] ?? 'unknown');
$pkgDeliveryRaw = trim((string) ($pkg['delivery_status_raw'] ?? ''));
$pkgDeliveryLabel = \App\Modules\Shipments\DeliveryStatus::label($pkgDeliveryStatus);
$pkgDeliveryDesc = $pkgDeliveryRaw !== '' ? \App\Modules\Shipments\DeliveryStatus::description($pkgProvider, $pkgDeliveryRaw) : '';
$pkgDeliveryLabel = DeliveryStatus::label($pkgDeliveryStatus);
$pkgDeliveryDesc = $pkgDeliveryRaw !== '' ? DeliveryStatus::description($pkgProvider, $pkgDeliveryRaw) : '';
$pkgDeliveryTitle = $pkgDeliveryRaw !== '' ? ($pkgDeliveryRaw . ' — ' . $pkgDeliveryDesc) : '';
?>
<?php
$pkgIsSystem = in_array($pkgDeliveryStatus, \App\Modules\Shipments\DeliveryStatus::ALL_STATUSES, true);
$pkgColor = $pkgIsSystem ? '' : \App\Modules\Shipments\DeliveryStatus::getColor($pkgDeliveryStatus);
$pkgIsSystem = in_array($pkgDeliveryStatus, DeliveryStatus::ALL_STATUSES, true);
$pkgColor = $pkgIsSystem ? '' : DeliveryStatus::getColor($pkgDeliveryStatus);
?>
<span class="delivery-badge<?= $pkgIsSystem ? ' delivery-badge--' . $e($pkgDeliveryStatus) : ' delivery-badge--custom' ?>" title="<?= $e($pkgDeliveryTitle) ?>"<?= $pkgColor !== '' ? ' style="--status-color: ' . $e($pkgColor) . '"' : '' ?>><?= $e($pkgDeliveryLabel) ?></span>
</td>
<td style="white-space:nowrap" data-pkg-tracking-cell="<?= $e((string) ($pkg['id'] ?? 0)) ?>">
<?= $e($pkgTracking !== '' ? $pkgTracking : '-') ?><?php
$pkgTrackUrl = \App\Modules\Shipments\DeliveryStatus::trackingUrl($pkgProvider, $pkgTracking, $pkgCarrierId);
$pkgTrackUrl = DeliveryStatus::trackingUrl($pkgProvider, $pkgTracking, $pkgCarrierId);
if ($pkgTrackUrl !== null): ?> <a href="<?= $e($pkgTrackUrl) ?>" target="_blank" class="tracking-link" title="Sledz przesylke">&#128279;</a><?php endif; ?>
</td>
<td><?php if ($isManual): ?><?= $e($pkgCarrierId !== '' ? $pkgCarrierId : 'Reczna') ?><?php elseif ($pkgCarrierId !== ''): ?><?= $e($pkgProviderLabel) ?> &rarr; <?= $e($pkgCarrierId) ?><?php elseif ($pkgProviderLabel !== ''): ?><?= $e($pkgProviderLabel) ?><?php else: ?>-<?php endif; ?></td>
@@ -903,7 +909,7 @@ foreach ($addressesList as $address) {
<tr>
<td class="text-nowrap"><?= $e($payDate !== '' ? $payDate : '—') ?></td>
<td>
<?php if (\App\Core\Support\StringHelper::isCodPayment($ptRaw)): ?>
<?php if (StringHelper::isCodPayment($ptRaw)): ?>
<span class="order-tag is-cod"><?= $e($ptLabel) ?></span>
<?php else: ?>
<?= $e($ptLabel) ?>
@@ -1512,4 +1518,4 @@ function copyBuyerName(btn, text) {
}
</script>
<?php require __DIR__ . '/partials/email-send-modal.php'; ?>
<?php $component('orders/partials/email-send-modal', ['addressesList' => $addressesList]); ?>

View File

@@ -36,4 +36,4 @@
</form>
</section>
<?php require __DIR__ . '/../components/table-list.php'; ?>
<?php $component('components/table-list'); ?>

View File

@@ -72,6 +72,7 @@ use App\Modules\Settings\EmailTemplateController;
use App\Modules\Settings\EmailTemplateRepository;
use App\Modules\Settings\SmsTemplateController;
use App\Modules\Settings\IntegrationSecretCipher;
use App\Modules\Settings\SmtpSecurityContextFactory;
use App\Modules\Email\AttachmentGenerator;
use App\Modules\Email\EmailSendingService;
use App\Modules\Email\VariableResolver;
@@ -343,7 +344,8 @@ return static function (Application $app): void {
$template,
$translator,
$auth,
$emailMailboxRepository
$emailMailboxRepository,
new SmtpSecurityContextFactory((bool) $app->config('app.smtp.allow_self_signed_dev', false))
);
$emailTemplateRepository = new EmailTemplateRepository($app->db());
$emailTemplateController = new EmailTemplateController(

View File

@@ -3,17 +3,19 @@ declare(strict_types=1);
namespace App\Core\Security;
use App\Core\Support\Session;
final class Csrf
{
private const SESSION_KEY = '_csrf_token';
public static function token(): string
{
if (empty($_SESSION[self::SESSION_KEY])) {
$_SESSION[self::SESSION_KEY] = bin2hex(random_bytes(32));
if (!is_string(Session::get(self::SESSION_KEY)) || Session::get(self::SESSION_KEY) === '') {
Session::set(self::SESSION_KEY, bin2hex(random_bytes(32)));
}
return (string) $_SESSION[self::SESSION_KEY];
return (string) Session::get(self::SESSION_KEY);
}
public static function validate(?string $token): bool
@@ -22,7 +24,7 @@ final class Csrf
return false;
}
$storedToken = (string) ($_SESSION[self::SESSION_KEY] ?? '');
$storedToken = (string) Session::get(self::SESSION_KEY, '');
if ($storedToken === '') {
return false;
}

View File

@@ -11,28 +11,29 @@ final class Flash
public static function set(string $key, mixed $value): void
{
if (!isset($_SESSION[self::FLASH_KEY]) || !is_array($_SESSION[self::FLASH_KEY])) {
$_SESSION[self::FLASH_KEY] = [];
$flash = Session::get(self::FLASH_KEY, []);
if (!is_array($flash)) {
$flash = [];
}
$_SESSION[self::FLASH_KEY][$key] = $value;
$flash[$key] = $value;
Session::set(self::FLASH_KEY, $flash);
}
public static function get(string $key, mixed $default = null): mixed
{
if (
!isset($_SESSION[self::FLASH_KEY]) ||
!is_array($_SESSION[self::FLASH_KEY]) ||
!array_key_exists($key, $_SESSION[self::FLASH_KEY])
) {
$flash = Session::get(self::FLASH_KEY, []);
if (!is_array($flash) || !array_key_exists($key, $flash)) {
return $default;
}
$value = $_SESSION[self::FLASH_KEY][$key];
unset($_SESSION[self::FLASH_KEY][$key]);
$value = $flash[$key];
unset($flash[$key]);
if (empty($_SESSION[self::FLASH_KEY])) {
unset($_SESSION[self::FLASH_KEY]);
if ($flash === []) {
Session::forget(self::FLASH_KEY);
} else {
Session::set(self::FLASH_KEY, $flash);
}
return $value;
@@ -42,14 +43,16 @@ final class Flash
{
$normalizedType = in_array($type, self::ALLOWED_TYPES, true) ? $type : 'info';
if (!isset($_SESSION[self::QUEUE_KEY]) || !is_array($_SESSION[self::QUEUE_KEY])) {
$_SESSION[self::QUEUE_KEY] = [];
$queue = Session::get(self::QUEUE_KEY, []);
if (!is_array($queue)) {
$queue = [];
}
$_SESSION[self::QUEUE_KEY][] = [
$queue[] = [
'type' => $normalizedType,
'message' => $message,
];
Session::set(self::QUEUE_KEY, $queue);
}
/**
@@ -61,8 +64,9 @@ final class Flash
{
$entries = [];
if (isset($_SESSION[self::QUEUE_KEY]) && is_array($_SESSION[self::QUEUE_KEY])) {
foreach ($_SESSION[self::QUEUE_KEY] as $entry) {
$queue = Session::pull(self::QUEUE_KEY, []);
if (is_array($queue)) {
foreach ($queue as $entry) {
if (!is_array($entry)) {
continue;
}
@@ -76,11 +80,11 @@ final class Flash
}
$entries[] = ['type' => $type, 'message' => $message];
}
unset($_SESSION[self::QUEUE_KEY]);
}
if (isset($_SESSION[self::FLASH_KEY]) && is_array($_SESSION[self::FLASH_KEY])) {
foreach ($_SESSION[self::FLASH_KEY] as $key => $value) {
$flash = Session::pull(self::FLASH_KEY, []);
if (is_array($flash)) {
foreach ($flash as $key => $value) {
if (!is_string($value) || $value === '') {
continue;
}
@@ -89,7 +93,6 @@ final class Flash
'message' => $value,
];
}
unset($_SESSION[self::FLASH_KEY]);
}
return $entries;

View File

@@ -27,4 +27,36 @@ final class Session
session_regenerate_id(true);
}
public static function get(string $key, mixed $default = null): mixed
{
return array_key_exists($key, $_SESSION) ? $_SESSION[$key] : $default;
}
public static function set(string $key, mixed $value): void
{
$_SESSION[$key] = $value;
}
public static function has(string $key): bool
{
return array_key_exists($key, $_SESSION);
}
public static function forget(string $key): void
{
unset($_SESSION[$key]);
}
public static function pull(string $key, mixed $default = null): mixed
{
if (!self::has($key)) {
return $default;
}
$value = $_SESSION[$key];
self::forget($key);
return $value;
}
}

View File

@@ -38,6 +38,9 @@ final class Template
$e = static fn (mixed $value): string => htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
$translator = $this->translator;
$t = static fn (string $key, array $replace = []): string => $translator->get($key, $replace);
$component = function (string $view, array $params = []) use ($data): void {
echo $this->renderFile($view, array_merge($data, $params));
};
ob_start();
extract($data, EXTR_SKIP);

View File

@@ -30,19 +30,19 @@ final class AuthService
Session::regenerate();
$_SESSION[self::SESSION_USER_KEY] = [
Session::set(self::SESSION_USER_KEY, [
'id' => (int) ($storedUser['id'] ?? 0),
'name' => (string) ($storedUser['name'] ?? ''),
'email' => strtolower((string) ($storedUser['email'] ?? '')),
'login_at' => date(DATE_ATOM),
];
]);
return true;
}
public function check(): bool
{
return isset($_SESSION[self::SESSION_USER_KEY]) && is_array($_SESSION[self::SESSION_USER_KEY]);
return is_array(Session::get(self::SESSION_USER_KEY));
}
/**
@@ -55,7 +55,7 @@ final class AuthService
}
/** @var array<string, mixed> $user */
$user = $_SESSION[self::SESSION_USER_KEY];
$user = Session::get(self::SESSION_USER_KEY);
return $user;
}
@@ -90,12 +90,12 @@ final class AuthService
Session::regenerate();
$_SESSION[self::SESSION_USER_KEY] = [
Session::set(self::SESSION_USER_KEY, [
'id' => (int) $user['id'],
'name' => (string) $user['name'],
'email' => (string) $user['email'],
'login_at' => date(DATE_ATOM),
];
]);
return true;
}
@@ -108,7 +108,7 @@ final class AuthService
}
$this->clearRememberCookie();
unset($_SESSION[self::SESSION_USER_KEY]);
Session::forget(self::SESSION_USER_KEY);
Session::regenerate();
}

View File

@@ -17,6 +17,7 @@ use App\Core\Constants\IntegrationSources;
use App\Core\Constants\RedirectPaths;
use App\Core\Exceptions\IntegrationConfigException;
use App\Core\Http\RedirectPathResolver;
use App\Core\Support\Session;
use Throwable;
final class AllegroIntegrationController
@@ -247,7 +248,7 @@ final class AllegroIntegrationController
try {
$credentials = $this->requireOAuthCredentials();
$state = bin2hex(random_bytes(24));
$_SESSION[self::OAUTH_STATE_SESSION_KEY] = $state;
Session::set(self::OAUTH_STATE_SESSION_KEY, $state);
$url = $this->oauthClient->buildAuthorizeUrl(
(string) $credentials['environment'],
@@ -278,8 +279,7 @@ final class AllegroIntegrationController
}
$state = trim((string) $request->input('state', ''));
$expectedState = trim((string) ($_SESSION[self::OAUTH_STATE_SESSION_KEY] ?? ''));
unset($_SESSION[self::OAUTH_STATE_SESSION_KEY]);
$expectedState = trim((string) Session::pull(self::OAUTH_STATE_SESSION_KEY, ''));
$authorizationCode = trim((string) $request->input('code', ''));
$validationError = $this->validateOAuthCallbackParams($state, $expectedState, $authorizationCode);

View File

@@ -18,7 +18,8 @@ final class EmailMailboxController
private readonly Template $template,
private readonly Translator $translator,
private readonly AuthService $auth,
private readonly EmailMailboxRepository $repository
private readonly EmailMailboxRepository $repository,
private readonly SmtpSecurityContextFactory $smtpSecurityContextFactory
) {
}
@@ -172,15 +173,12 @@ final class EmailMailboxController
return Response::json(['success' => false, 'message' => 'Serwer SMTP i uzytkownik sa wymagane'], 400);
}
$prefix = $encryption === 'ssl' ? 'ssl://' : ($encryption === 'tls' ? 'tls://' : '');
$prefix = $encryption === 'ssl' ? 'ssl://' : '';
$address = $prefix . $host . ':' . $port;
$context = stream_context_create([
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
]);
$context = in_array($encryption, ['ssl', 'tls'], true)
? $this->smtpSecurityContextFactory->create($host)
: stream_context_create();
$errorMessage = '';
set_error_handler(function (int $errno, string $errstr) use (&$errorMessage): bool {
@@ -188,6 +186,7 @@ final class EmailMailboxController
return true;
});
$socket = null;
try {
$socket = @stream_socket_client($address, $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $context);
@@ -211,21 +210,14 @@ final class EmailMailboxController
$this->sendSmtpCommand($socket, "EHLO orderpro\r\n");
$ehloResponse = $this->readSmtpResponse($socket);
if ($encryption === 'tls' && !str_starts_with($prefix, 'tls://')) {
if (str_contains($ehloResponse, 'STARTTLS')) {
$this->sendSmtpCommand($socket, "STARTTLS\r\n");
$starttlsResponse = $this->readSmtpResponse($socket);
if (!str_starts_with($starttlsResponse, '220')) {
fclose($socket);
if ($encryption === 'tls') {
$startTlsFailure = $this->upgradeToStartTls($socket, $ehloResponse);
if ($startTlsFailure !== null) {
return Response::json([
'success' => false,
'message' => "STARTTLS nie powiodlo sie: {$starttlsResponse}",
'message' => $startTlsFailure,
]);
}
stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
$this->sendSmtpCommand($socket, "EHLO orderpro\r\n");
$this->readSmtpResponse($socket);
}
}
$this->sendSmtpCommand($socket, "AUTH LOGIN\r\n");
@@ -271,6 +263,9 @@ final class EmailMailboxController
'message' => "Blad: {$e->getMessage()}",
]);
} finally {
if (is_resource($socket)) {
fclose($socket);
}
restore_error_handler();
}
}
@@ -300,4 +295,35 @@ final class EmailMailboxController
return trim($response);
}
/**
* @param resource $socket
*/
private function upgradeToStartTls($socket, string $ehloResponse): ?string
{
if (!str_contains($ehloResponse, 'STARTTLS')) {
return 'Serwer nie obsluguje STARTTLS.';
}
$this->sendSmtpCommand($socket, "STARTTLS\r\n");
$starttlsResponse = $this->readSmtpResponse($socket);
if (!str_starts_with($starttlsResponse, '220')) {
return "STARTTLS nie powiodlo sie: {$starttlsResponse}";
}
$cryptoEnabled = @stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
if ($cryptoEnabled !== true) {
return $this->smtpSecurityContextFactory->allowsSelfSignedDev()
? 'Nie udalo sie wlaczyc szyfrowania STARTTLS.'
: 'Nie udalo sie zweryfikowac certyfikatu TLS serwera SMTP.';
}
$this->sendSmtpCommand($socket, "EHLO orderpro\r\n");
$postTlsEhlo = $this->readSmtpResponse($socket);
if (!str_starts_with($postTlsEhlo, '250')) {
return "Serwer nie odpowiedzial poprawnie po STARTTLS: {$postTlsEhlo}";
}
return null;
}
}

View File

@@ -15,77 +15,10 @@ use Throwable;
final class EmailTemplateController
{
private const VARIABLE_GROUPS = [
'zamowienie' => [
'label' => 'Zamowienie',
'vars' => [
'numer' => 'Numer wewnetrzny (OP...)',
'numer_zewnetrzny' => 'Numer z platformy',
'zrodlo' => 'Zrodlo (Allegro/shopPRO/Erli)',
'kwota' => 'Kwota brutto',
'waluta' => 'Waluta (PLN/EUR/...)',
'data' => 'Data zamowienia',
],
],
'kupujacy' => [
'label' => 'Kupujacy',
'vars' => [
'imie_nazwisko' => 'Imie i nazwisko',
'email' => 'Adres e-mail',
'telefon' => 'Telefon',
'login' => 'Login platformy',
],
],
'adres' => [
'label' => 'Adres dostawy',
'vars' => [
'ulica' => 'Ulica z numerem',
'miasto' => 'Miasto',
'kod_pocztowy' => 'Kod pocztowy',
'kraj' => 'Kraj',
],
],
'firma' => [
'label' => 'Firma',
'vars' => [
'nazwa' => 'Nazwa firmy',
'nip' => 'NIP',
],
],
'przesylka' => [
'label' => 'Przesylka',
'vars' => [
'numer' => 'Numer przesylki (tracking)',
'link_sledzenia' => 'Link sledzenia zalezny od kuriera',
],
],
];
private const ATTACHMENT_TYPES = [
'receipt' => 'Paragon',
];
private const SAMPLE_DATA = [
'zamowienie.numer' => 'OP000001234',
'zamowienie.numer_zewnetrzny' => 'ALG-98765432',
'zamowienie.zrodlo' => 'Allegro',
'zamowienie.kwota' => '149,99',
'zamowienie.waluta' => 'PLN',
'zamowienie.data' => '2026-03-16',
'kupujacy.imie_nazwisko' => 'Jan Kowalski',
'kupujacy.email' => 'jan.kowalski@example.com',
'kupujacy.telefon' => '+48 600 123 456',
'kupujacy.login' => 'jankowalski82',
'adres.ulica' => 'ul. Dluga 15/3',
'adres.miasto' => 'Warszawa',
'adres.kod_pocztowy' => '00-238',
'adres.kraj' => 'PL',
'firma.nazwa' => 'Przykladowa Firma Sp. z o.o.',
'firma.nip' => '5271234567',
'przesylka.numer' => '123456789012345678901234',
'przesylka.link_sledzenia' => 'https://inpost.pl/sledzenie-przesylek?number=123456789012345678901234',
];
public function __construct(
private readonly Template $template,
private readonly Translator $translator,
@@ -151,6 +84,15 @@ final class EmailTemplateController
return Response::redirect($formPath);
}
$unknownVariables = TemplateVariableCatalog::findUnknownPlaceholders($subject, $bodyHtml);
if ($unknownVariables !== []) {
Flash::set(
'settings.email_templates.error',
'Nieznane zmienne w szablonie: ' . implode(', ', $unknownVariables)
);
return Response::redirect($formPath);
}
$attachment1Raw = trim((string) $request->input('attachment_1', ''));
$attachment1 = isset(self::ATTACHMENT_TYPES[$attachment1Raw]) ? $attachment1Raw : '';
@@ -250,8 +192,8 @@ final class EmailTemplateController
return Response::json([
'success' => true,
'subject' => self::resolveVariables($subject, self::SAMPLE_DATA),
'body_html' => self::resolveVariables($bodyHtml, self::SAMPLE_DATA),
'subject' => self::resolveVariables($subject, TemplateVariableCatalog::sampleData()),
'body_html' => self::resolveVariables($bodyHtml, TemplateVariableCatalog::sampleData()),
]);
}
@@ -259,7 +201,7 @@ final class EmailTemplateController
{
return Response::json([
'success' => true,
'groups' => self::VARIABLE_GROUPS,
'groups' => TemplateVariableCatalog::groups(),
]);
}
@@ -288,7 +230,7 @@ final class EmailTemplateController
'csrfToken' => Csrf::token(),
'mailboxes' => $this->mailboxRepository->listActive(),
'template' => $template,
'variableGroups' => self::VARIABLE_GROUPS,
'variableGroups' => TemplateVariableCatalog::groups(),
'attachmentTypes' => self::ATTACHMENT_TYPES,
'successMessage' => Flash::get('settings.email_templates.success', ''),
'errorMessage' => Flash::get('settings.email_templates.error', ''),

View File

@@ -16,52 +16,6 @@ use Throwable;
final class SmsTemplateController
{
private const VARIABLE_GROUPS = [
'zamowienie' => [
'label' => 'Zamowienie',
'vars' => [
'numer' => 'Numer wewnetrzny (OP...)',
'numer_zewnetrzny' => 'Numer z platformy',
'zrodlo' => 'Zrodlo (Allegro/shopPRO/Erli)',
'kwota' => 'Kwota brutto',
'waluta' => 'Waluta (PLN/EUR/...)',
'data' => 'Data zamowienia',
],
],
'kupujacy' => [
'label' => 'Kupujacy',
'vars' => [
'imie_nazwisko' => 'Imie i nazwisko',
'email' => 'Adres e-mail',
'telefon' => 'Telefon',
'login' => 'Login platformy',
],
],
'adres' => [
'label' => 'Adres dostawy',
'vars' => [
'ulica' => 'Ulica z numerem',
'miasto' => 'Miasto',
'kod_pocztowy' => 'Kod pocztowy',
'kraj' => 'Kraj',
],
],
'firma' => [
'label' => 'Firma',
'vars' => [
'nazwa' => 'Nazwa firmy',
'nip' => 'NIP',
],
],
'przesylka' => [
'label' => 'Przesylka',
'vars' => [
'numer' => 'Numer przesylki (tracking)',
'link_sledzenia' => 'Link sledzenia zalezny od kuriera',
],
],
];
public function __construct(
private readonly Template $template,
private readonly Translator $translator,
@@ -124,6 +78,15 @@ final class SmsTemplateController
return Response::redirect($formPath);
}
$unknownVariables = TemplateVariableCatalog::findUnknownPlaceholders($body);
if ($unknownVariables !== []) {
Flash::set(
'settings.sms_templates.error',
'Nieznane zmienne w szablonie: ' . implode(', ', $unknownVariables)
);
return Response::redirect($formPath);
}
try {
$this->repository->save([
'id' => $request->input('id', ''),
@@ -187,7 +150,7 @@ final class SmsTemplateController
{
return Response::json([
'success' => true,
'groups' => self::VARIABLE_GROUPS,
'groups' => TemplateVariableCatalog::groups(),
]);
}
@@ -200,7 +163,7 @@ final class SmsTemplateController
'user' => $this->auth->user(),
'csrfToken' => Csrf::token(),
'template' => $template,
'variableGroups' => self::VARIABLE_GROUPS,
'variableGroups' => TemplateVariableCatalog::groups(),
'successMessage' => Flash::get('settings.sms_templates.success', ''),
'errorMessage' => Flash::get('settings.sms_templates.error', ''),
], 'layouts/app');

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Modules\Settings;
use App\Core\Http\SslCertificateResolver;
final class SmtpSecurityContextFactory
{
public function __construct(private readonly bool $allowSelfSignedDev = false)
{
}
/**
* @return resource
*/
public function create(string $peerName)
{
return stream_context_create($this->options($peerName));
}
/**
* @return array{ssl: array<string, bool|string>}
*/
public function options(string $peerName): array
{
$verifyPeer = !$this->allowSelfSignedDev;
$options = [
'ssl' => [
'verify_peer' => $verifyPeer,
'verify_peer_name' => $verifyPeer,
'allow_self_signed' => $this->allowSelfSignedDev,
'peer_name' => $peerName,
],
];
$caPath = SslCertificateResolver::resolve();
if ($verifyPeer && $caPath !== null) {
$options['ssl']['cafile'] = $caPath;
}
return $options;
}
public function allowsSelfSignedDev(): bool
{
return $this->allowSelfSignedDev;
}
}

View File

@@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace App\Modules\Settings;
final class TemplateVariableCatalog
{
private const GROUPS = [
'zamowienie' => [
'label' => 'Zamowienie',
'vars' => [
'numer' => 'Numer wewnetrzny (OP...)',
'numer_zewnetrzny' => 'Numer z platformy',
'zrodlo' => 'Zrodlo (Allegro/shopPRO/Erli)',
'kwota' => 'Kwota brutto',
'waluta' => 'Waluta (PLN/EUR/...)',
'data' => 'Data zamowienia',
],
],
'kupujacy' => [
'label' => 'Kupujacy',
'vars' => [
'imie_nazwisko' => 'Imie i nazwisko',
'email' => 'Adres e-mail',
'telefon' => 'Telefon',
'login' => 'Login platformy',
],
],
'adres' => [
'label' => 'Adres dostawy',
'vars' => [
'ulica' => 'Ulica z numerem',
'miasto' => 'Miasto',
'kod_pocztowy' => 'Kod pocztowy',
'kraj' => 'Kraj',
],
],
'firma' => [
'label' => 'Firma',
'vars' => [
'nazwa' => 'Nazwa firmy',
'nip' => 'NIP',
],
],
'przesylka' => [
'label' => 'Przesylka',
'vars' => [
'numer' => 'Numer przesylki (tracking)',
'link_sledzenia' => 'Link sledzenia zalezny od kuriera',
],
],
];
private const SAMPLE_DATA = [
'zamowienie.numer' => 'OP000001234',
'zamowienie.numer_zewnetrzny' => 'ALG-98765432',
'zamowienie.zrodlo' => 'Allegro',
'zamowienie.kwota' => '149,99',
'zamowienie.waluta' => 'PLN',
'zamowienie.data' => '2026-03-16',
'kupujacy.imie_nazwisko' => 'Jan Kowalski',
'kupujacy.email' => 'jan.kowalski@example.com',
'kupujacy.telefon' => '+48 600 123 456',
'kupujacy.login' => 'jankowalski82',
'adres.ulica' => 'ul. Dluga 15/3',
'adres.miasto' => 'Warszawa',
'adres.kod_pocztowy' => '00-238',
'adres.kraj' => 'PL',
'firma.nazwa' => 'Przykladowa Firma Sp. z o.o.',
'firma.nip' => '5271234567',
'przesylka.numer' => '123456789012345678901234',
'przesylka.link_sledzenia' => 'https://inpost.pl/sledzenie-przesylek?number=123456789012345678901234',
];
/**
* @return array<string, array{label: string, vars: array<string, string>}>
*/
public static function groups(): array
{
return self::GROUPS;
}
/**
* @return array<string, string>
*/
public static function sampleData(): array
{
return self::SAMPLE_DATA;
}
/**
* @return list<string>
*/
public static function findUnknownPlaceholders(string ...$texts): array
{
$allowed = array_flip(self::allowedKeys());
$unknown = [];
foreach ($texts as $text) {
if ($text === '') {
continue;
}
preg_match_all('/\{\{([A-Za-z_]+)\.([A-Za-z_]+)\}\}/', $text, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$key = $match[1] . '.' . $match[2];
if (!isset($allowed[$key])) {
$unknown[$key] = true;
}
}
}
return array_keys($unknown);
}
/**
* @return list<string>
*/
private static function allowedKeys(): array
{
$keys = [];
foreach (self::GROUPS as $groupKey => $group) {
foreach ($group['vars'] as $variableKey => $_label) {
$keys[] = $groupKey . '.' . $variableKey;
}
}
return $keys;
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Tests\Unit;
use App\Modules\Settings\SmtpSecurityContextFactory;
use PHPUnit\Framework\TestCase;
final class SmtpSecurityContextFactoryTest extends TestCase
{
public function testDefaultContextVerifiesPeerAndHostName(): void
{
$factory = new SmtpSecurityContextFactory(false);
$options = $factory->options('smtp.example.com');
self::assertTrue($options['ssl']['verify_peer']);
self::assertTrue($options['ssl']['verify_peer_name']);
self::assertFalse($options['ssl']['allow_self_signed']);
self::assertSame('smtp.example.com', $options['ssl']['peer_name']);
}
public function testDevContextAllowsSelfSignedWithoutVerification(): void
{
$factory = new SmtpSecurityContextFactory(true);
$options = $factory->options('smtp.local');
self::assertFalse($options['ssl']['verify_peer']);
self::assertFalse($options['ssl']['verify_peer_name']);
self::assertTrue($options['ssl']['allow_self_signed']);
self::assertSame('smtp.local', $options['ssl']['peer_name']);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Tests\Unit;
use App\Modules\Settings\TemplateVariableCatalog;
use PHPUnit\Framework\TestCase;
final class TemplateVariableCatalogTest extends TestCase
{
public function testKnownPlaceholdersAreAllowed(): void
{
$unknown = TemplateVariableCatalog::findUnknownPlaceholders(
'Zamowienie {{zamowienie.numer}} dla {{kupujacy.imie_nazwisko}}',
'<p>{{przesylka.link_sledzenia}}</p>'
);
self::assertSame([], $unknown);
}
public function testUnknownPlaceholdersAreReturnedOnce(): void
{
$unknown = TemplateVariableCatalog::findUnknownPlaceholders(
'Bledna {{foo.bar}} i {{zamowienie.brak}}',
'Powtorka {{foo.bar}}'
);
self::assertSame(['foo.bar', 'zamowienie.brak'], $unknown);
}
}