Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b2d156e84 | |||
| c44f59894e |
@@ -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
|
||||
|
||||
@@ -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 ] );
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
BIN
updates/0.30/ver_0.322.zip
Normal file
BIN
updates/0.30/ver_0.322.zip
Normal file
Binary file not shown.
24
updates/0.30/ver_0.322_manifest.json
Normal file
24
updates/0.30/ver_0.322_manifest.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"changelog": "FIX - custom_fields: jawne mapowanie kluczy w ProductRepository, spójne !empty w ProductsApiController",
|
||||
"version": "0.322",
|
||||
"files": {
|
||||
"added": [
|
||||
|
||||
],
|
||||
"deleted": [
|
||||
|
||||
],
|
||||
"modified": [
|
||||
"autoload/Domain/Product/ProductRepository.php",
|
||||
"autoload/api/Controllers/ProductsApiController.php"
|
||||
]
|
||||
},
|
||||
"checksum_zip": "sha256:7c6e04cc393fdcd94e6fc9d2ea7a85f9c078e4beb9075d490be90675e4f6eae7",
|
||||
"sql": [
|
||||
|
||||
],
|
||||
"date": "2026-02-24",
|
||||
"directories_deleted": [
|
||||
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
<?
|
||||
$current_ver = 322;
|
||||
$current_ver = 323;
|
||||
|
||||
for ($i = 1; $i <= $current_ver; $i++)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user