From 3b2d156e84c4794bb7ab94f4ab52bcda676de384 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Tue, 24 Feb 2026 21:05:23 +0100 Subject: [PATCH] =?UTF-8?q?ver.=200.323:=20fix=20import=20zdj=C4=99=C4=87,?= =?UTF-8?q?=20trwa=C5=82e=20usuwanie=20produkt=C3=B3w,=20fix=20API=20uploa?= =?UTF-8?q?d=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IntegrationsRepository: refactor importu zdjęć — walidacja HTTP, curl timeouty, logi, czytelny komunikat - ProductRepository: saveCustomFields tylko gdy klucz istnieje (partial API update), delete() czyści custom_fields - ProductArchiveController: przycisk i metoda delete_permanent() do trwałego usunięcia z archiwum - ProductsApiController: fix ścieżki upload (api.php działa z rootu projektu) Co-Authored-By: Claude Opus 4.6 --- .../Integrations/IntegrationsRepository.php | 85 +++++++++++++++++-- autoload/Domain/Product/ProductRepository.php | 6 +- .../Controllers/ProductArchiveController.php | 28 ++++++ .../api/Controllers/ProductsApiController.php | 3 +- docs/CHANGELOG.md | 11 +++ docs/DATABASE_STRUCTURE.md | 11 +++ updates/versions.php | 2 +- 7 files changed, 135 insertions(+), 11 deletions(-) diff --git a/autoload/Domain/Integrations/IntegrationsRepository.php b/autoload/Domain/Integrations/IntegrationsRepository.php index 1286ffc..8631ce3 100644 --- a/autoload/Domain/Integrations/IntegrationsRepository.php +++ b/autoload/Domain/Integrations/IntegrationsRepository.php @@ -747,26 +747,55 @@ class IntegrationsRepository // Import images $images = $mdb2->select( 'pp_shop_products_images', '*', [ 'product_id' => $productId ] ); + $importLog = []; + $domainRaw = preg_replace( '#^https?://#', '', (string)($settings['domain'] ?? '') ); if ( is_array( $images ) ) { foreach ( $images as $image ) { - $imageUrl = 'https://' . $settings['domain'] . $image['src']; + $srcPath = (string)($image['src'] ?? ''); + $imageUrl = 'https://' . rtrim( $domainRaw, '/' ) . '/' . ltrim( $srcPath, '/' ); + $imageName = basename( $srcPath ); + + if ( $imageName === '' ) { + $importLog[] = '[SKIP] Pusta nazwa pliku dla src: ' . $srcPath; + continue; + } $ch = curl_init( $imageUrl ); curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true ); curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false ); curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false ); - $imageData = curl_exec( $ch ); + curl_setopt( $ch, CURLOPT_TIMEOUT, 30 ); + curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 10 ); + $imageData = curl_exec( $ch ); + $httpCode = (int)curl_getinfo( $ch, CURLINFO_HTTP_CODE ); + $curlErrno = curl_errno( $ch ); + $curlError = curl_error( $ch ); curl_close( $ch ); - $imageName = basename( $imageUrl ); - $imageDir = '../upload/product_images/product_' . $newProductId; + if ( $curlErrno !== 0 || $imageData === false ) { + $importLog[] = '[ERROR] cURL: ' . $imageUrl . ' — błąd ' . $curlErrno . ': ' . $curlError; + continue; + } + + if ( $httpCode !== 200 ) { + $importLog[] = '[ERROR] HTTP ' . $httpCode . ': ' . $imageUrl; + continue; + } + + $imageDir = dirname( __DIR__, 3 ) . '/upload/product_images/product_' . $newProductId; $imagePath = $imageDir . '/' . $imageName; - if ( !file_exists( $imageDir ) ) - mkdir( $imageDir, 0777, true ); + if ( !file_exists( $imageDir ) && !mkdir( $imageDir, 0777, true ) && !file_exists( $imageDir ) ) { + $importLog[] = '[ERROR] Nie można utworzyć katalogu: ' . $imageDir; + continue; + } - file_put_contents( $imagePath, $imageData ); + $written = file_put_contents( $imagePath, $imageData ); + if ( $written === false ) { + $importLog[] = '[ERROR] Zapis pliku nieudany: ' . $imagePath; + continue; + } $this->db->insert( 'pp_shop_products_images', [ 'product_id' => $newProductId, @@ -774,10 +803,50 @@ class IntegrationsRepository 'alt' => $image['alt'] ?? '', 'o' => $image['o'], ] ); + $importLog[] = '[OK] ' . $imageUrl . ' → ' . $imagePath . ' (' . $written . ' B)'; } } - return [ 'success' => true, 'message' => 'Produkt został zaimportowany.' ]; + // Zapisz log importu zdjęć (ścieżka absolutna — niezależna od cwd) + $logDir = dirname( __DIR__, 3 ) . '/logs'; + $logFile = $logDir . '/shoppro-import-debug.log'; + $mkdirOk = file_exists( $logDir ) || mkdir( $logDir, 0755, true ) || file_exists( $logDir ); + $logEntry = '[' . date( 'Y-m-d H:i:s' ) . '] Import produktu #' . $productId . ' → #' . $newProductId . "\n" + . ' Domain: ' . $domainRaw . "\n" + . ' Obrazy źródłowe: ' . count( $images ?: [] ) . "\n"; + foreach ( $importLog as $line ) { + $logEntry .= ' ' . $line . "\n"; + } + // Zawsze loguj do error_log (niezależnie od uprawnień do pliku) + error_log( '[shopPRO shoppro-import] ' . str_replace( "\n", ' | ', $logEntry ) ); + + if ( $mkdirOk && file_put_contents( $logFile, $logEntry, FILE_APPEND ) === false ) { + error_log( '[shopPRO shoppro-import] WARN: nie można zapisać logu do: ' . $logFile ); + } elseif ( !$mkdirOk ) { + error_log( '[shopPRO shoppro-import] WARN: nie można utworzyć katalogu: ' . $logDir ); + } + + // Zbuduj czytelny komunikat z wynikiem importu zdjęć + $imgCount = count( $images ?: [] ); + if ( $imgCount === 0 ) { + $imgSummary = 'Zdjęcia: brak w bazie źródłowej.'; + } else { + $ok = 0; + $errors = []; + foreach ( $importLog as $line ) { + if ( strncmp( $line, '[OK]', 4 ) === 0 ) { + $ok++; + } else { + $errors[] = $line; + } + } + $imgSummary = 'Zdjęcia: ' . $ok . '/' . $imgCount . ' zaimportowanych.'; + if ( !empty( $errors ) ) { + $imgSummary .= ' Błędy: ' . implode( '; ', $errors ); + } + } + + return [ 'success' => true, 'message' => 'Produkt został zaimportowany. ' . $imgSummary ]; } private function missingShopproSetting( array $settings, array $requiredKeys ): ?string diff --git a/autoload/Domain/Product/ProductRepository.php b/autoload/Domain/Product/ProductRepository.php index 8eafad5..f0dd1e8 100644 --- a/autoload/Domain/Product/ProductRepository.php +++ b/autoload/Domain/Product/ProductRepository.php @@ -1331,7 +1331,10 @@ class ProductRepository $this->saveImagesOrder( $productId, $d['gallery_order'] ); } - $this->saveCustomFields( $productId, $d['custom_field_name'] ?? [], $d['custom_field_type'] ?? [], $d['custom_field_required'] ?? [] ); + // Zapisz custom fields tylko gdy jawnie podane (partial update przez API może nie zawierać tego klucza) + if ( array_key_exists( 'custom_field_name', $d ) ) { + $this->saveCustomFields( $productId, $d['custom_field_name'] ?? [], $d['custom_field_type'] ?? [], $d['custom_field_required'] ?? [] ); + } if ( !$isNew ) { $this->cleanupDeletedFiles( $productId ); @@ -1645,6 +1648,7 @@ class ProductRepository $this->db->delete( 'pp_shop_products_langs', [ 'product_id' => $productId ] ); $this->db->delete( 'pp_shop_products_images', [ 'product_id' => $productId ] ); $this->db->delete( 'pp_shop_products_files', [ 'product_id' => $productId ] ); + $this->db->delete( 'pp_shop_products_custom_fields', [ 'id_product' => $productId ] ); $this->db->delete( 'pp_shop_products_attributes', [ 'product_id' => $productId ] ); $this->db->delete( 'pp_shop_products', [ 'id' => $productId ] ); $this->db->delete( 'pp_shop_product_sets_products', [ 'product_id' => $productId ] ); diff --git a/autoload/admin/Controllers/ProductArchiveController.php b/autoload/admin/Controllers/ProductArchiveController.php index 970b9b4..a9516dd 100644 --- a/autoload/admin/Controllers/ProductArchiveController.php +++ b/autoload/admin/Controllers/ProductArchiveController.php @@ -106,6 +106,14 @@ class ProductArchiveController 'confirm_ok' => 'Przywroc', 'confirm_cancel' => 'Anuluj', ], + [ + 'label' => 'Usun trwale', + 'url' => '/admin/product_archive/delete_permanent/product_id=' . $id, + 'class' => 'btn btn-xs btn-danger', + 'confirm' => 'UWAGA! Operacja nieodwracalna!' . "\n\n" . 'Produkt "' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '" zostanie trwale usuniety razem ze wszystkimi zdjeciami i zalacznikami z serwera.' . "\n\n" . 'Czy na pewno chcesz usunac ten produkt?', + 'confirm_ok' => 'Tak, usun trwale', + 'confirm_cancel' => 'Anuluj', + ], ], ]; } @@ -162,4 +170,24 @@ class ProductArchiveController header( 'Location: /admin/product_archive/list/' ); exit; } + + public function delete_permanent(): void + { + $productId = (int) \Shared\Helpers\Helpers::get( 'product_id' ); + + if ( $productId <= 0 ) { + \Shared\Helpers\Helpers::alert( 'Nieprawidłowe ID produktu.' ); + header( 'Location: /admin/product_archive/list/' ); + exit; + } + + if ( $this->productRepository->delete( $productId ) ) { + \Shared\Helpers\Helpers::set_message( 'Produkt został trwale usunięty wraz ze zdjęciami i załącznikami.' ); + } else { + \Shared\Helpers\Helpers::alert( 'Podczas usuwania produktu wystąpił błąd. Proszę spróbować ponownie.' ); + } + + header( 'Location: /admin/product_archive/list/' ); + exit; + } } diff --git a/autoload/api/Controllers/ProductsApiController.php b/autoload/api/Controllers/ProductsApiController.php index f655aa9..9b8c48b 100644 --- a/autoload/api/Controllers/ProductsApiController.php +++ b/autoload/api/Controllers/ProductsApiController.php @@ -338,7 +338,8 @@ class ProductsApiController $safeName = 'image_' . md5((string)microtime(true)) . '.jpg'; } - $baseDir = '../upload/product_images/product_' . $productId; + // api.php działa z rootu projektu (nie z admin/), więc ścieżka bez ../ + $baseDir = 'upload/product_images/product_' . $productId; if (!is_dir($baseDir) && !mkdir($baseDir, 0775, true) && !is_dir($baseDir)) { ApiRouter::sendError('INTERNAL_ERROR', 'Failed to create target directory', 500); return; diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 92a55d1..a823872 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,17 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze. --- +## ver. 0.323 (2026-02-24) - Import zdjęć, trwałe usuwanie, fix API upload + +- **FIX**: `IntegrationsRepository::shopproImportProduct()` — kompletny refactor importu zdjęć: walidacja HTTP response, curl timeouty, bezpieczna budowa URL, szczegółowy log do `logs/shoppro-import-debug.log` i `error_log`, czytelny komunikat z wynikiem +- **FIX**: `ProductRepository::saveProduct()` — `saveCustomFields()` wywoływane tylko gdy klucz `custom_field_name` istnieje w danych (partial update przez API nie czyści custom fields) +- **FIX**: `ProductRepository::delete()` — usuwanie rekordów z `pp_shop_products_custom_fields` przy kasowaniu produktu +- **FIX**: `ProductsApiController::upload_image()` — poprawka ścieżki uploadu (`upload/` zamiast `../upload/` — api.php działa z rootu projektu) +- **NEW**: `ProductArchiveController::delete_permanent()` — trwałe usunięcie produktu z archiwum (wraz ze zdjęciami i załącznikami) +- **NEW**: Przycisk "Usuń trwale" w liście produktów archiwalnych z potwierdzeniem + +--- + ## ver. 0.318 (2026-02-24) - ShopPRO export produktów + API endpoints - **NEW**: `IntegrationsRepository::shopproExportProduct()` — eksport produktu do zdalnej instancji shopPRO: pola główne, tłumaczenia, custom fields, zdjęcia przez API (base64) diff --git a/docs/DATABASE_STRUCTURE.md b/docs/DATABASE_STRUCTURE.md index ada59ba..80ca4d9 100644 --- a/docs/DATABASE_STRUCTURE.md +++ b/docs/DATABASE_STRUCTURE.md @@ -46,6 +46,17 @@ Zdjęcia produktów. | src | Ścieżka do pliku | | alt | Tekst alternatywny | +## pp_shop_products_custom_fields +Dodatkowe pola produktów (custom fields). + +| Kolumna | Opis | +|---------|------| +| id_additional_field | PK | +| id_product | FK do pp_shop_products | +| name | Nazwa pola | +| type | Typ pola (VARCHAR 30) | +| is_required | Czy wymagane (0/1) | + ## pp_shop_products_categories Przypisanie produktów do kategorii. diff --git a/updates/versions.php b/updates/versions.php index 3645a46..177ec24 100644 --- a/updates/versions.php +++ b/updates/versions.php @@ -1,5 +1,5 @@