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
+*}