From 29fc4d6edb0d8ef526cc67c491a8b926ca6c0031 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Wed, 25 Mar 2026 21:29:11 +0100 Subject: [PATCH] update --- .serena/project.yml | 14 +++ .../Controllers/ShopBasketController.php | 89 +++++++++++++------ change.md | 75 ++++++++++++++++ 3 files changed, 149 insertions(+), 29 deletions(-) create mode 100644 change.md diff --git a/.serena/project.yml b/.serena/project.yml index 3a2c370..5e713d4 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -133,3 +133,17 @@ symbol_info_budget: # list of regex patterns which, when matched, mark a memory entry as read‑only. # Extends the list from the global configuration, merging the two lists. read_only_memory_patterns: [] + +# list of regex patterns for memories to completely ignore. +# Matching memories will not appear in list_memories or activate_project output +# and cannot be accessed via read_memory or write_memory. +# To access ignored memory files, use the read_file tool on the raw file path. +# Extends the list from the global configuration, merging the two lists. +# Example: ["_archive/.*", "_episodes/.*"] +ignored_memory_patterns: [] + +# advanced configuration option allowing to configure language server-specific options. +# Maps the language key to the options. +# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available. +# No documentation on options means no options are available. +ls_specific_settings: {} diff --git a/autoload/front/Controllers/ShopBasketController.php b/autoload/front/Controllers/ShopBasketController.php index eb382e4..78398aa 100644 --- a/autoload/front/Controllers/ShopBasketController.php +++ b/autoload/front/Controllers/ShopBasketController.php @@ -5,6 +5,7 @@ class ShopBasketController { private const ORDER_SUBMIT_TOKEN_SESSION_KEY = 'order-submit-token'; private const ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY = 'order-submit-last-order-id'; + private const ORDER_SUBMIT_TOKEN_TTL = 1800; // 30 minutes public static $title = [ 'mainView' => 'Koszyk' @@ -276,19 +277,6 @@ class ShopBasketController exit; } - $existingOrderId = isset( $_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ] ) - ? (int)$_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ] - : 0; - if ( $existingOrderId > 0 ) - { - $existingOrderHash = $this->orderRepository->findHashById( $existingOrderId ); - if ( $existingOrderHash ) - { - header( 'Location: /zamowienie/' . $existingOrderHash ); - exit; - } - } - $client = \Shared\Helpers\Helpers::get_session( 'client' ); $orderSubmitToken = $this->createOrderSubmitToken(); @@ -308,23 +296,26 @@ class ShopBasketController public function basketSave() { - $orderSubmitToken = (string)\Shared\Helpers\Helpers::get( 'order_submit_token', true ); + $basket = \Shared\Helpers\Helpers::get_session( 'basket' ); $existingOrderId = isset( $_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ] ) ? (int)$_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ] : 0; + if ( ( !is_array( $basket ) || empty( $basket ) ) && $existingOrderId > 0 ) + { + $existingOrderHash = $this->orderRepository->findHashById( $existingOrderId ); + if ( $existingOrderHash ) + { + $this->logOrder( 'Double-submit detected, redirecting to existing order id=' . $existingOrderId ); + header( 'Location: /zamowienie/' . $existingOrderHash ); + exit; + } + } + + $orderSubmitToken = (string)\Shared\Helpers\Helpers::get( 'order_submit_token', true ); if ( !$this->isValidOrderSubmitToken( $orderSubmitToken ) ) { - if ( $existingOrderId > 0 ) - { - $existingOrderHash = $this->orderRepository->findHashById( $existingOrderId ); - if ( $existingOrderHash ) - { - header( 'Location: /zamowienie/' . $existingOrderHash ); - exit; - } - } - + $this->logOrder( 'Token validation failed. formToken=' . ($orderSubmitToken ?: '(empty)') ); \Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'zamowienie-zostalo-zlozone-komunikat-blad' ) ); - header( 'Location: /koszyk' ); + header( 'Location: /koszyk-podsumowanie' ); exit; } @@ -367,7 +358,7 @@ class ShopBasketController } catch ( \Exception $e ) { - error_log( '[basketSave] createFromBasket exception: ' . $e->getMessage() ); + $this->logOrder( 'createFromBasket exception: ' . $e->getMessage() ); \Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'zamowienie-zostalo-zlozone-komunikat-blad' ) ); header( 'Location: /koszyk' ); exit; @@ -400,6 +391,7 @@ class ShopBasketController } else { + $this->logOrder( 'createFromBasket returned falsy order_id. client_id=' . ($client['id'] ?? 'guest') . ' email=' . \Shared\Helpers\Helpers::get( 'email', true ) ); \Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'zamowienie-zostalo-zlozone-komunikat-blad' ) ); header( 'Location: /koszyk' ); exit; @@ -471,8 +463,23 @@ class ShopBasketController private function createOrderSubmitToken() { + $existingTokenData = isset( $_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ] ) ? $_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ] : null; + + if ( is_array( $existingTokenData ) && !empty( $existingTokenData['token'] ) && !empty( $existingTokenData['created_at'] ) ) + { + $age = time() - (int)$existingTokenData['created_at']; + if ( $age < self::ORDER_SUBMIT_TOKEN_TTL ) + { + \Shared\Helpers\Helpers::delete_session( self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ); + return $existingTokenData['token']; + } + } + $token = $this->generateOrderSubmitToken(); - \Shared\Helpers\Helpers::set_session( self::ORDER_SUBMIT_TOKEN_SESSION_KEY, $token ); + \Shared\Helpers\Helpers::set_session( self::ORDER_SUBMIT_TOKEN_SESSION_KEY, [ + 'token' => $token, + 'created_at' => time() + ] ); \Shared\Helpers\Helpers::delete_session( self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ); return $token; @@ -495,10 +502,26 @@ class ShopBasketController if ( !$token ) return false; - $sessionToken = isset( $_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ] ) ? (string)$_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ] : ''; - if ( !$sessionToken ) + $tokenData = isset( $_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ] ) ? $_SESSION[ self::ORDER_SUBMIT_TOKEN_SESSION_KEY ] : null; + + if ( is_string( $tokenData ) ) + { + $sessionToken = $tokenData; + if ( !$sessionToken ) + return false; + + return function_exists( 'hash_equals' ) ? hash_equals( $sessionToken, $token ) : $sessionToken === $token; + } + + if ( !is_array( $tokenData ) || empty( $tokenData['token'] ) || empty( $tokenData['created_at'] ) ) return false; + $age = time() - (int)$tokenData['created_at']; + if ( $age > self::ORDER_SUBMIT_TOKEN_TTL ) + return false; + + $sessionToken = (string)$tokenData['token']; + if ( function_exists( 'hash_equals' ) ) return hash_equals( $sessionToken, $token ); @@ -509,4 +532,12 @@ class ShopBasketController { \Shared\Helpers\Helpers::delete_session( self::ORDER_SUBMIT_TOKEN_SESSION_KEY ); } + + private function logOrder( $message ) + { + $logDir = $_SERVER['DOCUMENT_ROOT'] . '/logs'; + $logFile = $logDir . '/logs-order-' . date( 'Y-m-d' ) . '.log'; + $line = '[' . date( 'Y-m-d H:i:s' ) . '] ' . $message . PHP_EOL; + @file_put_contents( $logFile, $line, FILE_APPEND | LOCK_EX ); + } } diff --git a/change.md b/change.md new file mode 100644 index 0000000..1fd31d2 --- /dev/null +++ b/change.md @@ -0,0 +1,75 @@ +# Zmiana: Fix przekierowania w summaryView + +## Plik +`autoload/front/Controllers/ShopBasketController.php` + +## Problem +Po złożeniu pierwszego zamówienia, próba złożenia drugiego zamówienia powodowała przekierowanie na stronę poprzedniego zamówienia (`/zamowienie/{hash}`) zamiast na stronę podsumowania koszyka (`/koszyk-podsumowanie`). + +## Przyczyna +W metodzie `summaryView()` był guard sprawdzający sesyjny klucz `order-submit-last-order-id`. Jeśli istniał (a istniał po każdym złożonym zamówieniu), metoda od razu redirectowała na stronę starego zamówienia — nigdy nie dochodziło do `createOrderSubmitToken()`, które czyści ten klucz. + +## Co usunięto +Usunięto blok (dawne linie 279-290): +```php +$existingOrderId = isset( $_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ] ) + ? (int)$_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ] + : 0; +if ( $existingOrderId > 0 ) +{ + $existingOrderHash = $this->orderRepository->findHashById( $existingOrderId ); + if ( $existingOrderHash ) + { + header( 'Location: /zamowienie/' . $existingOrderHash ); + exit; + } +} +``` + +## Zabezpieczenie double-submit +Ochrona przed podwójnym wysłaniem formularza pozostaje nienaruszona w metodzie `basketSave()` — tam ten sam mechanizm działa poprawnie. + +--- + +# Zmiana 2: Logowanie błędów w basketSave + naprawa tokena zamówienia + +## Plik +`autoload/front/Controllers/ShopBasketController.php` + +## Problem +Klientka nie mogła złożyć zamówienia — komunikat "Podczas składania zamówienia wystąpił błąd". Brak logowania uniemożliwiał diagnozę. Dodatkowo token zamówienia był jednorazowy i nadpisywany przy każdym wejściu na podsumowanie — otwarcie drugiej karty, użycie "wstecz" w przeglądarce lub odświeżenie strony unieważniało token i blokowało złożenie zamówienia. + +## Dodane logowanie do `logs/logs-order-{data}.log` +Dodana prywatna metoda `logOrder()` zapisująca do pliku `logs/logs-order-YYYY-MM-DD.log` (schemat jak `logs-db-*`). + +Logowanie w 4 miejscach w `basketSave()`: + +1. **Double-submit (pusty koszyk + istnieje stare zamówienie)** — log: `Double-submit detected, redirecting to existing order id=...` +2. **Token nieprawidłowy** — log: `Token validation failed. formToken=...` +3. **createFromBasket rzucił wyjątek** — log: `createFromBasket exception: ...` +4. **createFromBasket zwróciło falsy order_id** — log: `createFromBasket returned falsy order_id. client_id=... email=...` + +## Naprawa tokena — z jednorazowego na czasowy (TTL 30 min) + +### Stare zachowanie +- `createOrderSubmitToken()` generował nowy token przy KAŻDYM wejściu na podsumowanie +- Każde otwarcie nowej karty/odświeżenie nadpisywało token w sesji +- Formularz w starej karcie miał stary token → walidacja failowała +- Przy nieudanej walidacji tokena redirect na `/koszyk` (użytkownik tracił cały kontekst) + +### Nowe zachowanie +- Dodana stała `ORDER_SUBMIT_TOKEN_TTL = 1800` (30 minut) +- Token przechowywany jako `['token' => '...', 'created_at' => time()]` +- `createOrderSubmitToken()`: jeśli istnieje ważny token (< 30 min), zwraca ten sam zamiast generować nowy +- `isValidOrderSubmitToken()`: sprawdza czy token nie wygasł + backward compatibility ze starymi stringowymi tokenami +- `consumeOrderSubmitToken()`: bez zmian — po złożeniu zamówienia token jest usuwany +- Przy nieudanej walidacji tokena redirect na `/koszyk-podsumowanie` (nowy token się generuje, użytkownik może od razu ponowić) +- Dodany osobny guard na double-submit: pusty koszyk + istniejące zamówienie w sesji → redirect na stronę zamówienia + +### Efekt +- Wiele kart z podsumowaniem → ten sam token → wszystkie działają +- Przycisk "wstecz" → token nadal ważny +- Odświeżenie strony → token nadal ważny +- Po 30 minutach token wygasa → redirect na podsumowanie, nowy token, ponów +- Po złożeniu zamówienia token jest konsumowany + koszyk czyszczony → double-submit chroniony +- Błąd tokena nie wyrzuca na /koszyk tylko na /koszyk-podsumowanie