--- phase: 07-pre-expansion-fixes plan: 02 type: execute wave: 1 depends_on: [] files_modified: - src/Modules/Settings/AllegroApiClient.php - src/Modules/Settings/AllegroOAuthClient.php - src/Modules/Settings/ShopproApiClient.php - src/Modules/Settings/ApaczkaApiClient.php - src/Core/Application.php - database/migrations/20260313_000049_add_cron_last_run_at_setting.sql - .env.example autonomous: true --- ## Goal Naprawić trzy problemy stabilności przed rozbudową: zduplikowany numer migracji 000014, brak weryfikacji SSL w 4 klientach API, cron throttle przechowywany w sesji zamiast bazie danych. ## Purpose Każdy nowy klient API dodany bez wzorca SSL verification powiela podatność. Cron throttle w sesji powoduje wielokrotne uruchamianie crona przy wielu aktywnych sesjach. Duplikat migracji wprowadza niejednoznaczność przy deploy na nowe środowisko. Wszystkie trzy to fundament do naprawy przed dodawaniem nowych integracji. ## Output - 4 pliki ApiClient z `CURLOPT_SSL_VERIFYPEER => true` + `CURLOPT_CAINFO` - `.env.example` z `CURL_CA_BUNDLE_PATH` - `Application::isWebCronThrottled()` czytający timestamp z `app_settings` zamiast `$_SESSION` - Nowa migracja seed dla klucza `cron_web_last_run_at` w `app_settings` - Migracja 000014b (rename duplikatu) ## Project Context @.paul/PROJECT.md ## Source Files @src/Modules/Settings/AllegroApiClient.php @src/Modules/Settings/AllegroOAuthClient.php @src/Core/Application.php @.env.example ## AC-1: SSL weryfikowany w każdym cURL wywołaniu ```gherkin Given 4 klasy ApiClient (AllegroApiClient, AllegroOAuthClient, ShopproApiClient, ApaczkaApiClient) wykonują cURL bez CURLOPT_SSL_VERIFYPEER When dowolny ApiClient wykonuje zapytanie HTTP Then CURLOPT_SSL_VERIFYPEER jest ustawione na true AND CURLOPT_CAINFO wskazuje na ścieżkę z .env (lub systemowy bundle jako fallback) AND .env.example zawiera CURL_CA_BUNDLE_PATH z komentarzem ``` ## AC-2: Web cron throttle oparty na DB, nie sesji ```gherkin Given Application::isWebCronThrottled() zapisuje/czyta timestamp z $_SESSION['cron_web_last_run_at'] When isWebCronThrottled() jest wywołane Then timestamp jest czytany z app_settings (klucz: cron_web_last_run_at) zamiast $_SESSION AND zapis po uruchomieniu crona idzie do app_settings (UPDATE lub INSERT ON DUPLICATE KEY) AND $_SESSION['cron_web_last_run_at'] nie jest już używany ``` ## AC-3: Migracja 000014 zdeduplikowana ```gherkin Given dwa pliki mają sekwencję 000014 w nazwie When sprawdzasz listę migracji Then jeden z nich jest przemianowany na 000014b AND plik zawiera idempotentny INSERT (ON DUPLICATE KEY UPDATE lub INSERT IGNORE) żeby powtórne uruchomienie na istniejącej bazie nie powodowało błędu ``` Task 1: Dodaj SSL verification do 4 ApiClient klas src/Modules/Settings/AllegroApiClient.php, src/Modules/Settings/AllegroOAuthClient.php, src/Modules/Settings/ShopproApiClient.php, src/Modules/Settings/ApaczkaApiClient.php, .env.example Dla każdej z 4 klas: 1. Odczytaj plik — zidentyfikuj wszystkie `curl_setopt_array($ch, [...])` wywołania 2. Do każdego array dodaj (jeśli nie ma): ```php CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_CAINFO => $this->getCaBundlePath(), ``` 3. Dodaj prywatną metodę pomocniczą do każdej klasy: ```php private function getCaBundlePath(): string { $envPath = (string) ($_ENV['CURL_CA_BUNDLE_PATH'] ?? ''); if ($envPath !== '' && is_file($envPath)) { return $envPath; } // Windows XAMPP fallback $xamppCa = 'C:/xampp/php/extras/ssl/cacert.pem'; if (is_file($xamppCa)) { return $xamppCa; } // System default (Linux/macOS) return '/etc/ssl/certs/ca-certificates.crt'; } ``` Jeśli wszystkie 4 klasy mają wspólny trait lub klasę bazową — wydziel metodę tam. Jeśli nie — dodaj do każdej osobno (duplikacja jest OK, unikamy przedwczesnej abstrakcji). 4. W `.env.example` dodaj sekcję: ``` # SSL/TLS — ścieżka do CA bundle dla cURL # Windows XAMPP: C:/xampp/php/extras/ssl/cacert.pem # Linux: /etc/ssl/certs/ca-certificates.crt CURL_CA_BUNDLE_PATH= ``` WAŻNE: Nie dodawaj CURLOPT_SSL_VERIFYPEER => false nigdzie. Jeśli CA bundle nie istnieje, metoda getCaBundlePath() zwraca ścieżkę — PHP zgłosi błąd cURL jeśli plik nie istnieje. To jest pożądane zachowanie (fail-fast, nie silent MITM). php -l src/Modules/Settings/AllegroApiClient.php php -l src/Modules/Settings/AllegroOAuthClient.php php -l src/Modules/Settings/ShopproApiClient.php php -l src/Modules/Settings/ApaczkaApiClient.php grep -n "CURLOPT_SSL_VERIFYPEER" src/Modules/Settings/AllegroApiClient.php grep -c "CURLOPT_SSL_VERIFYPEER" src/Modules/Settings/ShopproApiClient.php AC-1 satisfied: wszystkie 4 ApiClienty mają CURLOPT_SSL_VERIFYPEER => true Task 2: Cron throttle z $_SESSION → app_settings DB src/Core/Application.php, database/migrations/20260313_000049_add_cron_last_run_at_setting.sql W `Application.php`, znajdź metodę `isWebCronThrottled()` (linie ~268-285). Obecna logika: ```php $lastRunAt = isset($_SESSION['cron_web_last_run_at']) ? (int) $_SESSION['cron_web_last_run_at'] : 0; // ... $_SESSION['cron_web_last_run_at'] = $now; ``` Zastąp odczyt sesji odczytem z DB: ```php $lastRunAt = $this->getWebCronLastRunAt(); ``` I zapis sesji zapisem do DB: ```php $this->setWebCronLastRunAt($now); ``` Dodaj prywatne metody pomocnicze: ```php private function getWebCronLastRunAt(): int { try { $stmt = $this->db->pdo()->prepare( "SELECT setting_value FROM app_settings WHERE setting_key = 'cron_web_last_run_at'" ); $stmt->execute(); $value = $stmt->fetchColumn(); return $value !== false ? (int) $value : 0; } catch (Throwable) { return 0; } } private function setWebCronLastRunAt(int $timestamp): void { try { $this->db->pdo()->prepare( "INSERT INTO app_settings (setting_key, setting_value, created_at, updated_at) VALUES ('cron_web_last_run_at', :ts, NOW(), NOW()) ON DUPLICATE KEY UPDATE setting_value = :ts, updated_at = NOW()" )->execute(['ts' => (string) $timestamp]); } catch (Throwable) { // throttle failure is non-critical — GET_LOCK is the real guard } } ``` Sprawdź jak Application uzyskuje dostęp do PDO — jeśli przez `$this->db->pdo()` lub `$this->db->query()` — dostosuj. Sprawdź jak CronHandlerFactory i inne miejsca używają DB. Usuń odwołania do `$_SESSION['cron_web_last_run_at']` całkowicie. Stwórz migrację `20260313_000049_add_cron_last_run_at_setting.sql`: ```sql INSERT INTO app_settings (setting_key, setting_value, created_at, updated_at) VALUES ('cron_web_last_run_at', '0', NOW(), NOW()) ON DUPLICATE KEY UPDATE updated_at = updated_at; ``` php -l src/Core/Application.php grep -n "SESSION.*cron_web\|cron_web.*SESSION" src/Core/Application.php # Powinno zwrócić 0 wyników grep -n "getWebCronLastRunAt\|setWebCronLastRunAt" src/Core/Application.php AC-2 satisfied: isWebCronThrottled() nie używa $_SESSION, czyta/zapisuje app_settings Task 3: Rename duplikatu migracji 000014 database/migrations/20260301_000014b_add_products_sku_format_setting.sql Migracja `20260301_000014_add_products_sku_format_setting.sql` ma ten sam numer co `20260227_000014_create_product_integration_translations.sql`. Ponieważ migrator śledzi migracje po **pełnej nazwie pliku** (nie numerze sekwencji), rename jest bezpieczny dla nowych instalacji. Dla istniejących instalacji plik pod nową nazwą będzie wyglądał jako nowy — dlatego zawartość musi być idempotentna. Kroki: 1. Odczytaj zawartość `20260301_000014_add_products_sku_format_setting.sql` 2. Sprawdź czy INSERT używa `ON DUPLICATE KEY UPDATE` lub `INSERT IGNORE` - Jeśli nie: zmień INSERT na idempotentny (INSERT IGNORE lub ON DUPLICATE KEY) 3. Utwórz nowy plik `20260301_000014b_add_products_sku_format_setting.sql` z zaktualizowaną treścią 4. Usuń stary plik `20260301_000014_add_products_sku_format_setting.sql` UWAGA: Użyj `git mv` (przez bash) żeby git śledził rename: ```bash git mv "database/migrations/20260301_000014_add_products_sku_format_setting.sql" \ "database/migrations/20260301_000014b_add_products_sku_format_setting.sql" ``` Potem edytuj plik (jeśli INSERT wymaga korekty na idempotentny). ls database/migrations/ | grep "000014" # Powinno pokazać: 20260227_000014_create... i 20260301_000014b_add... # NIE powinno być: 20260301_000014_add... (stary plik) AC-3 satisfied: brak duplikatu, jeden plik przemianowany na 000014b ## DO NOT CHANGE - Logika biznesowa żadnego ApiClienta — tylko dodanie SSL options do istniejących curl_setopt_array - GET_LOCK logika w Application.php — nie ruszamy mutex, tylko session→DB dla timestamp - Inne klucze w app_settings - Istniejące migracje poza rename 000014 ## SCOPE LIMITS - Nie implementuj rotacji CSRF tokenów (osobny deferred concern) - Nie dodawaj path traversal check dla label files (osobny deferred concern) - CA bundle path — środowisko aplikacyjne, nie certyfikat self-signed Przed zamknięciem planu: - [ ] php -l na wszystkich 4 ApiClient plikach - [ ] php -l src/Core/Application.php - [ ] grep "SSL_VERIFYPEER" — każdy ApiClient ma ten wpis - [ ] grep "SESSION.*cron_web" Application.php — 0 wyników - [ ] ls database/migrations/ | grep "000014" — tylko 000014 i 000014b - [ ] Plik migracji 000049 istnieje - 4 ApiClienty z SSL verification - .env.example z CURL_CA_BUNDLE_PATH - Application::isWebCronThrottled() bez $_SESSION - Migracja 000049 (cron_web_last_run_at seed) - Rename 000014 → 000014b - Zero błędów składniowych PHP Po zakończeniu utwórz `.paul/phases/07-pre-expansion-fixes/07-02-SUMMARY.md`