diff --git a/docs.md b/docs.md new file mode 100644 index 0000000..b76e89d --- /dev/null +++ b/docs.md @@ -0,0 +1,8 @@ +# > [!IMPORTANT] + +> dont forget to set up ps_configuration -> ps_ssl + +## TODO + +update price to show recommended retail price in shop, for back office keep price and add recommended retail price. +fix stock, all products are out of stock diff --git a/ecomzone-v20.zip b/ecomzone-v20-working.zip similarity index 100% rename from ecomzone-v20.zip rename to ecomzone-v20-working.zip diff --git a/ecomzone-v21.zip b/ecomzone-v21.zip new file mode 100644 index 0000000..9e834d0 Binary files /dev/null and b/ecomzone-v21.zip differ diff --git a/ecomzone-v22-mark3.7.zip b/ecomzone-v22-mark3.7.zip new file mode 100644 index 0000000..30a5f25 Binary files /dev/null and b/ecomzone-v22-mark3.7.zip differ diff --git a/ecomzone/README.md b/ecomzone/README.md new file mode 100644 index 0000000..14cc381 --- /dev/null +++ b/ecomzone/README.md @@ -0,0 +1,61 @@ +# EcomZone PrestaShop Module - Troubleshooting + +## Issue #1: 405 Method Not Allowed Error +The module was encountering a 405 Method Not Allowed error with the message: "The ARRAY method is not supported for this route. Supported methods: GET, HEAD." + +### Root Cause +The issue was happening because the code was incorrectly passing parameters to the `makeRequest` method, causing the HTTP method to be set as an array instead of a string. + +### Fixes Applied +1. **Fixed `EcomZoneClient.php` Methods**: + - Updated `getCatalog` to explicitly pass "GET" as the HTTP method + - Updated `getProduct` to explicitly pass "GET" as the HTTP method + +2. **Enhanced `makeApiRequest` Method**: + - Added validation to ensure the method parameter is always a string + - Added more detailed logging for debugging + +3. **Fixed API Request Calls**: + - Updated all calls to `makeApiRequest` to explicitly specify "GET" as the method + +## Issue #2: Missing cURL Extension +The module was unable to make API requests because the PHP cURL extension was not installed. + +### Fix Applied +- Installed the PHP cURL extension using: `sudo apt-get install php-curl` + +## Testing Tools +Two testing tools have been created to verify API connectivity: + +1. **api_test.php**: + - A standalone script that tests API connectivity without requiring PrestaShop + - Verifies that the API token is valid and can retrieve product data + +2. **get_token.php**: + - Retrieves the API token from the PrestaShop configuration or database + - Useful for troubleshooting API authentication issues + +## Development Environment Testing +When testing in a development environment without a full PrestaShop installation: + +1. Use the standalone `api_test.php` script which doesn't require PrestaShop configuration files +2. The regular `test_api.php` script requires access to PrestaShop's `config/config.inc.php` and `init.php` files +3. Edit the paths in `test_api.php` if needed to point to your actual PrestaShop installation + +## Production Environment Testing +In a production environment: + +1. Copy the module to your PrestaShop modules directory +2. Install the module through the PrestaShop admin panel +3. Configure your API credentials in the module settings +4. Run the `test_api.php` script from within the module directory + +## How to Verify +1. Run the standalone API test script: `php -f api_test.php` +2. If the API connection is working, you should see product data from the EcomZone API +3. Check the module logs in `/log/ecomzone.log` for any new error messages + +If you continue to experience issues, please check: +- Your API token is valid and correctly configured +- The API URL is correctly set to `https://dropship.ecomzone.eu/api` +- Your web server has the required PHP extensions installed (cURL, JSON) \ No newline at end of file diff --git a/ecomzone/api_test.php b/ecomzone/api_test.php new file mode 100644 index 0000000..d320572 --- /dev/null +++ b/ecomzone/api_test.php @@ -0,0 +1,143 @@ + 'https://dropship.ecomzone.eu/api', + 'ECOMZONE_API_TOKEN' => 'klRyAdrXaxL0s6PEUp7LDlH6T8aPSCtBY8NiEHsHiWpc6646K2TZPi5KMxUg' // Replace with your actual API token + ]; + return isset($config[$key]) ? $config[$key] : $default; + } + + public static function updateValue($key, $value) { + return true; + } + } +} + +// Mock PrestaShop classes +if (!class_exists('Context')) { + class Context { + public $shop; + public static $instance; + + public function __construct() { + $this->shop = new stdClass(); + $this->shop->id = 1; + } + + public static function getContext() { + if (!self::$instance) { + self::$instance = new Context(); + } + return self::$instance; + } + } +} + +if (!class_exists('PrestaShopLogger')) { + class PrestaShopLogger { + public static function addLog($message, $severity = 1, $error_code = null, $object_type = null, $object_id = null, $allow_duplicate = false, $id_employee = null) { + return true; + } + } +} + +// Create log directory if it doesn't exist +$logDir = __DIR__ . '/log'; +if (!file_exists($logDir)) { + mkdir($logDir, 0755, true); +} + +try { + // Get API settings + $apiUrl = Configuration::get('ECOMZONE_API_URL'); + $apiToken = Configuration::get('ECOMZONE_API_TOKEN'); + + echo "API URL: " . $apiUrl . "\n"; + echo "API Token: " . (empty($apiToken) || $apiToken === 'YOUR_API_TOKEN' ? + "Not set - Please edit api_test.php and replace YOUR_API_TOKEN with your actual token" : + "Set (length: " . strlen($apiToken) . " chars)") . "\n\n"; + + if (empty($apiToken) || $apiToken === 'YOUR_API_TOKEN') { + throw new Exception("API token is not configured. Please edit api_test.php and replace YOUR_API_TOKEN with your actual token."); + } + + echo "Testing API connection...\n\n"; + + // Create API client + $client = new EcomZoneClient(); + + // Test API connectivity with catalog endpoint + echo "Requesting catalog data (page 1, limit 1)...\n"; + $result = $client->getCatalog(1, 1); + + if (isset($result['data']) && is_array($result['data'])) { + echo "Success! Received " . count($result['data']) . " product(s)\n"; + echo "Total products available: " . ($result['total'] ?? 'unknown') . "\n\n"; + + if (!empty($result['data'])) { + $sku = $result['data'][0]['sku'] ?? null; + + if ($sku) { + echo "Testing product detail retrieval...\n"; + echo "Requesting product with SKU: " . $sku . "\n"; + + $productDetail = $client->getProduct($sku); + + if (isset($productDetail['data']) && !empty($productDetail['data'])) { + echo "Success! Retrieved product details\n"; + echo "Product name: " . ($productDetail['data']['product_name'] ?? 'unknown') . "\n"; + } else { + echo "Error: Failed to retrieve product details\n"; + echo "Response: " . print_r($productDetail, true) . "\n"; + } + } + } + } else { + echo "Error: Invalid catalog response\n"; + echo "Response: " . print_r($result, true) . "\n"; + } + + echo "\nAPI test completed successfully!\n"; + +} catch (Exception $e) { + echo "ERROR: " . $e->getMessage() . "\n"; + + if ($e instanceof EcomZoneException) { + echo "Error Code: " . $e->getCode() . "\n"; + } + + echo "\nStack Trace:\n" . $e->getTraceAsString() . "\n"; +} \ No newline at end of file diff --git a/ecomzone/classes/EcomZoneClient.php b/ecomzone/classes/EcomZoneClient.php index f282a9c..62a2dac 100644 --- a/ecomzone/classes/EcomZoneClient.php +++ b/ecomzone/classes/EcomZoneClient.php @@ -8,25 +8,16 @@ class EcomZoneClient private $apiToken; private $apiUrl; private $maxRetries = 3; - private $timeout = 30; + private $timeout = 60; // Increased timeout public function __construct() { $this->apiToken = Configuration::get("ECOMZONE_API_TOKEN"); $this->apiUrl = Configuration::get("ECOMZONE_API_URL"); - if (empty($this->apiToken)) { - throw new EcomZoneException( - "API token not configured", - EcomZoneException::API_ERROR - ); - } - if (empty($this->apiUrl)) { - throw new EcomZoneException( - "API URL not configured", - EcomZoneException::API_ERROR - ); + $this->apiUrl = 'https://dropship.ecomzone.eu/api'; // Default API URL + Configuration::updateValue("ECOMZONE_API_URL", $this->apiUrl); } EcomZoneLogger::log("API Client initialized", "INFO", [ @@ -34,44 +25,60 @@ class EcomZoneClient ]); } + public function validateApiCredentials() + { + if (empty($this->apiToken)) { + throw new EcomZoneException( + "API token not configured", + EcomZoneException::API_ERROR + ); + } + + return true; + } + public function getCatalog($page = 1, $perPage = 100) { - $url = $this->apiUrl . "/catalog"; + $this->validateApiCredentials(); + $params = [ "page" => $page, "per_page" => $perPage, ]; - return $this->makeRequest("GET", $url, $params); + return $this->makeRequest("catalog", "GET", $params); } public function getProduct($sku) { - $url = $this->apiUrl . "/product/" . urlencode($sku); - return $this->makeRequest("GET", $url); + $this->validateApiCredentials(); + + return $this->makeRequest("product/" . urlencode($sku), "GET"); } - private function makeRequest($method, $url, $params = [], $data = null) + private function makeRequest($endpoint, $method = "GET", $params = [], $data = null) { + // Validate API credentials first + $this->validateApiCredentials(); + $retryCount = 0; $lastError = null; + $url = rtrim($this->apiUrl, '/') . '/' . ltrim($endpoint, '/'); + + // Add query parameters to URL if method is GET + if ($method === "GET" && !empty($params) && is_array($params)) { + $url .= (strpos($url, "?") === false ? "?" : "&") . http_build_query($params); + } do { try { EcomZoneLogger::log("Making API request", "INFO", [ "method" => $method, "url" => $url, - "params" => $params, "retry" => $retryCount, + "params" => $params ]); - // Add query parameters - if (!empty($params)) { - $url .= - (strpos($url, "?") === false ? "?" : "&") . - http_build_query($params); - } - $curl = curl_init(); $options = [ CURLOPT_URL => $url, @@ -86,36 +93,108 @@ class EcomZoneClient "Authorization: Bearer " . $this->apiToken, "Accept: application/json", "Content-Type: application/json", + "User-Agent: PrestaShop-EcomZone/1.0", + "Connection: close" // Avoid keep-alive issues ], + // SSL Options - disable verification for development + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => 0, + // Verbose debugging + CURLOPT_VERBOSE => true, ]; + // Create a temporary file for curl verbose output + $verbose = fopen('php://temp', 'w+'); + curl_setopt($curl, CURLOPT_STDERR, $verbose); + + // Add POST data if provided if ($data !== null) { - $options[CURLOPT_POSTFIELDS] = json_encode($data); + $jsonData = json_encode($data); + curl_setopt($curl, CURLOPT_POSTFIELDS, $jsonData); + EcomZoneLogger::log("Request payload", "DEBUG", [ + 'data' => $jsonData + ]); + } + // For POST requests with params, add them to the body + else if ($method !== "GET" && !empty($params)) { + $jsonData = json_encode($params); + curl_setopt($curl, CURLOPT_POSTFIELDS, $jsonData); + EcomZoneLogger::log("Request params as payload", "DEBUG", [ + 'data' => $jsonData + ]); } curl_setopt_array($curl, $options); + + // Execute the request $response = curl_exec($curl); $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + $contentType = curl_getinfo($curl, CURLINFO_CONTENT_TYPE); $error = curl_error($curl); + $errorNo = curl_errno($curl); + + // Get verbose information + rewind($verbose); + $verboseLog = stream_get_contents($verbose); + fclose($verbose); + + // Log detailed response information + EcomZoneLogger::log("API Response Details", "DEBUG", [ + 'http_code' => $httpCode, + 'content_type' => $contentType, + 'response_length' => strlen($response), + 'curl_error_code' => $errorNo, + 'curl_error' => $error, + 'verbose_log' => $verboseLog, + 'raw_response' => strlen($response) > 1000 ? + substr($response, 0, 1000) . '... [truncated]' : + $response + ]); + curl_close($curl); - if ($error) { - throw new Exception("cURL Error: " . $error); + if ($errorNo > 0) { + throw new Exception("cURL Error ({$errorNo}): " . $error); } + // Handle HTTP errors if ($httpCode >= 400) { - throw new Exception( - "HTTP Error: " . $httpCode . " Response: " . $response - ); + $errorMessage = "HTTP Error: " . $httpCode; + + // Try to extract error details from response + if (!empty($response)) { + try { + $errorData = json_decode($response, true); + if (json_last_error() === JSON_ERROR_NONE && !empty($errorData['error'])) { + $errorMessage .= " - " . $errorData['error']; + } else { + $errorMessage .= " - Response: " . substr($response, 0, 200); + } + } catch (Exception $e) { + $errorMessage .= " - Raw response: " . substr($response, 0, 200); + } + } + + throw new Exception($errorMessage); } + // Handle empty responses + if (empty($response)) { + throw new Exception("Empty response received from API"); + } + + // Decode JSON response $decodedResponse = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { - throw new Exception("Invalid JSON response"); + throw new Exception( + "Invalid JSON response: " . json_last_error_msg() . + "\nRaw response: " . substr($response, 0, 500) + ); } EcomZoneLogger::log("API request successful", "INFO", [ "http_code" => $httpCode, + "content_type" => $contentType ]); return $decodedResponse; @@ -126,10 +205,18 @@ class EcomZoneClient EcomZoneLogger::log("API request failed", "ERROR", [ "error" => $e->getMessage(), "retry" => $retryCount, + "url" => $url, + "trace" => $e->getTraceAsString() ]); if ($retryCount < $this->maxRetries) { - sleep(pow(2, $retryCount)); + $sleepTime = pow(2, $retryCount); + EcomZoneLogger::log("Retrying request", "INFO", [ + "retry_count" => $retryCount, + "sleep_time" => $sleepTime, + "url" => $url + ]); + sleep($sleepTime); continue; } @@ -142,11 +229,117 @@ class EcomZoneClient } } while ($retryCount < $this->maxRetries); + // This should never be reached, but just in case throw new EcomZoneException( "Max retries reached: " . $lastError->getMessage(), EcomZoneException::API_ERROR, $lastError ); } + + /** + * Download an image from the EcomZone API with proper authentication + * + * @param string $imageUrl The full URL of the image to download + * @return array Array containing image data and metadata + * @throws EcomZoneException If the download fails + */ + public function downloadImage(string $imageUrl): array + { + try { + EcomZoneLogger::log("Downloading image", "DEBUG", [ + 'url' => $imageUrl + ]); + + // Initialize cURL + $ch = curl_init(); + + curl_setopt_array($ch, [ + CURLOPT_URL => $imageUrl, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 5, + CURLOPT_TIMEOUT => 30, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + CURLOPT_HTTPHEADER => [ + 'Accept: image/webp,image/apng,image/*,*/*;q=0.8', + 'Accept-Encoding: gzip, deflate, br', + 'Authorization: Bearer ' . $this->getApiToken() + ] + ]); + + $imageData = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); + $error = curl_error($ch); + + curl_close($ch); + + // Check if the response is HTML instead of an image (authentication error) + $isHtml = stripos($contentType, 'text/html') !== false || + (strlen($imageData) > 15 && stripos($imageData, '') !== false); + + if ($isHtml) { + throw new EcomZoneException( + "Authentication error: Received HTML instead of image data. Check API token.", + EcomZoneException::API_ERROR + ); + } + + if ($httpCode !== 200 || empty($imageData)) { + throw new EcomZoneException( + "Failed to download image (HTTP $httpCode): $error", + EcomZoneException::API_ERROR + ); + } + + EcomZoneLogger::log("Image download complete", "DEBUG", [ + 'url' => $imageUrl, + 'content_type' => $contentType, + 'size' => strlen($imageData) + ]); + + return [ + 'data' => $imageData, + 'content_type' => $contentType, + 'http_code' => $httpCode + ]; + + } catch (Exception $e) { + EcomZoneLogger::log("Image download failed", "ERROR", [ + 'url' => $imageUrl, + 'error' => $e->getMessage() + ]); + + throw new EcomZoneException( + "Image download failed: " . $e->getMessage(), + EcomZoneException::API_ERROR, + $e + ); + } + } + + /** + * Get the API token, either from the class property or from the configuration + * + * @return string The API token + */ + protected function getApiToken() + { + if (empty($this->apiToken)) { + $this->apiToken = Configuration::get("ECOMZONE_API_TOKEN"); + } + + if (empty($this->apiToken)) { + throw new EcomZoneException( + "API token not configured", + EcomZoneException::API_ERROR + ); + } + + return $this->apiToken; + } } diff --git a/ecomzone/classes/EcomZoneLogger.php b/ecomzone/classes/EcomZoneLogger.php index 637c7dc..919bdf9 100644 --- a/ecomzone/classes/EcomZoneLogger.php +++ b/ecomzone/classes/EcomZoneLogger.php @@ -5,66 +5,173 @@ if (!defined("_PS_VERSION_")) { class EcomZoneLogger { - private static $logFile = 'ecomzone.log'; - private static $maxLogSize = 10485760; // 10MB - - public static function log($message, $level = "INFO", $context = []) + const LOG_FILE = 'ecomzone.log'; + + // Log levels with corresponding severity + const LEVELS = [ + 'DEBUG' => 1, + 'INFO' => 2, + 'WARNING' => 3, + 'ERROR' => 4, + 'CRITICAL' => 5 + ]; + + // Current log level - can be changed through configuration + private static function getLogLevel() + { + $configLevel = Configuration::get('ECOMZONE_LOG_LEVEL', 'INFO'); + return isset(self::LEVELS[$configLevel]) ? $configLevel : 'INFO'; + } + + public static function log($message, $level = 'INFO', $context = []) { try { - $logDir = _PS_MODULE_DIR_ . 'ecomzone/log/'; - if (!is_dir($logDir)) { - mkdir($logDir, 0755, true); + // Check if we should log this level + $logLevel = self::getLogLevel(); + if (self::LEVELS[$level] < self::LEVELS[$logLevel]) { + return true; // Skip logging for less severe levels } - - $timestamp = date('Y-m-d H:i:s'); - $contextStr = !empty($context) ? json_encode($context) : ''; - $logMessage = sprintf( - "[%s] %s: %s %s\n", - $timestamp, - $level, - $message, - $contextStr - ); - - file_put_contents( - $logDir . self::$logFile, - $logMessage, - FILE_APPEND - ); - } catch (Exception $e) { - // Fallback to PrestaShop's logger - PrestaShopLogger::addLog( - "EcomZone: $message", - ($level === 'ERROR' ? 3 : 1) - ); - } - } - - private static function initLogFile() - { - if (!file_exists(self::$logFile)) { - $logDir = dirname(self::$logFile); + // Ensure the log directory exists + $logDir = _PS_MODULE_DIR_ . 'ecomzone/log'; if (!is_dir($logDir)) { - mkdir($logDir, 0755, true); + if (!@mkdir($logDir, 0755, true)) { + throw new Exception("Failed to create log directory: " . $logDir); + } + @chmod($logDir, 0755); // Ensure the directory is writable } - touch(self::$logFile); - chmod(self::$logFile, 0666); + + // Format the log entry + $logEntry = [ + 'timestamp' => date('Y-m-d H:i:s'), + 'level' => strtoupper($level), + 'message' => $message, + 'context' => $context + ]; + + // For errors and critical issues, also include a stack trace + if (in_array($level, ['ERROR', 'CRITICAL']) && !isset($context['trace'])) { + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); + $logEntry['trace'] = array_slice($trace, 1); // Skip the log function itself + } + + $logLine = json_encode($logEntry) . PHP_EOL; + + // Rotate log file if it's too large (> 10MB) + $logFile = $logDir . '/' . self::LOG_FILE; + if (file_exists($logFile) && filesize($logFile) > 10 * 1024 * 1024) { + self::rotateLogFile($logFile); + } + + // Write to log file with exclusive lock + if (!@file_put_contents($logFile, $logLine, FILE_APPEND | LOCK_EX)) { + throw new Exception("Failed to write to log file: " . $logFile); + } + + // Also write to PrestaShop log for error/critical levels + if (in_array($level, ['ERROR', 'CRITICAL'])) { + $psLogLevel = ($level === 'ERROR') ? 3 : 4; + $contextStr = json_encode($context); + PrestaShopLogger::addLog( + "EcomZone: {$message} - Context: {$contextStr}", + $psLogLevel, + null, + 'EcomZone' + ); + } + + return true; + } catch (Exception $e) { + // Last resort logging to PHP error log + error_log('EcomZone logging error: ' . $e->getMessage() . + '. Original message: ' . $message); + + // Try to log to PrestaShop's log + try { + PrestaShopLogger::addLog( + 'EcomZone logging error: ' . $e->getMessage() . '. Original message: ' . $message, + 3, // Error level + null, + 'EcomZone' + ); + } catch (Exception $innerEx) { + error_log('Failed to log to PrestaShop: ' . $innerEx->getMessage()); + } + + return false; } } - private static function rotateLogIfNeeded() + private static function rotateLogFile($logFile) { - if (!file_exists(self::$logFile)) { - return; - } + try { + $backup = $logFile . '.' . date('Y-m-d-H-i-s') . '.bak'; + if (!@rename($logFile, $backup)) { + throw new Exception("Failed to rotate log file"); + } - if (filesize(self::$logFile) > self::$maxLogSize) { - $backup = self::$logFile . "." . date("Y-m-d-H-i-s") . ".bak"; - rename(self::$logFile, $backup); - touch(self::$logFile); - chmod(self::$logFile, 0666); + // Keep only last 5 backup files + $backups = glob($logFile . '.*.bak'); + if (count($backups) > 5) { + usort($backups, function($a, $b) { + return filemtime($b) - filemtime($a); + }); + + $toDelete = array_slice($backups, 5); + foreach ($toDelete as $file) { + @unlink($file); + } + } + + return true; + } catch (Exception $e) { + error_log('EcomZone log rotation error: ' . $e->getMessage()); + return false; } } + + public static function getLogPath() + { + // When running in standalone mode, use the local directory + if (!defined('_PS_MODULE_DIR_') || !file_exists(_PS_MODULE_DIR_ . 'ecomzone')) { + return __DIR__ . '/../log/' . self::LOG_FILE; + } + + return _PS_MODULE_DIR_ . 'ecomzone/log/' . self::LOG_FILE; + } + + public static function getLogContents($maxLines = 100) + { + $logFile = self::getLogPath(); + if (!file_exists($logFile)) { + return []; + } + + $logs = []; + $lines = array_reverse(file($logFile)); + $count = 0; + + foreach ($lines as $line) { + if ($count >= $maxLines) break; + + $entry = json_decode($line, true); + if ($entry) { + $logs[] = $entry; + $count++; + } + } + + return $logs; + } + + public static function clearLogs() + { + $logFile = self::getLogPath(); + if (file_exists($logFile)) { + @unlink($logFile); + self::log("Log file cleared", "INFO"); + } + return true; + } } diff --git a/ecomzone/classes/EcomZoneProductSync.php b/ecomzone/classes/EcomZoneProductSync.php index 15a8591..fbf5c35 100644 --- a/ecomzone/classes/EcomZoneProductSync.php +++ b/ecomzone/classes/EcomZoneProductSync.php @@ -3,6 +3,8 @@ if (!defined("_PS_VERSION_")) { exit(); } +require_once dirname(__FILE__) . '/interfaces/IProductSync.php'; + class EcomZoneProductSync implements IProductSync { private EcomZoneClient $client; @@ -22,63 +24,112 @@ class EcomZoneProductSync implements IProductSync public function importProducts(int $perPage = 100): array { - $page = 1; - $totalImported = 0; - $totalAvailable = 0; - $errors = []; - try { - EcomZoneLogger::log("Starting product import", "INFO", [ + // Get or initialize sync state + $syncState = $this->getSyncState(); + + // If this is a new sync (page 1 and no imports), reset everything + if (empty($syncState) || ($syncState['current_page'] ?? 1) === 1 && ($syncState['total_imported'] ?? 0) === 0) { + $this->resetSyncState(); + $syncState = []; + } + + $page = $syncState['current_page'] ?? 1; + $totalImported = $syncState['total_imported'] ?? 0; + $totalAvailable = $syncState['total_available'] ?? 0; + $errors = $syncState['errors'] ?? []; + $maxProductsPerBatch = 20; // Process 20 products at a time to avoid timeout + + EcomZoneLogger::log("Starting/Resuming product import", "INFO", [ + "page" => $page, "per_page" => $perPage, + "total_imported" => $totalImported, + "is_new_sync" => empty($syncState) ]); - do { - $catalog = $this->client->getCatalog($page, $perPage); + // Get catalog for current page + $catalog = $this->client->getCatalog($page, $perPage); - if (!isset($catalog["data"]) || !is_array($catalog["data"])) { - throw new EcomZoneException( - "Invalid catalog response", - EcomZoneException::API_ERROR - ); - } - - foreach ($catalog["data"] as $product) { - try { - if ($this->importSingleProduct($product)) { - $totalImported++; - } - } catch (Exception $e) { - $errors[] = [ - "sku" => $product["sku"] ?? "unknown", - "error" => $e->getMessage(), - ]; - EcomZoneLogger::log("Product import failed", "ERROR", [ - "sku" => $product["sku"] ?? "unknown", - "error" => $e->getMessage(), - ]); - } - } + if (!isset($catalog["data"]) || !is_array($catalog["data"])) { + throw new EcomZoneException( + "Invalid catalog response", + EcomZoneException::API_ERROR + ); + } + // Update total available on first page or if it changes + if ($page === 1 || ($catalog["total"] ?? 0) !== $totalAvailable) { $totalAvailable = $catalog["total"] ?? 0; - $page++; + } - EcomZoneLogger::log("Page processed", "INFO", [ - "page" => $page - 1, - "imported" => $totalImported, - "total" => $totalAvailable, - ]); - } while ( - isset($catalog["next_page_url"]) && - $catalog["next_page_url"] !== null - ); + $productsProcessed = 0; + $batchErrors = []; + + // Process only a batch of products + foreach ($catalog["data"] as $product) { + if ($productsProcessed >= $maxProductsPerBatch) { + break; + } - $this->clearCache(); + try { + if ($this->importSingleProduct($product)) { + $totalImported++; + } + } catch (Exception $e) { + $batchErrors[] = [ + "sku" => $product["sku"] ?? "unknown", + "error" => $e->getMessage(), + ]; + EcomZoneLogger::log("Product import failed", "ERROR", [ + "sku" => $product["sku"] ?? "unknown", + "error" => $e->getMessage(), + ]); + } + $productsProcessed++; + } + + // Update errors array with new batch errors + $errors = array_merge($errors, $batchErrors); + + // Calculate progress + $progress = [ + 'current_page' => $page, + 'total_imported' => $totalImported, + 'total_available' => $totalAvailable, + 'errors' => $errors, + 'is_complete' => false, + 'products_processed_on_page' => $productsProcessed + ]; + + // Check if we need to continue to next page + if ($productsProcessed < count($catalog["data"])) { + // Still more products to process on this page + $progress['products_processed_on_page'] = $productsProcessed; + } else { + // Move to next page if available + if (isset($catalog["next_page_url"]) && $catalog["next_page_url"] !== null) { + $progress['current_page'] = $page + 1; + $progress['products_processed_on_page'] = 0; + } else { + $progress['is_complete'] = true; + } + } + + // Save progress + $this->saveSyncState($progress); + + // Clear cache if complete + if ($progress['is_complete']) { + $this->clearCache(); + } return [ "success" => true, "imported" => $totalImported, "total" => $totalAvailable, "errors" => $errors, + "is_complete" => $progress['is_complete'], + "current_page" => $progress['current_page'] ]; } catch (Exception $e) { EcomZoneLogger::log("Product import process failed", "ERROR", [ @@ -192,8 +243,11 @@ class EcomZoneProductSync implements IProductSync $product->name[$this->defaultLangId] = $data['product_name']; $product->description[$this->defaultLangId] = $data['long_description'] ?? $data['description']; $product->description_short[$this->defaultLangId] = $data['description']; - $product->price = (float)$data['product_price']; - $product->wholesale_price = (float)($data['recommended_retail_price'] ?? 0); + + // Update prices + $product->wholesale_price = (float)$data['product_price']; // Original price as wholesale price + $product->price = (float)($data['recommended_retail_price'] ?? $data['product_price']); // Recommended retail price for shop display + $product->reference = $data['sku']; $product->ean13 = $data['ean'] ?? ''; @@ -354,15 +408,32 @@ class EcomZoneProductSync implements IProductSync private function updateProductStock(Product $product, array $data): void { try { - $quantity = (int) ($data['stock_quantity'] ?? 0); - StockAvailable::setQuantity($product->id, 0, $quantity); + $quantity = (int)($data['stock'] ?? 0); + + // Make sure product is saved first + if (!$product->id) { + $product->save(); + } + + // Update stock quantity + StockAvailable::setQuantity( + $product->id, + 0, // Combination ID (0 for products without combinations) + $quantity + ); // Update out of stock behavior StockAvailable::setProductOutOfStock( $product->id, - $quantity > 0 ? 2 : 0, // 2 = Allow orders, 0 = Deny orders + 2, // Always allow orders $this->defaultShopId ); + + EcomZoneLogger::log("Stock updated", "DEBUG", [ + "product" => $product->reference, + "quantity" => $quantity + ]); + } catch (Exception $e) { EcomZoneLogger::log("Failed to update stock", "WARNING", [ "product" => $product->reference, @@ -375,49 +446,126 @@ class EcomZoneProductSync implements IProductSync { try { $imageUrls = []; + + EcomZoneLogger::log("Raw product data for images", "DEBUG", [ + "product" => $product->reference, + "data" => json_encode($data) + ]); - // Process main image - if (!empty($data["image"])) { - $imageUrls[] = $data["image"]; - } - - // Process additional images - if (!empty($data["images"])) { - $additionalImages = json_decode($data["images"], true); - if (is_array($additionalImages)) { - $imageUrls = array_merge($imageUrls, $additionalImages); + // Handle different possible data structures + if (isset($data['images']) && is_string($data['images'])) { + $images = json_decode($data['images'], true); + if (json_last_error() === JSON_ERROR_NONE && is_array($images)) { + $imageUrls = array_merge($imageUrls, $images); } + } elseif (isset($data['images']) && is_array($data['images'])) { + $imageUrls = array_merge($imageUrls, $data['images']); } + // Add main image if available + if (!empty($data['image'])) { + array_unshift($imageUrls, $data['image']); // Add as first image + } + + // Filter out empty or invalid URLs + $imageUrls = array_filter($imageUrls, function($url) { + return !empty($url) && filter_var($url, FILTER_VALIDATE_URL); + }); + if (empty($imageUrls)) { + EcomZoneLogger::log("No valid images found for product", "WARNING", [ + "product" => $product->reference, + "data" => json_encode($data) + ]); return; } // Delete existing images if any $product->deleteImages(); + $successCount = 0; foreach ($imageUrls as $index => $imageUrl) { - $this->importProductImage($product, $imageUrl, $index === 0); + try { + EcomZoneLogger::log("Processing image", "DEBUG", [ + "product" => $product->reference, + "image_url" => $imageUrl, + "index" => $index + ]); + + $this->importProductImage($product, $imageUrl, $index === 0); + $successCount++; + } catch (Exception $e) { + EcomZoneLogger::log("Failed to process image", "WARNING", [ + "product" => $product->reference, + "image_url" => $imageUrl, + "error" => $e->getMessage() + ]); + continue; + } } + + if ($successCount === 0 && !empty($imageUrls)) { + throw new Exception(sprintf( + "Failed to process any images for the product. Attempted %d images.", + count($imageUrls) + )); + } + + EcomZoneLogger::log("Processed product images", "INFO", [ + "product" => $product->reference, + "total_images" => count($imageUrls), + "successful_imports" => $successCount + ]); + } catch (Exception $e) { - EcomZoneLogger::log("Failed to process product images", "WARNING", [ + EcomZoneLogger::log("Failed to process product images", "ERROR", [ "sku" => $product->reference, "error" => $e->getMessage(), + "data" => json_encode($data) ]); + throw $e; } } private function importProductImage(Product $product, string $imageUrl, bool $cover = false): void { try { - // Create temporary file - $tempFile = tempnam(_PS_TMP_IMG_DIR_, 'import_'); + // Create temporary directory if it doesn't exist + $tempDir = _PS_MODULE_DIR_ . 'ecomzone/tmp/'; + if (!is_dir($tempDir)) { + mkdir($tempDir, 0755, true); + chmod($tempDir, 0755); + } + + // Clean URL - handle double escaped slashes + $cleanUrl = stripslashes(stripslashes(trim($imageUrl))); - // Download image - if (!copy($imageUrl, $tempFile)) { - throw new Exception("Failed to download image: " . $imageUrl); + EcomZoneLogger::log("Processing image URL", "DEBUG", [ + "product" => $product->reference, + "original_url" => $imageUrl, + "cleaned_url" => $cleanUrl + ]); + + // Create temp file + $tempFile = tempnam($tempDir, 'import_'); + if ($tempFile) { + chmod($tempFile, 0644); } + // Use the client class to download the image + $imageResult = $this->client->downloadImage($cleanUrl); + $imageData = $imageResult['data']; + $contentType = $imageResult['content_type']; + + // Save image data to temp file + file_put_contents($tempFile, $imageData); + + // Verify the downloaded file is a valid image + $imageInfo = @getimagesize($tempFile); + if (!$imageInfo) { + throw new Exception("Downloaded file is not a valid image. Content type: $contentType"); + } + // Create image object $image = new Image(); $image->id_product = $product->id; @@ -433,29 +581,60 @@ class EcomZoneProductSync implements IProductSync $imageDir = dirname($imagePath); if (!file_exists($imageDir)) { - mkdir($imageDir, 0755, true); + if (!mkdir($imageDir, 0755, true)) { + throw new Exception("Failed to create image directory: " . $imageDir); + } + chmod($imageDir, 0755); } // Generate image if (!ImageManager::resize( $tempFile, - $imagePath . '.jpg' + $imagePath . '.jpg', + null, + null, + 'jpg' )) { throw new Exception("Failed to process image"); } + + // Ensure the generated image has correct permissions + chmod($imagePath . '.jpg', 0644); // Generate thumbnails $this->generateThumbnails($image); // Clean up - unlink($tempFile); + if (file_exists($tempFile)) { + unlink($tempFile); + } + + EcomZoneLogger::log("Image imported successfully", "DEBUG", [ + "product" => $product->reference, + "image_url" => $cleanUrl, + "mime_type" => $imageInfo['mime'] + ]); } catch (Exception $e) { EcomZoneLogger::log("Failed to import product image", "ERROR", [ "product" => $product->reference, "image_url" => $imageUrl, - "error" => $e->getMessage() + "clean_url" => $cleanUrl ?? '', + "error" => $e->getMessage(), + "content_type" => $contentType ?? null ]); + + // Clean up on error + if (isset($tempFile) && file_exists($tempFile)) { + unlink($tempFile); + } + + // Delete image record if it was created + if (isset($image) && $image->id) { + $image->delete(); + } + + throw $e; } } @@ -463,117 +642,41 @@ class EcomZoneProductSync implements IProductSync { try { $imageTypes = ImageType::getImagesTypes('products'); + $sourceFile = _PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '.jpg'; + + if (!file_exists($sourceFile)) { + throw new Exception("Source image file not found"); + } + foreach ($imageTypes as $imageType) { - $dir = _PS_PROD_IMG_DIR_ . $image->getExistingImgPath(); + $destination = _PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '-' . stripslashes($imageType['name']) . '.jpg'; + $dir = dirname($destination); + if (!file_exists($dir)) { - mkdir($dir, 0755, true); + if (!mkdir($dir, 0755, true)) { + throw new Exception("Failed to create thumbnail directory: " . $dir); + } + chmod($dir, 0755); } - ImageManager::resize( - _PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '.jpg', - _PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '-' . stripslashes($imageType['name']) . '.jpg', + + if (!ImageManager::resize( + $sourceFile, + $destination, (int)$imageType['width'], (int)$imageType['height'] - ); + )) { + throw new Exception("Failed to generate thumbnail: " . $imageType['name']); + } + + // Set proper permissions for the thumbnail + chmod($destination, 0644); } } catch (Exception $e) { EcomZoneLogger::log("Failed to generate thumbnails", "WARNING", [ "image_id" => $image->id, "error" => $e->getMessage() ]); - } - } - - private function generateImageType( - string $srcPath, - string $destPath, - ?int $width, - ?int $height - ): void { - try { - $this->createDirectoryIfNotExists(dirname($destPath)); - - if ($width === null || $height === null) { - copy($srcPath, $destPath); - return; - } - - $imageInfo = getimagesize($srcPath); - if (!$imageInfo) { - throw new Exception("Invalid image file"); - } - - $srcImage = $this->createImageFromType($srcPath, $imageInfo[2]); - $destImage = imagecreatetruecolor($width, $height); - - // Preserve transparency for PNG images - if ($imageInfo[2] === IMAGETYPE_PNG) { - imagealphablending($destImage, false); - imagesavealpha($destImage, true); - $transparent = imagecolorallocatealpha( - $destImage, - 255, - 255, - 255, - 127 - ); - imagefilledrectangle( - $destImage, - 0, - 0, - $width, - $height, - $transparent - ); - } - - imagecopyresampled( - $destImage, - $srcImage, - 0, - 0, - 0, - 0, - $width, - $height, - $imageInfo[0], - $imageInfo[1] - ); - - imagejpeg($destImage, $destPath, 95); - - imagedestroy($srcImage); - imagedestroy($destImage); - } catch (Exception $e) { - throw new EcomZoneException( - "Failed to generate image type: " . $e->getMessage(), - EcomZoneException::IMAGE_PROCESSING_ERROR, - $e - ); - } - } - - private function createImageFromType(string $filename, int $type) - { - switch ($type) { - case IMAGETYPE_JPEG: - return imagecreatefromjpeg($filename); - case IMAGETYPE_PNG: - return imagecreatefrompng($filename); - case IMAGETYPE_GIF: - return imagecreatefromgif($filename); - default: - throw new Exception("Unsupported image type: " . $type); - } - } - - private function createDirectoryIfNotExists(string $directory): void - { - if (!is_dir($directory)) { - if (!mkdir($directory, 0755, true)) { - throw new Exception( - "Failed to create directory: " . $directory - ); - } + throw $e; } } @@ -716,6 +819,27 @@ class EcomZoneProductSync implements IProductSync Media::clearCache(); PrestaShopAutoload::getInstance()->generateIndex(); } + + private function getSyncState(): array + { + $state = Configuration::get('ECOMZONE_SYNC_STATE'); + return $state ? json_decode($state, true) : []; + } + + private function saveSyncState(array $state): void + { + Configuration::updateValue('ECOMZONE_SYNC_STATE', json_encode($state)); + } + + public function resetSyncState(): void + { + // Delete both sync state and progress + Configuration::deleteByName('ECOMZONE_SYNC_STATE'); + Configuration::deleteByName('ECOMZONE_SYNC_PROGRESS'); + Configuration::deleteByName('ECOMZONE_LAST_SYNC'); + + EcomZoneLogger::log("Sync state reset", "INFO"); + } } $token = Configuration::get('ECOMZONE_CRON_TOKEN'); diff --git a/ecomzone/classes/interfaces/IProductSync.php b/ecomzone/classes/interfaces/IProductSync.php index 067d111..3913813 100644 --- a/ecomzone/classes/interfaces/IProductSync.php +++ b/ecomzone/classes/interfaces/IProductSync.php @@ -5,6 +5,17 @@ if (!defined('_PS_VERSION_')) { interface IProductSync { + /** + * Import products from external source + * @param int $perPage Number of products to import per page + * @return array Import results containing success status, counts, and any errors + */ public function importProducts(int $perPage = 100): array; + + /** + * Import a single product + * @param array $productData Product data to import + * @return bool Success status + */ public function importSingleProduct(array $productData): bool; } \ No newline at end of file diff --git a/ecomzone/cron.php b/ecomzone/cron.php deleted file mode 100644 index 0285624..0000000 --- a/ecomzone/cron.php +++ /dev/null @@ -1,123 +0,0 @@ - 'classes/EcomZoneException.php', - 'EcomZoneClient' => 'classes/EcomZoneClient.php', - 'EcomZoneLogger' => 'classes/EcomZoneLogger.php', - 'EcomZoneProductSync' => 'classes/EcomZoneProductSync.php', - 'EcomZoneProductImport' => 'classes/EcomZoneProductImport.php', - 'EcomZoneCategoryHandler' => 'classes/EcomZoneCategoryHandler.php', - 'IProductSync' => 'classes/interfaces/IProductSync.php', - 'ICategory' => 'classes/interfaces/ICategory.php' - ]; - - if (isset($classMap[$className])) { - require_once dirname(__FILE__) . '/' . $classMap[$className]; - } -}); - -// Function to send JSON response -function sendJsonResponse($data, $statusCode = 200) -{ - if (php_sapi_name() === 'cli') { - echo json_encode($data, JSON_PRETTY_PRINT) . "\n"; - } else { - if (!headers_sent()) { - http_response_code($statusCode); - header("Content-Type: application/json"); - } - echo json_encode($data); - } - exit($statusCode === 200 ? 0 : 1); -} - -// Get token from either GET parameter or command line argument -$token = ''; -if (php_sapi_name() === 'cli') { - global $argv; - foreach ($argv as $arg) { - if (strpos($arg, 'token=') === 0) { - $token = substr($arg, 6); - break; - } - } -} else { - $token = Tools::getValue('token'); -} - -// Security check -$configToken = Configuration::get("ECOMZONE_CRON_TOKEN"); - -if (empty($token) || $token !== $configToken) { - sendJsonResponse([ - 'success' => false, - 'error' => 'Invalid token', - 'provided_token' => $token, - 'expected_token' => $configToken - ], 403); -} - -try { - // Initialize logger - EcomZoneLogger::log("Starting cron execution", "INFO", [ - "time" => date("Y-m-d H:i:s"), - ]); - - // Check last run time to prevent too frequent execution - $lastRun = Configuration::get("ECOMZONE_LAST_CRON_RUN"); - $minInterval = 360; - - if (!empty($lastRun) && strtotime($lastRun) + $minInterval > time()) { - EcomZoneLogger::log("Skipping cron - too soon since last run", "INFO", [ - "last_run" => $lastRun, - "next_run" => date("Y-m-d H:i:s", strtotime($lastRun) + $minInterval), - ]); - - sendJsonResponse([ - "success" => false, - "message" => "Too soon since last run", - "last_run" => $lastRun, - "next_run" => date("Y-m-d H:i:s", strtotime($lastRun) + $minInterval), - ]); - } - - // Initialize Product Sync - $productSync = new EcomZoneProductSync(); - - // Start the import process - $result = $productSync->importProducts(100); // Import 100 products per page - - // Update last run time - Configuration::updateValue("ECOMZONE_LAST_CRON_RUN", date("Y-m-d H:i:s")); - - // Log completion and send response - EcomZoneLogger::log("Cron execution completed", "INFO", [ - "imported" => $result["imported"], - "total" => $result["total"], - "errors" => count($result["errors"]), - ]); - - sendJsonResponse([ - "success" => true, - "result" => $result, - "timestamp" => date("Y-m-d H:i:s"), - ]); -} catch (Exception $e) { - EcomZoneLogger::log("Cron execution failed", "ERROR", [ - "error" => $e->getMessage(), - "trace" => $e->getTraceAsString() - ]); - - sendJsonResponse([ - "success" => false, - "error" => $e->getMessage() - ], 500); -} - diff --git a/ecomzone/ecomzone.php b/ecomzone/ecomzone.php index 80afe84..1b9ee96 100644 --- a/ecomzone/ecomzone.php +++ b/ecomzone/ecomzone.php @@ -20,6 +20,9 @@ class Ecomzone extends Module parent::__construct(); + // Register autoloader + spl_autoload_register([$this, 'autoload']); + $this->displayName = $this->trans( "EcomZone Dropshipping", [], @@ -37,78 +40,462 @@ class Ecomzone extends Module ); } - public function install() + /** + * Autoload EcomZone classes + */ + public function autoload($className) { - if (!parent::install() - || !$this->registerHook('actionCronJob') - || !$this->registerHook('actionProductUpdate') - || !$this->registerHook('actionProductDelete') - ) { - return false; - } - - // Set default configuration values - Configuration::updateValue('ECOMZONE_API_URL', 'https://dropship.ecomzone.eu/api'); - Configuration::updateValue('ECOMZONE_CRON_TOKEN', Tools::encrypt(uniqid())); - Configuration::updateValue('ECOMZONE_LAST_SYNC', ''); - - // Create required directories - $this->createRequiredDirectories(); - - return true; - } - - private function createRequiredDirectories() - { - $dirs = [ - _PS_MODULE_DIR_ . $this->name . '/log', - _PS_MODULE_DIR_ . $this->name . '/tmp' + // Define class mapping + $classMap = [ + 'EcomZoneException' => 'classes/EcomZoneException.php', + 'EcomZoneClient' => 'classes/EcomZoneClient.php', + 'EcomZoneLogger' => 'classes/EcomZoneLogger.php', + 'EcomZoneProductSync' => 'classes/EcomZoneProductSync.php', + 'EcomZoneProductImport' => 'classes/EcomZoneProductImport.php', + 'EcomZoneCategoryHandler' => 'classes/EcomZoneCategoryHandler.php', + 'IProductSync' => 'classes/interfaces/IProductSync.php', + 'ICategory' => 'classes/interfaces/ICategory.php' ]; - foreach ($dirs as $dir) { - if (!file_exists($dir)) { - mkdir($dir, 0755, true); + // Check if the class exists in our map + if (isset($classMap[$className])) { + $file = _PS_MODULE_DIR_ . $this->name . '/' . $classMap[$className]; + if (file_exists($file)) { + require_once($file); + } + } + } + + private function setImageDirectoryPermissions(): bool + { + try { + $directories = [ + _PS_IMG_DIR_, + _PS_PROD_IMG_DIR_, + _PS_MODULE_DIR_ . $this->name . '/tmp/', + _PS_MODULE_DIR_ . $this->name . '/log/' + ]; + + EcomZoneLogger::log("Setting up directory permissions", "INFO"); + + foreach ($directories as $dir) { + if (!file_exists($dir)) { + EcomZoneLogger::log("Creating directory", "INFO", ['directory' => $dir]); + if (!@mkdir($dir, 0755, true)) { + $error = error_get_last(); + throw new Exception("Failed to create directory {$dir}: " . ($error['message'] ?? 'Unknown error')); + } + } + + if (!is_writable($dir)) { + // Get current permissions + $currentPerms = fileperms($dir) & 0777; + EcomZoneLogger::log("Directory not writable, setting permissions", "INFO", [ + 'directory' => $dir, + 'current_perms' => decoct($currentPerms), + 'target_perms' => '0755' + ]); + + if (!@chmod($dir, 0755)) { + $error = error_get_last(); + throw new Exception("Failed to set permissions for {$dir}: " . ($error['message'] ?? 'Unknown error')); + } + } + + // If this is the product image directory, ensure all subdirectories are writable + if (strpos($dir, _PS_PROD_IMG_DIR_) === 0) { + EcomZoneLogger::log("Processing product image subdirectories", "INFO", ['directory' => $dir]); + + try { + if (!is_dir($dir)) { + continue; // Skip if directory doesn't exist + } + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST + ); + + foreach ($iterator as $item) { + if ($item->isDir()) { + if (!is_writable($item->getPathname())) { + if (!@chmod($item->getPathname(), 0755)) { + $error = error_get_last(); + EcomZoneLogger::log("Failed to set subdirectory permissions", "WARNING", [ + 'directory' => $item->getPathname(), + 'error' => $error['message'] ?? 'Unknown error' + ]); + } + } + } else { + if (!is_writable($item->getPathname())) { + if (!@chmod($item->getPathname(), 0644)) { + $error = error_get_last(); + EcomZoneLogger::log("Failed to set file permissions", "WARNING", [ + 'file' => $item->getPathname(), + 'error' => $error['message'] ?? 'Unknown error' + ]); + } + } + } + } + } catch (UnexpectedValueException $e) { + // Log but don't fail if we can't read a subdirectory + EcomZoneLogger::log("Failed to process subdirectory", "WARNING", [ + 'directory' => $dir, + 'error' => $e->getMessage() + ]); + } + } + } + + EcomZoneLogger::log("Directory permissions setup complete", "INFO"); + return true; + } catch (Exception $e) { + EcomZoneLogger::log("Failed to set directory permissions", "ERROR", [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + return false; + } + } + + public function install() + { + EcomZoneLogger::log("Starting module installation", "INFO"); + + try { + // First check if module directory exists and is writable + if (!is_dir(_PS_MODULE_DIR_ . $this->name)) { + EcomZoneLogger::log("Creating module directory", "INFO"); + if (!@mkdir(_PS_MODULE_DIR_ . $this->name, 0755, true)) { + $error = error_get_last(); + throw new Exception('Failed to create module directory: ' . ($error['message'] ?? 'Unknown error')); + } + } + + // Create required directories first + $dirs = [ + _PS_MODULE_DIR_ . $this->name . '/log', + _PS_MODULE_DIR_ . $this->name . '/tmp', + _PS_IMG_DIR_, + _PS_PROD_IMG_DIR_ + ]; + + foreach ($dirs as $dir) { + if (!file_exists($dir)) { + EcomZoneLogger::log("Creating directory: " . $dir, "INFO"); + if (!@mkdir($dir, 0755, true)) { + $error = error_get_last(); + throw new Exception('Failed to create directory: ' . $dir . ' - ' . ($error['message'] ?? 'Unknown error')); + } + } + + if (!is_writable($dir)) { + EcomZoneLogger::log("Setting directory writable: " . $dir, "INFO"); + if (!@chmod($dir, 0755)) { + $error = error_get_last(); + throw new Exception('Failed to set permissions for directory: ' . $dir . ' - ' . ($error['message'] ?? 'Unknown error')); + } + } + } + + // Set up database tables + EcomZoneLogger::log("Setting up database tables", "INFO"); + $this->createTables(); + + // Install parent module + EcomZoneLogger::log("Installing parent module", "INFO"); + if (!parent::install()) { + throw new Exception('Failed to install parent module'); + } + + // Register hooks + EcomZoneLogger::log("Registering hooks", "INFO"); + if (!$this->registerHook('actionProductUpdate') + || !$this->registerHook('actionProductDelete') + || !$this->registerHook('actionCronJob')) { + throw new Exception('Failed to register hooks'); + } + + // Set default configuration values + EcomZoneLogger::log("Setting default configuration values", "INFO"); + $defaultConfig = [ + 'ECOMZONE_API_URL' => 'https://dropship.ecomzone.eu/api', + 'ECOMZONE_API_TOKEN' => '', + 'ECOMZONE_LAST_SYNC' => '', + 'ECOMZONE_LOG_LEVEL' => 'INFO', + 'ECOMZONE_CRON_TOKEN' => Tools::passwdGen(32) // Generate random token for cron + ]; + + foreach ($defaultConfig as $key => $value) { + if (!Configuration::updateValue($key, $value)) { + throw new Exception('Failed to set configuration value: ' . $key); + } + } + + // Set directory permissions + EcomZoneLogger::log("Setting directory permissions", "INFO"); + if (!$this->setImageDirectoryPermissions()) { + throw new Exception('Failed to set directory permissions'); + } + + EcomZoneLogger::log("Module installation completed successfully", "INFO"); + return true; + } catch (Exception $e) { + // Log the error + EcomZoneLogger::log('Module installation failed', 'ERROR', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + // Clean up any partial installation + $this->uninstall(); + + // Set error message that can be retrieved + $this->_errors[] = $this->l('Installation failed: ') . $e->getMessage(); + + return false; + } + } + + private function createTables() + { + $sql = []; + + // Add module logging table + $sql[] = 'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'ecomzone_sync_log` ( + `id_sync_log` int(11) NOT NULL AUTO_INCREMENT, + `sync_date` datetime NOT NULL, + `status` varchar(32) NOT NULL, + `message` text, + `details` text, + PRIMARY KEY (`id_sync_log`) + ) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8;'; + + // Add product sync table to track imported products + $sql[] = 'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'ecomzone_product` ( + `id_product` int(11) NOT NULL, + `reference` varchar(64) NOT NULL, + `last_sync` datetime NOT NULL, + `sync_status` varchar(32) NOT NULL, + PRIMARY KEY (`id_product`), + UNIQUE KEY `reference` (`reference`) + ) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8;'; + + foreach ($sql as $query) { + if (!Db::getInstance()->execute($query)) { + throw new Exception('Failed to create required database table: ' . Db::getInstance()->getMsgError()); } } } public function uninstall() { - return parent::uninstall() && - Configuration::deleteByName("ECOMZONE_API_URL") && - Configuration::deleteByName("ECOMZONE_API_TOKEN") && - Configuration::deleteByName("ECOMZONE_CRON_TOKEN") && - Configuration::deleteByName("ECOMZONE_LAST_SYNC"); + EcomZoneLogger::log("Starting module uninstallation", "INFO"); + + try { + // Remove configuration values + $configKeys = [ + 'ECOMZONE_API_URL', + 'ECOMZONE_API_TOKEN', + 'ECOMZONE_LAST_SYNC', + 'ECOMZONE_SYNC_STATE', + 'ECOMZONE_SYNC_PROGRESS', + 'ECOMZONE_LOG_LEVEL', + 'ECOMZONE_CRON_TOKEN' + ]; + + foreach ($configKeys as $key) { + Configuration::deleteByName($key); + } + + // Uninstall parent module + if (!parent::uninstall()) { + throw new Exception('Failed to uninstall parent module'); + } + + EcomZoneLogger::log("Module uninstallation completed successfully", "INFO"); + return true; + } catch (Exception $e) { + EcomZoneLogger::log('Module uninstallation failed', 'ERROR', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + $this->_errors[] = $this->l('Uninstallation failed: ') . $e->getMessage(); + return false; + } } public function getContent() { - $output = ""; + // Ensure no output before AJAX responses + if (Tools::getValue('ajax')) { + ob_clean(); + header('Content-Type: application/json'); + } + $output = ""; + + // Get admin token + $token = Tools::getAdminTokenLite('AdminModules'); + if (Tools::isSubmit("submitEcomZoneModule")) { $apiToken = Tools::getValue("ECOMZONE_API_TOKEN"); - if (Configuration::updateValue("ECOMZONE_API_TOKEN", $apiToken)) { - $output .= $this->displayConfirmation( - $this->trans( - "Settings updated", - [], - "Modules.Ecomzone.Admin" - ) - ); + $apiUrl = Tools::getValue("ECOMZONE_API_URL"); + + Configuration::updateValue("ECOMZONE_API_TOKEN", $apiToken); + Configuration::updateValue("ECOMZONE_API_URL", $apiUrl); + + $output .= $this->displayConfirmation( + $this->trans( + "Settings updated", + [], + "Modules.Ecomzone.Admin" + ) + ); + } + + // Handle manual sync request + if (Tools::isSubmit("syncProducts")) { + try { + // If this is an AJAX request, we need to handle it differently + $isAjax = Tools::getValue('ajax'); + + if (empty(Configuration::get("ECOMZONE_API_TOKEN"))) { + throw new Exception($this->l('API token not configured. Please configure the API token first.')); + } + + $productSync = new EcomZoneProductSync(); + + // Reset sync state when starting new sync + if (!$isAjax) { + $productSync->resetSyncState(); + } + + $result = $productSync->importProducts(100); + + if ($result['is_complete']) { + Configuration::updateValue("ECOMZONE_LAST_SYNC", date("Y-m-d H:i:s")); + } + + // Store sync progress + $progress = [ + 'processed' => $result["imported"], + 'total' => $result["total"], + 'percentage' => ($result["total"] > 0) ? round(($result["imported"] / $result["total"]) * 100) : 0, + 'is_complete' => $result['is_complete'], + 'current_page' => $result['current_page'] + ]; + + Configuration::updateValue("ECOMZONE_SYNC_PROGRESS", json_encode($progress)); + + if ($isAjax) { + die(json_encode([ + 'success' => true, + 'progress' => $progress + ])); + } + + if ($result['is_complete']) { + $output .= $this->displayConfirmation( + sprintf( + $this->trans( + "Products synchronized successfully. Imported: %d, Total Available: %d, Errors: %d", + [], + "Modules.Ecomzone.Admin" + ), + $result["imported"], + $result["total"], + count($result["errors"]) + ) + ); + + if (!empty($result["errors"])) { + $errorMessages = array_map(function($error) { + return sprintf("SKU: %s - Error: %s", $error["sku"], $error["error"]); + }, $result["errors"]); + + $output .= $this->displayWarning( + implode("
", $errorMessages) + ); + } + } + } catch (Exception $e) { + EcomZoneLogger::log("Sync failed", "ERROR", [ + "error" => $e->getMessage(), + "trace" => $e->getTraceAsString() + ]); + + if ($isAjax) { + die(json_encode([ + 'success' => false, + 'error' => $e->getMessage() + ])); + } + + $output .= $this->displayError($e->getMessage()); } } - // Handle product fetch request + // Handle continue sync request (AJAX) + if (Tools::getValue('action') === 'continueSyncProducts' && Tools::getValue('ajax')) { + try { + if (empty(Configuration::get("ECOMZONE_API_TOKEN"))) { + throw new Exception($this->l('API token not configured. Please configure the API token first.')); + } + + $productSync = new EcomZoneProductSync(); + $result = $productSync->importProducts(100); + + if ($result['is_complete']) { + Configuration::updateValue("ECOMZONE_LAST_SYNC", date("Y-m-d H:i:s")); + } + + // Store sync progress + $progress = [ + 'processed' => $result["imported"], + 'total' => $result["total"], + 'percentage' => ($result["total"] > 0) ? round(($result["imported"] / $result["total"]) * 100) : 0, + 'is_complete' => $result['is_complete'], + 'current_page' => $result['current_page'] + ]; + + Configuration::updateValue("ECOMZONE_SYNC_PROGRESS", json_encode($progress)); + + die(json_encode([ + 'success' => true, + 'progress' => $progress + ])); + } catch (Exception $e) { + EcomZoneLogger::log("Sync continuation failed", "ERROR", [ + "error" => $e->getMessage() + ]); + die(json_encode([ + 'success' => false, + 'error' => $e->getMessage() + ])); + } + } + + // Handle product fetch request for preview if (Tools::isSubmit("fetchProducts") || Tools::getValue("page")) { try { $page = (int) Tools::getValue("page", 1); $perPage = 10; $result = $this->makeApiRequest( - "catalog?page={$page}&per_page={$perPage}" + "catalog?page={$page}&per_page={$perPage}", + "GET" ); if (!empty($result["data"])) { + // Add sync status to products + foreach ($result["data"] as &$product) { + $productId = Product::getIdByReference($product["sku"]); + $product["sync_status"] = $productId ? "success" : "pending"; + } + $totalPages = ceil($result["total"] / $perPage); $this->context->smarty->assign([ @@ -131,15 +518,27 @@ class Ecomzone extends Module } } } catch (Exception $e) { + EcomZoneLogger::log("Product fetch failed", "ERROR", [ + "error" => $e->getMessage() + ]); $output .= $this->displayError($e->getMessage()); } } + // Get sync progress + $syncProgress = json_decode(Configuration::get("ECOMZONE_SYNC_PROGRESS"), true); + + // Get recent activity logs + $activityLog = $this->getRecentActivityLogs(); + $this->context->smarty->assign([ "module_dir" => $this->_path, "ECOMZONE_API_TOKEN" => Configuration::get("ECOMZONE_API_TOKEN"), - "ECOMZONE_CRON_TOKEN" => Configuration::get("ECOMZONE_CRON_TOKEN"), + "ECOMZONE_API_URL" => Configuration::get("ECOMZONE_API_URL"), "ECOMZONE_LAST_SYNC" => Configuration::get("ECOMZONE_LAST_SYNC"), + "SYNC_PROGRESS" => $syncProgress, + "ACTIVITY_LOG" => $activityLog, + "token" => $token, "current_url" => $this->context->link->getAdminLink("AdminModules", false) . "&configure=" . @@ -151,78 +550,77 @@ class Ecomzone extends Module "shop_url" => $this->context->link->getBaseLink(), ]); - return $output . - $this->display(__FILE__, "views/templates/admin/configure.tpl"); + return $output . $this->display(__FILE__, "views/templates/admin/configure.tpl"); + } + + private function getRecentActivityLogs($limit = 50) + { + $logFile = _PS_MODULE_DIR_ . $this->name . '/log/ecomzone.log'; + $logs = []; + + if (file_exists($logFile)) { + $lines = array_reverse(file($logFile)); + $count = 0; + + foreach ($lines as $line) { + if ($count >= $limit) break; + + $logEntry = json_decode($line, true); + if ($logEntry) { + $logs[] = [ + 'timestamp' => $logEntry['timestamp'] ?? date('Y-m-d H:i:s'), + 'level' => $logEntry['level'] ?? 'INFO', + 'message' => $logEntry['message'] ?? '', + 'details' => json_encode($logEntry['context'] ?? []) + ]; + $count++; + } + } + } + + return $logs; } private function makeApiRequest($endpoint, $method = "GET", $data = null) { - $apiUrl = Configuration::get("ECOMZONE_API_URL"); - $apiToken = Configuration::get("ECOMZONE_API_TOKEN"); - - if (empty($apiToken)) { - throw new Exception( - $this->trans( - "API token not configured", - [], - "Modules.Ecomzone.Admin" - ) - ); + try { + // Use the EcomZoneClient for all API requests + $client = new EcomZoneClient(); + + // Add endpoint to URL + $params = []; + + // Extract query params if present in endpoint + if (strpos($endpoint, '?') !== false) { + list($endpoint, $queryString) = explode('?', $endpoint, 2); + parse_str($queryString, $params); + } + + // Ensure method is a string and not an array (prevent 405 Method Not Allowed) + if (!is_string($method)) { + EcomZoneLogger::log("Invalid method type detected", "WARNING", [ + 'endpoint' => $endpoint, + 'method_type' => gettype($method), + 'method' => $method + ]); + $method = "GET"; // Default to GET if method is not a string + } + + // Make the request using our client + if ($method === 'GET') { + return $client->makeRequest($endpoint, $method, $params); + } else { + return $client->makeRequest($endpoint, $method, [], $data); + } + } catch (Exception $e) { + EcomZoneLogger::log("API Request Failed in module", "ERROR", [ + 'endpoint' => $endpoint, + 'method' => $method, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + throw $e; } - - $curl = curl_init(); - $url = rtrim($apiUrl, "/") . "/" . ltrim($endpoint, "/"); - - $options = [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_ENCODING => "", - CURLOPT_MAXREDIRS => 10, - CURLOPT_TIMEOUT => 30, - CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, - CURLOPT_CUSTOMREQUEST => $method, - CURLOPT_HTTPHEADER => [ - "Authorization: Bearer " . $apiToken, - "Accept: application/json", - "Content-Type: application/json", - ], - CURLOPT_SSL_VERIFYPEER => true, - CURLOPT_SSL_VERIFYHOST => 2, - ]; - - if ($data !== null) { - $options[CURLOPT_POSTFIELDS] = json_encode($data); - } - - curl_setopt_array($curl, $options); - - $response = curl_exec($curl); - $err = curl_error($curl); - $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); - - curl_close($curl); - - if ($err) { - throw new Exception("cURL Error: " . $err); - } - - if ($httpCode >= 400) { - $errorData = json_decode($response, true); - $errorMessage = isset($errorData['error']) - ? $errorData['error'] - : "HTTP Error: " . $httpCode; - throw new EcomZoneException( - $errorMessage, - EcomZoneException::API_ERROR - ); - } - - $decodedResponse = json_decode($response, true); - if (json_last_error() !== JSON_ERROR_NONE) { - throw new Exception("Invalid JSON response"); - } - - return $decodedResponse; } public function hookActionCronJob($params) @@ -245,7 +643,8 @@ class Ecomzone extends Module do { $result = $this->makeApiRequest( - "catalog?page={$page}&per_page={$perPage}" + "catalog?page={$page}&per_page={$perPage}", + "GET" ); if (empty($result["data"])) { @@ -341,5 +740,35 @@ class Ecomzone extends Module ); } } + + public function hookActionProductUpdate($params) + { + try { + $product = $params['product']; + EcomZoneLogger::log("Product updated", "INFO", [ + "product_id" => $product->id, + "reference" => $product->reference + ]); + } catch (Exception $e) { + EcomZoneLogger::log("Failed to handle product update", "ERROR", [ + "error" => $e->getMessage() + ]); + } + } + + public function hookActionProductDelete($params) + { + try { + $product = $params['product']; + EcomZoneLogger::log("Product deleted", "INFO", [ + "product_id" => $product->id, + "reference" => $product->reference + ]); + } catch (Exception $e) { + EcomZoneLogger::log("Failed to handle product deletion", "ERROR", [ + "error" => $e->getMessage() + ]); + } + } } diff --git a/ecomzone/get_token.php b/ecomzone/get_token.php new file mode 100644 index 0000000..058b5a7 --- /dev/null +++ b/ecomzone/get_token.php @@ -0,0 +1,53 @@ + 'localhost', + 'username' => 'prestashop', + 'password' => 'prestashop', + 'database' => 'prestashop', + 'prefix' => 'ps_' +]; + +echo "Attempting direct database connection...\n"; + +try { + $db = new PDO("mysql:host={$dbConfig['host']};dbname={$dbConfig['database']}", $dbConfig['username'], $dbConfig['password']); + $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $stmt = $db->prepare("SELECT value FROM {$dbConfig['prefix']}configuration WHERE name = 'ECOMZONE_API_TOKEN'"); + $stmt->execute(); + + $result = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($result && isset($result['value'])) { + echo "API Token found in database: " . $result['value'] . "\n"; + } else { + echo "API Token not found in database.\n"; + } +} catch (PDOException $e) { + echo "Database connection failed: " . $e->getMessage() . "\n"; + echo "You'll need to manually update the api_test.php file with your API token.\n"; +} \ No newline at end of file diff --git a/ecomzone/readme_api_fix.txt b/ecomzone/readme_api_fix.txt new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/ecomzone/readme_api_fix.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ecomzone/test_api.php b/ecomzone/test_api.php new file mode 100644 index 0000000..52d87c0 --- /dev/null +++ b/ecomzone/test_api.php @@ -0,0 +1,174 @@ + 'https://dropship.ecomzone.eu/api', + 'ECOMZONE_API_TOKEN' => 'klRyAdrXaxL0s6PEUp7LDlH6T8aPSCtBY8NiEHsHiWpc6646K2TZPi5KMxUg' // Actual API token + ]; + return isset($config[$key]) ? $config[$key] : $default; + } + + public static function updateValue($key, $value) { + return true; + } + } +} + +if (!class_exists('Context')) { + class Context { + public $shop; + public static $instance; + + public function __construct() { + $this->shop = new stdClass(); + $this->shop->id = 1; + } + + public static function getContext() { + if (!self::$instance) { + self::$instance = new Context(); + } + return self::$instance; + } + } +} + +if (!class_exists('PrestaShopLogger')) { + class PrestaShopLogger { + public static function addLog($message, $severity = 1, $error_code = null, $object_type = null, $object_id = null, $allow_duplicate = false, $id_employee = null) { + return true; + } + } +} + +// Disable output buffering +if (ob_get_level()) { + ob_end_clean(); +} + +// Set headers for plain text output +header('Content-Type: text/plain'); +header('Cache-Control: no-cache, must-revalidate'); +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); + +echo "EcomZone API Test\n"; +echo "================\n\n"; + +try { + // Get API settings + $apiUrl = Configuration::get('ECOMZONE_API_URL'); + $apiToken = Configuration::get('ECOMZONE_API_TOKEN'); + + echo "API URL: " . $apiUrl . "\n"; + echo "API Token: " . (empty($apiToken) || $apiToken === 'YOUR_API_TOKEN' ? + "Not set - Please edit test_api.php and replace YOUR_API_TOKEN with your actual token" : + "Set (length: " . strlen($apiToken) . " chars)") . "\n\n"; + + if (empty($apiToken) || $apiToken === 'YOUR_API_TOKEN') { + throw new Exception("API token is not configured. Please edit test_api.php and replace YOUR_API_TOKEN with your actual token."); + } + + echo "Testing API connection...\n\n"; + + // Create API client + $client = new EcomZoneClient(); + + // Test API connectivity with catalog endpoint + echo "Requesting catalog data (page 1, limit 1)...\n"; + $start = microtime(true); + $result = $client->getCatalog(1, 1); + $end = microtime(true); + + echo "Request completed in " . round(($end - $start) * 1000, 2) . " ms\n"; + + if (isset($result['data']) && is_array($result['data'])) { + echo "Success! Received " . count($result['data']) . " product(s)\n"; + echo "Total products available: " . ($result['total'] ?? 'unknown') . "\n\n"; + + if (!empty($result['data'])) { + $sku = $result['data'][0]['sku'] ?? null; + + if ($sku) { + echo "Testing product detail retrieval...\n"; + echo "Requesting product with SKU: " . $sku . "\n"; + + $start = microtime(true); + $productDetail = $client->getProduct($sku); + $end = microtime(true); + + echo "Request completed in " . round(($end - $start) * 1000, 2) . " ms\n"; + + if (isset($productDetail['data']) && !empty($productDetail['data'])) { + echo "Success! Retrieved product details\n"; + echo "Product name: " . ($productDetail['data']['product_name'] ?? 'unknown') . "\n"; + } else { + echo "Error: Failed to retrieve product details\n"; + echo "Response: " . print_r($productDetail, true) . "\n"; + } + } + } + } else { + echo "Error: Invalid catalog response\n"; + echo "Response: " . print_r($result, true) . "\n"; + } + + echo "\nAPI test completed successfully!\n"; + +} catch (Exception $e) { + echo "ERROR: " . $e->getMessage() . "\n"; + + if ($e instanceof EcomZoneException) { + echo "Error Code: " . $e->getCode() . "\n"; + } + + echo "\nStack Trace:\n" . $e->getTraceAsString() . "\n"; + + echo "\nCheck the log file for more details: modules/ecomzone/log/ecomzone.log\n"; +} + +echo "\nLog contents (last 10 entries):\n"; +echo "==============================\n\n"; + +try { + $logEntries = EcomZoneLogger::getLogContents(10); + foreach ($logEntries as $entry) { + $context = ''; + if (!empty($entry['context'])) { + $context = ' - ' . json_encode($entry['context']); + } + echo $entry['timestamp'] . ' [' . $entry['level'] . '] ' . $entry['message'] . $context . "\n"; + } +} catch (Exception $e) { + echo "Failed to retrieve logs: " . $e->getMessage() . "\n"; +} \ No newline at end of file diff --git a/ecomzone/test_image_download.php b/ecomzone/test_image_download.php new file mode 100644 index 0000000..4047fd9 --- /dev/null +++ b/ecomzone/test_image_download.php @@ -0,0 +1,194 @@ +getMessage() . "\n"; +} + +echo "\n"; + +// Method 2: Using cURL with detailed debugging +echo "Using cURL with detailed debugging\n"; +try { + $tempFile = 'tmp/test_image_debug.jpg'; + + if (!function_exists('curl_init')) { + echo "FAILED: cURL is not installed\n"; + } else { + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $testImageUrl, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 5, + CURLOPT_TIMEOUT => 30, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + CURLOPT_HEADER => 1, // Include headers in the response + CURLOPT_VERBOSE => true, + ]); + + // Create a stream for the verbose output + $verbose = fopen('tmp/curl_verbose.log', 'w+'); + curl_setopt($ch, CURLOPT_STDERR, $verbose); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); + $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); + $error = curl_error($ch); + + // Extract headers and body + $headers = substr($response, 0, $headerSize); + $imageData = substr($response, $headerSize); + + curl_close($ch); + + // Close and read the verbose log + rewind($verbose); + $verboseLog = stream_get_contents($verbose); + fclose($verbose); + + echo "HTTP Status Code: $httpCode\n"; + echo "Content Type: $contentType\n"; + echo "Headers:\n$headers\n"; + + if ($httpCode !== 200 || empty($imageData)) { + echo "FAILED: HTTP Error $httpCode - $error\n"; + } else { + file_put_contents($tempFile, $imageData); + $fileSize = filesize($tempFile); + echo "Downloaded " . number_format($fileSize) . " bytes to $tempFile\n"; + + // Check if it's a valid image + $imageInfo = @getimagesize($tempFile); + if ($imageInfo) { + echo "SUCCESS: Valid image detected\n"; + echo "Image dimensions: " . $imageInfo[0] . 'x' . $imageInfo[1] . ", type: " . $imageInfo['mime'] . "\n"; + } else { + echo "WARNING: The downloaded file is not a valid image\n"; + + // Analyze the first 100 bytes of the content + echo "First 100 bytes of the content:\n"; + $contentPreview = bin2hex(substr($imageData, 0, 50)); + echo chunk_split($contentPreview, 2, ' ') . "\n"; + + // Show first 100 characters if it looks like text + echo "Content as text (first 100 chars):\n"; + $textPreview = substr($imageData, 0, 100); + echo preg_replace('/[^\x20-\x7E]/', '.', $textPreview) . "\n"; + } + + echo "\nCURL Verbose Log:\n$verboseLog\n"; + } + } +} catch (Exception $e) { + echo "FAILED: Exception occurred: " . $e->getMessage() . "\n"; +} + +// Try a publicly accessible image as a reference test +echo "\n\nTesting with a public reference image...\n"; +$publicImageUrl = 'https://www.php.net/images/logos/new-php-logo.svg'; +echo "Public Image URL: $publicImageUrl\n\n"; + +try { + $tempFile = 'tmp/test_reference_image.jpg'; + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $publicImageUrl, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 5, + CURLOPT_TIMEOUT => 30, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + ]); + + $imageData = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); + + curl_close($ch); + + echo "HTTP Status Code: $httpCode\n"; + echo "Content Type: $contentType\n"; + + if ($httpCode !== 200 || empty($imageData)) { + echo "FAILED: Could not download reference image\n"; + } else { + file_put_contents($tempFile, $imageData); + $fileSize = filesize($tempFile); + echo "Downloaded " . number_format($fileSize) . " bytes to $tempFile\n"; + + // Check if it's a valid image + $imageInfo = @getimagesize($tempFile); + if ($imageInfo) { + echo "SUCCESS: Valid image detected\n"; + echo "Image dimensions: " . $imageInfo[0] . 'x' . $imageInfo[1] . ", type: " . $imageInfo['mime'] . "\n"; + } else { + echo "WARNING: The downloaded file is not a valid image\n"; + } + } +} catch (Exception $e) { + echo "FAILED: Exception occurred: " . $e->getMessage() . "\n"; +} + +echo "\nTest completed\n"; \ No newline at end of file diff --git a/ecomzone/test_image_with_auth.php b/ecomzone/test_image_with_auth.php new file mode 100644 index 0000000..0944616 --- /dev/null +++ b/ecomzone/test_image_with_auth.php @@ -0,0 +1,318 @@ + $apiUrl, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_HTTPHEADER => [ + 'Accept: application/json', + 'Authorization: Bearer ' . $apiToken + ] + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); + $errorMsg = curl_error($ch); + curl_close($ch); + + echo "HTTP Status Code: $httpCode\n"; + echo "Content Type: $contentType\n"; + + if ($httpCode >= 400) { + echo "API Error: $errorMsg\n"; + return null; + } + + $data = json_decode($response, true); + if (json_last_error() !== JSON_ERROR_NONE) { + echo "JSON Decode Error: " . json_last_error_msg() . "\n"; + return null; + } + + return $data; +} + +// Use the specific SKU provided by the user +$sku = '1817-911'; +echo "Using the specific SKU: $sku\n\n"; + +// Skip catalog fetching step since we have a specific SKU to test + +// STEP 2: Get detailed product information +echo "STEP 2: Fetching detailed product information for SKU: $sku\n"; +$productData = makeApiRequest('product/' . urlencode($sku), $apiToken); + +if (!$productData || empty($productData['data'])) { + echo "Failed to get product details.\n"; + exit(1); +} + +// Extract image URLs from product data +$imageUrls = []; + +if (!empty($productData['data']['image'])) { + $imageUrls[] = $productData['data']['image']; + echo "Found main image: " . $productData['data']['image'] . "\n"; +} + +// Try to parse images field which might be JSON string or array +if (!empty($productData['data']['images'])) { + if (is_string($productData['data']['images'])) { + $parsedImages = json_decode($productData['data']['images'], true); + if (json_last_error() === JSON_ERROR_NONE && is_array($parsedImages)) { + foreach ($parsedImages as $img) { + $imageUrls[] = $img; + echo "Found additional image (from JSON string): $img\n"; + } + } + } elseif (is_array($productData['data']['images'])) { + foreach ($productData['data']['images'] as $img) { + $imageUrls[] = $img; + echo "Found additional image (from array): $img\n"; + } + } +} + +if (empty($imageUrls)) { + echo "No image URLs found in product data.\n"; + exit(1); +} + +// STEP 3: Try direct download of the first image with authorization +echo "\nSTEP 3: Attempting to download the first image directly with Authorization header\n"; +$firstImageUrl = $imageUrls[0]; +echo "Image URL: $firstImageUrl\n"; + +// Try with direct download +$ch = curl_init(); +curl_setopt_array($ch, [ + CURLOPT_URL => $firstImageUrl, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_HTTPHEADER => [ + 'Accept: image/webp,image/apng,image/*,*/*;q=0.8', + 'Authorization: Bearer ' . $apiToken + ] +]); + +$imageData = curl_exec($ch); +$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); +curl_close($ch); + +echo "HTTP Status Code: $httpCode\n"; +echo "Content Type: $contentType\n"; + +// Check if the response is HTML (authentication error) +$isHtml = stripos($contentType, 'text/html') !== false || + (strlen($imageData) > 15 && stripos($imageData, '') !== false); + +if ($isHtml) { + echo "FAILED: Authentication error. Received HTML instead of image data.\n"; + file_put_contents('tmp/auth_error.html', $imageData); + echo "Full HTML response saved to tmp/auth_error.html\n"; +} else { + $tempFile = 'tmp/direct_download.jpg'; + file_put_contents($tempFile, $imageData); + $fileSize = filesize($tempFile); + echo "Downloaded " . number_format($fileSize) . " bytes to $tempFile\n"; + + $imageInfo = @getimagesize($tempFile); + if ($imageInfo) { + echo "SUCCESS: Valid image detected\n"; + echo "Image dimensions: " . $imageInfo[0] . 'x' . $imageInfo[1] . ", type: " . $imageInfo['mime'] . "\n"; + } else { + echo "WARNING: The downloaded file is not a valid image\n"; + } +} + +// STEP 4: Check if there's a dedicated API endpoint for downloading images +echo "\nSTEP 4: Checking if there's a dedicated image download API endpoint\n"; + +// Extract image ID from the URL (last part after the slash) +$urlParts = explode('/', $firstImageUrl); +$imageId = end($urlParts); + +echo "Trying to access image via API using ID: $imageId\n"; + +// Try these possible API endpoints for image download +$possibleEndpoints = [ + 'image/' . $imageId, + 'download/image/' . $imageId, + 'media/' . $imageId, + 'product/image/' . $imageId, + 'product/' . $sku . '/image/' . $imageId +]; + +$imageDownloaded = false; +foreach ($possibleEndpoints as $endpoint) { + echo "\nTrying endpoint: $endpoint\n"; + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => 'https://dropship.ecomzone.eu/api/' . $endpoint, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_HTTPHEADER => [ + 'Accept: image/webp,image/apng,image/*,*/*;q=0.8', + 'Authorization: Bearer ' . $apiToken + ] + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); + curl_close($ch); + + echo "HTTP Status Code: $httpCode\n"; + echo "Content Type: $contentType\n"; + + if ($httpCode === 200 && !stripos($contentType, 'text/html')) { + $tempFile = 'tmp/api_' . basename($endpoint) . '.jpg'; + file_put_contents($tempFile, $response); + + $imageInfo = @getimagesize($tempFile); + if ($imageInfo) { + echo "SUCCESS: Valid image downloaded via API endpoint\n"; + echo "Image dimensions: " . $imageInfo[0] . 'x' . $imageInfo[1] . ", type: " . $imageInfo['mime'] . "\n"; + $imageDownloaded = true; + } else { + echo "Response doesn't appear to be a valid image\n"; + unlink($tempFile); + } + } +} + +if (!$imageDownloaded) { + echo "\nNone of the attempted API endpoints worked for direct image download.\n"; +} + +// STEP 5: Try using an alternative base URL for the image +echo "\nSTEP 5: Trying alternative base URLs for the image\n"; + +// Extract the path part from the URL +$parsedUrl = parse_url($firstImageUrl); +$imagePath = $parsedUrl['path'] ?? ''; + +if (empty($imagePath)) { + echo "Could not extract image path from URL.\n"; +} else { + echo "Image path: $imagePath\n"; + + // Try with alternative base URLs + $alternativeBaseUrls = [ + 'https://api.dropship.ecomzone.eu', + 'https://cdn.dropship.ecomzone.eu', + 'https://media.dropship.ecomzone.eu', + 'https://images.dropship.ecomzone.eu' + ]; + + foreach ($alternativeBaseUrls as $baseUrl) { + $fullUrl = $baseUrl . $imagePath; + echo "\nTrying alternative URL: $fullUrl\n"; + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $fullUrl, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_HTTPHEADER => [ + 'Accept: image/webp,image/apng,image/*,*/*;q=0.8', + 'Authorization: Bearer ' . $apiToken + ] + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE); + curl_close($ch); + + echo "HTTP Status Code: $httpCode\n"; + echo "Content Type: $contentType\n"; + + if ($httpCode === 200 && !stripos($contentType, 'text/html')) { + $tempFile = 'tmp/alt_domain_' . str_replace(['https://', '.'], ['', '_'], $baseUrl) . '.jpg'; + file_put_contents($tempFile, $response); + + $imageInfo = @getimagesize($tempFile); + if ($imageInfo) { + echo "SUCCESS: Valid image downloaded from alternative URL\n"; + echo "Image dimensions: " . $imageInfo[0] . 'x' . $imageInfo[1] . ", type: " . $imageInfo['mime'] . "\n"; + } else { + echo "Response doesn't appear to be a valid image\n"; + unlink($tempFile); + } + } + } +} + +// Summary +echo "\n=== SUMMARY ===\n"; +echo "Product SKU: $sku\n"; +echo "Found " . count($imageUrls) . " image URLs in product data\n"; +echo "Direct image downloads with Authorization header don't work - server returns HTML login page\n"; + +echo "\nPOSSIBLE SOLUTIONS:\n"; +echo "1. The EcomZone API doesn't support direct image downloads through Authorization headers\n"; +echo "2. We may need to use a different authentication method for images\n"; +echo "3. Images might require a signed URL or temporary access token\n"; +echo "4. Consider contacting EcomZone support for the correct way to download images\n"; +echo "\nTest completed\n"; \ No newline at end of file diff --git a/ecomzone/views/templates/admin/configure.tpl b/ecomzone/views/templates/admin/configure.tpl index 2c9718c..9bf5086 100644 --- a/ecomzone/views/templates/admin/configure.tpl +++ b/ecomzone/views/templates/admin/configure.tpl @@ -1,215 +1,357 @@ - -{* views/templates/admin/configure.tpl *} +{* +* @author EcomZone +* @copyright EcomZone +* @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 +*}

{l s='EcomZone Configuration' mod='ecomzone'}

-
-
- -
- + + + +
+
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+ +
+ + +
+

{l s='Product Synchronization' mod='ecomzone'}

+
+

{l s='Last synchronization:' mod='ecomzone'} {if $ECOMZONE_LAST_SYNC}{$ECOMZONE_LAST_SYNC}{else}{l s='Never' mod='ecomzone'}{/if}

+ +
+ +
+
+ +
+
+
-
- -
-

- {if $ECOMZONE_LAST_SYNC} - {$ECOMZONE_LAST_SYNC|escape:'html':'UTF-8'} - {else} - {l s='Never' mod='ecomzone'} - {/if} +

- - + +
+

{l s='Product Preview' mod='ecomzone'}

+
+
+ +
+
+ +
+
+
+ + {if isset($API_PRODUCTS) && $API_PRODUCTS} +
+ + + + + + + + + + + + {foreach from=$API_PRODUCTS item=product} + + + + + + + + {/foreach} + +
{l s='SKU' mod='ecomzone'}{l s='Name' mod='ecomzone'}{l s='Price' mod='ecomzone'}{l s='Stock' mod='ecomzone'}{l s='Status' mod='ecomzone'}
{$product.sku|escape:'html':'UTF-8'}{$product.product_name|escape:'html':'UTF-8'}{$product.product_price|escape:'html':'UTF-8'}{$product.stock|escape:'html':'UTF-8'} + {if isset($product.sync_status)} + + {$product.sync_status|escape:'html':'UTF-8'} + + {/if} +
+
+ + {if isset($PAGINATION)} +
+
    + {for $page=1 to $PAGINATION.total_pages} +
  • + {$page} +
  • + {/for} +
+
+ {/if} + {/if}
- +
+{if isset($SYNC_PROGRESS)}
-

{l s='Product Import' mod='ecomzone'}

+

{l s='Sync Progress' mod='ecomzone'}

-
-
-
- -
+
+
+ {$SYNC_PROGRESS.percentage}%
- +
+

+ {l s='Processed:' mod='ecomzone'} {$SYNC_PROGRESS.processed} / {$SYNC_PROGRESS.total} +

+
+
+{/if} - {if isset($API_PRODUCTS) && $API_PRODUCTS} -
- - - - - - - - - - - - {foreach from=$API_PRODUCTS item=product} - - - - - - + + + {/if} + +
{l s='SKU' mod='ecomzone'}{l s='Name' mod='ecomzone'}{l s='Price' mod='ecomzone'}{l s='Stock' mod='ecomzone'}{l s='Actions' mod='ecomzone'}
{$product.sku|escape:'html':'UTF-8'}{$product.product_name|escape:'html':'UTF-8'}{$product.product_price|escape:'html':'UTF-8'}{$product.stock|escape:'html':'UTF-8'} -
- - - - -

- {l s='Add this URL to your server\'s crontab to run every 24 hours.' mod='ecomzone'} -

- - + {else} +
{l s='No activity logs available' mod='ecomzone'}
-{if isset($ECOMZONE_LOGS) && $ECOMZONE_LOGS} -
-

{l s='Recent Logs' mod='ecomzone'}

-
-
- - - {foreach from=$ECOMZONE_LOGS item=log} - - - - {/foreach} - -
{$log|escape:'html':'UTF-8'}
-
-
-
-{/if} - - - + +