This commit is contained in:
echo committed 2025-06-24 21:46:54 +02:00
1 parent c8bcfe6bd6
commit 9c277e6db8
18 files changed
+2401 -566

No files matched your search

+8
View File
@@ -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
File renamed without changes.
BIN
View File
Binary file not shown.
Binary file not shown.
+61
View File
@@ -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)
+143
View File
@@ -0,0 +1,143 @@
<?php
// Simple API test script that doesn't rely on PrestaShop configuration
// Display errors
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// Define required constants
if (!defined('_PS_VERSION_')) {
define('_PS_VERSION_', '8.0.0');
}
if (!defined('_PS_MODULE_DIR_')) {
define('_PS_MODULE_DIR_', __DIR__ . '/../../modules/');
}
if (!defined('_PS_ROOT_DIR_')) {
define('_PS_ROOT_DIR_', dirname(__DIR__, 2));
}
// Include required files directly
require_once(__DIR__ . '/classes/EcomZoneException.php');
require_once(__DIR__ . '/classes/EcomZoneLogger.php');
require_once(__DIR__ . '/classes/EcomZoneClient.php');
// Set headers for plain text output
header('Content-Type: text/plain');
header('Cache-Control: no-cache, must-revalidate');
echo "EcomZone API Test (Standalone)\n";
echo "============================\n\n";
// Create a mock Configuration class if it doesn't exist
if (!class_exists('Configuration')) {
class Configuration {
public static function get($key, $default = null) {
$config = [
'ECOMZONE_API_URL' => '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";
}
+226 -33
View File
@@ -8,25 +8,16 @@ class EcomZoneClient
private $apiToken; private $apiToken;
private $apiUrl; private $apiUrl;
private $maxRetries = 3; private $maxRetries = 3;
private $timeout = 30; private $timeout = 60; // Increased timeout
public function __construct() public function __construct()
{ {
$this->apiToken = Configuration::get("ECOMZONE_API_TOKEN"); $this->apiToken = Configuration::get("ECOMZONE_API_TOKEN");
$this->apiUrl = Configuration::get("ECOMZONE_API_URL"); $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)) { if (empty($this->apiUrl)) {
throw new EcomZoneException( $this->apiUrl = 'https://dropship.ecomzone.eu/api'; // Default API URL
"API URL not configured", Configuration::updateValue("ECOMZONE_API_URL", $this->apiUrl);
EcomZoneException::API_ERROR
);
} }
EcomZoneLogger::log("API Client initialized", "INFO", [ 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) public function getCatalog($page = 1, $perPage = 100)
{ {
$url = $this->apiUrl . "/catalog"; $this->validateApiCredentials();
$params = [ $params = [
"page" => $page, "page" => $page,
"per_page" => $perPage, "per_page" => $perPage,
]; ];
return $this->makeRequest("GET", $url, $params); return $this->makeRequest("catalog", "GET", $params);
} }
public function getProduct($sku) public function getProduct($sku)
{ {
$url = $this->apiUrl . "/product/" . urlencode($sku); $this->validateApiCredentials();
return $this->makeRequest("GET", $url);
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; $retryCount = 0;
$lastError = null; $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 { do {
try { try {
EcomZoneLogger::log("Making API request", "INFO", [ EcomZoneLogger::log("Making API request", "INFO", [
"method" => $method, "method" => $method,
"url" => $url, "url" => $url,
"params" => $params,
"retry" => $retryCount, "retry" => $retryCount,
"params" => $params
]); ]);
// Add query parameters
if (!empty($params)) {
$url .=
(strpos($url, "?") === false ? "?" : "&") .
http_build_query($params);
}
$curl = curl_init(); $curl = curl_init();
$options = [ $options = [
CURLOPT_URL => $url, CURLOPT_URL => $url,
@@ -86,36 +93,108 @@ class EcomZoneClient
"Authorization: Bearer " . $this->apiToken, "Authorization: Bearer " . $this->apiToken,
"Accept: application/json", "Accept: application/json",
"Content-Type: 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) { 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); curl_setopt_array($curl, $options);
// Execute the request
$response = curl_exec($curl); $response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
$contentType = curl_getinfo($curl, CURLINFO_CONTENT_TYPE);
$error = curl_error($curl); $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); curl_close($curl);
if ($error) { if ($errorNo > 0) {
throw new Exception("cURL Error: " . $error); throw new Exception("cURL Error ({$errorNo}): " . $error);
} }
// Handle HTTP errors
if ($httpCode >= 400) { if ($httpCode >= 400) {
throw new Exception( $errorMessage = "HTTP Error: " . $httpCode;
"HTTP Error: " . $httpCode . " Response: " . $response
); // 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); $decodedResponse = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) { 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", [ EcomZoneLogger::log("API request successful", "INFO", [
"http_code" => $httpCode, "http_code" => $httpCode,
"content_type" => $contentType
]); ]);
return $decodedResponse; return $decodedResponse;
@@ -126,10 +205,18 @@ class EcomZoneClient
EcomZoneLogger::log("API request failed", "ERROR", [ EcomZoneLogger::log("API request failed", "ERROR", [
"error" => $e->getMessage(), "error" => $e->getMessage(),
"retry" => $retryCount, "retry" => $retryCount,
"url" => $url,
"trace" => $e->getTraceAsString()
]); ]);
if ($retryCount < $this->maxRetries) { 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; continue;
} }
@@ -142,11 +229,117 @@ class EcomZoneClient
} }
} while ($retryCount < $this->maxRetries); } while ($retryCount < $this->maxRetries);
// This should never be reached, but just in case
throw new EcomZoneException( throw new EcomZoneException(
"Max retries reached: " . $lastError->getMessage(), "Max retries reached: " . $lastError->getMessage(),
EcomZoneException::API_ERROR, EcomZoneException::API_ERROR,
$lastError $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, '<!DOCTYPE html>') !== 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;
}
} }
+146 -39
View File
@@ -5,66 +5,173 @@ if (!defined("_PS_VERSION_")) {
class EcomZoneLogger class EcomZoneLogger
{ {
private static $logFile = 'ecomzone.log'; const LOG_FILE = 'ecomzone.log';
private static $maxLogSize = 10485760; // 10MB
public static function log($message, $level = "INFO", $context = []) // 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 { try {
$logDir = _PS_MODULE_DIR_ . 'ecomzone/log/'; // Check if we should log this level
if (!is_dir($logDir)) { $logLevel = self::getLogLevel();
mkdir($logDir, 0755, true); if (self::LEVELS[$level] < self::LEVELS[$logLevel]) {
return true; // Skip logging for less severe levels
} }
$timestamp = date('Y-m-d H:i:s'); // Ensure the log directory exists
$contextStr = !empty($context) ? json_encode($context) : ''; $logDir = _PS_MODULE_DIR_ . 'ecomzone/log';
if (!is_dir($logDir)) {
if (!@mkdir($logDir, 0755, true)) {
throw new Exception("Failed to create log directory: " . $logDir);
}
@chmod($logDir, 0755); // Ensure the directory is writable
}
$logMessage = sprintf( // Format the log entry
"[%s] %s: %s %s\n", $logEntry = [
$timestamp, 'timestamp' => date('Y-m-d H:i:s'),
$level, 'level' => strtoupper($level),
$message, 'message' => $message,
$contextStr 'context' => $context
); ];
file_put_contents( // For errors and critical issues, also include a stack trace
$logDir . self::$logFile, if (in_array($level, ['ERROR', 'CRITICAL']) && !isset($context['trace'])) {
$logMessage, $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
FILE_APPEND $logEntry['trace'] = array_slice($trace, 1); // Skip the log function itself
); }
} catch (Exception $e) {
// Fallback to PrestaShop's logger $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( PrestaShopLogger::addLog(
"EcomZone: $message", "EcomZone: {$message} - Context: {$contextStr}",
($level === 'ERROR' ? 3 : 1) $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());
} }
private static function initLogFile() return false;
}
}
private static function rotateLogFile($logFile)
{ {
if (!file_exists(self::$logFile)) { try {
$logDir = dirname(self::$logFile); $backup = $logFile . '.' . date('Y-m-d-H-i-s') . '.bak';
if (!is_dir($logDir)) { if (!@rename($logFile, $backup)) {
mkdir($logDir, 0755, true); throw new Exception("Failed to rotate log file");
} }
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);
} }
} }
private static function rotateLogIfNeeded() return true;
} catch (Exception $e) {
error_log('EcomZone log rotation error: ' . $e->getMessage());
return false;
}
}
public static function getLogPath()
{ {
if (!file_exists(self::$logFile)) { // When running in standalone mode, use the local directory
return; if (!defined('_PS_MODULE_DIR_') || !file_exists(_PS_MODULE_DIR_ . 'ecomzone')) {
return __DIR__ . '/../log/' . self::LOG_FILE;
} }
if (filesize(self::$logFile) > self::$maxLogSize) { return _PS_MODULE_DIR_ . 'ecomzone/log/' . self::LOG_FILE;
$backup = self::$logFile . "." . date("Y-m-d-H-i-s") . ".bak";
rename(self::$logFile, $backup);
touch(self::$logFile);
chmod(self::$logFile, 0666);
} }
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;
} }
} }
+269 -145
View File
@@ -3,6 +3,8 @@ if (!defined("_PS_VERSION_")) {
exit(); exit();
} }
require_once dirname(__FILE__) . '/interfaces/IProductSync.php';
class EcomZoneProductSync implements IProductSync class EcomZoneProductSync implements IProductSync
{ {
private EcomZoneClient $client; private EcomZoneClient $client;
@@ -22,17 +24,30 @@ class EcomZoneProductSync implements IProductSync
public function importProducts(int $perPage = 100): array public function importProducts(int $perPage = 100): array
{ {
$page = 1;
$totalImported = 0;
$totalAvailable = 0;
$errors = [];
try { 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, "per_page" => $perPage,
"total_imported" => $totalImported,
"is_new_sync" => empty($syncState)
]); ]);
do { // Get catalog for current page
$catalog = $this->client->getCatalog($page, $perPage); $catalog = $this->client->getCatalog($page, $perPage);
if (!isset($catalog["data"]) || !is_array($catalog["data"])) { if (!isset($catalog["data"]) || !is_array($catalog["data"])) {
@@ -42,13 +57,26 @@ class EcomZoneProductSync implements IProductSync
); );
} }
// Update total available on first page or if it changes
if ($page === 1 || ($catalog["total"] ?? 0) !== $totalAvailable) {
$totalAvailable = $catalog["total"] ?? 0;
}
$productsProcessed = 0;
$batchErrors = [];
// Process only a batch of products
foreach ($catalog["data"] as $product) { foreach ($catalog["data"] as $product) {
if ($productsProcessed >= $maxProductsPerBatch) {
break;
}
try { try {
if ($this->importSingleProduct($product)) { if ($this->importSingleProduct($product)) {
$totalImported++; $totalImported++;
} }
} catch (Exception $e) { } catch (Exception $e) {
$errors[] = [ $batchErrors[] = [
"sku" => $product["sku"] ?? "unknown", "sku" => $product["sku"] ?? "unknown",
"error" => $e->getMessage(), "error" => $e->getMessage(),
]; ];
@@ -57,28 +85,51 @@ class EcomZoneProductSync implements IProductSync
"error" => $e->getMessage(), "error" => $e->getMessage(),
]); ]);
} }
$productsProcessed++;
} }
$totalAvailable = $catalog["total"] ?? 0; // Update errors array with new batch errors
$page++; $errors = array_merge($errors, $batchErrors);
EcomZoneLogger::log("Page processed", "INFO", [ // Calculate progress
"page" => $page - 1, $progress = [
"imported" => $totalImported, 'current_page' => $page,
"total" => $totalAvailable, 'total_imported' => $totalImported,
]); 'total_available' => $totalAvailable,
} while ( 'errors' => $errors,
isset($catalog["next_page_url"]) && 'is_complete' => false,
$catalog["next_page_url"] !== null '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(); $this->clearCache();
}
return [ return [
"success" => true, "success" => true,
"imported" => $totalImported, "imported" => $totalImported,
"total" => $totalAvailable, "total" => $totalAvailable,
"errors" => $errors, "errors" => $errors,
"is_complete" => $progress['is_complete'],
"current_page" => $progress['current_page']
]; ];
} catch (Exception $e) { } catch (Exception $e) {
EcomZoneLogger::log("Product import process failed", "ERROR", [ EcomZoneLogger::log("Product import process failed", "ERROR", [
@@ -192,8 +243,11 @@ class EcomZoneProductSync implements IProductSync
$product->name[$this->defaultLangId] = $data['product_name']; $product->name[$this->defaultLangId] = $data['product_name'];
$product->description[$this->defaultLangId] = $data['long_description'] ?? $data['description']; $product->description[$this->defaultLangId] = $data['long_description'] ?? $data['description'];
$product->description_short[$this->defaultLangId] = $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->reference = $data['sku'];
$product->ean13 = $data['ean'] ?? ''; $product->ean13 = $data['ean'] ?? '';
@@ -354,15 +408,32 @@ class EcomZoneProductSync implements IProductSync
private function updateProductStock(Product $product, array $data): void private function updateProductStock(Product $product, array $data): void
{ {
try { try {
$quantity = (int) ($data['stock_quantity'] ?? 0); $quantity = (int)($data['stock'] ?? 0);
StockAvailable::setQuantity($product->id, 0, $quantity);
// 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 // Update out of stock behavior
StockAvailable::setProductOutOfStock( StockAvailable::setProductOutOfStock(
$product->id, $product->id,
$quantity > 0 ? 2 : 0, // 2 = Allow orders, 0 = Deny orders 2, // Always allow orders
$this->defaultShopId $this->defaultShopId
); );
EcomZoneLogger::log("Stock updated", "DEBUG", [
"product" => $product->reference,
"quantity" => $quantity
]);
} catch (Exception $e) { } catch (Exception $e) {
EcomZoneLogger::log("Failed to update stock", "WARNING", [ EcomZoneLogger::log("Failed to update stock", "WARNING", [
"product" => $product->reference, "product" => $product->reference,
@@ -376,46 +447,123 @@ class EcomZoneProductSync implements IProductSync
try { try {
$imageUrls = []; $imageUrls = [];
// Process main image EcomZoneLogger::log("Raw product data for images", "DEBUG", [
if (!empty($data["image"])) { "product" => $product->reference,
$imageUrls[] = $data["image"]; "data" => json_encode($data)
]);
// 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']);
} }
// Process additional images // Add main image if available
if (!empty($data["images"])) { if (!empty($data['image'])) {
$additionalImages = json_decode($data["images"], true); array_unshift($imageUrls, $data['image']); // Add as first image
if (is_array($additionalImages)) {
$imageUrls = array_merge($imageUrls, $additionalImages);
}
} }
// Filter out empty or invalid URLs
$imageUrls = array_filter($imageUrls, function($url) {
return !empty($url) && filter_var($url, FILTER_VALIDATE_URL);
});
if (empty($imageUrls)) { if (empty($imageUrls)) {
EcomZoneLogger::log("No valid images found for product", "WARNING", [
"product" => $product->reference,
"data" => json_encode($data)
]);
return; return;
} }
// Delete existing images if any // Delete existing images if any
$product->deleteImages(); $product->deleteImages();
$successCount = 0;
foreach ($imageUrls as $index => $imageUrl) { foreach ($imageUrls as $index => $imageUrl) {
try {
EcomZoneLogger::log("Processing image", "DEBUG", [
"product" => $product->reference,
"image_url" => $imageUrl,
"index" => $index
]);
$this->importProductImage($product, $imageUrl, $index === 0); $this->importProductImage($product, $imageUrl, $index === 0);
} $successCount++;
} catch (Exception $e) { } catch (Exception $e) {
EcomZoneLogger::log("Failed to process product images", "WARNING", [ 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", "ERROR", [
"sku" => $product->reference, "sku" => $product->reference,
"error" => $e->getMessage(), "error" => $e->getMessage(),
"data" => json_encode($data)
]); ]);
throw $e;
} }
} }
private function importProductImage(Product $product, string $imageUrl, bool $cover = false): void private function importProductImage(Product $product, string $imageUrl, bool $cover = false): void
{ {
try { try {
// Create temporary file // Create temporary directory if it doesn't exist
$tempFile = tempnam(_PS_TMP_IMG_DIR_, 'import_'); $tempDir = _PS_MODULE_DIR_ . 'ecomzone/tmp/';
if (!is_dir($tempDir)) {
mkdir($tempDir, 0755, true);
chmod($tempDir, 0755);
}
// Download image // Clean URL - handle double escaped slashes
if (!copy($imageUrl, $tempFile)) { $cleanUrl = stripslashes(stripslashes(trim($imageUrl)));
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 // Create image object
@@ -433,29 +581,60 @@ class EcomZoneProductSync implements IProductSync
$imageDir = dirname($imagePath); $imageDir = dirname($imagePath);
if (!file_exists($imageDir)) { 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 // Generate image
if (!ImageManager::resize( if (!ImageManager::resize(
$tempFile, $tempFile,
$imagePath . '.jpg' $imagePath . '.jpg',
null,
null,
'jpg'
)) { )) {
throw new Exception("Failed to process image"); throw new Exception("Failed to process image");
} }
// Ensure the generated image has correct permissions
chmod($imagePath . '.jpg', 0644);
// Generate thumbnails // Generate thumbnails
$this->generateThumbnails($image); $this->generateThumbnails($image);
// Clean up // Clean up
if (file_exists($tempFile)) {
unlink($tempFile); unlink($tempFile);
}
EcomZoneLogger::log("Image imported successfully", "DEBUG", [
"product" => $product->reference,
"image_url" => $cleanUrl,
"mime_type" => $imageInfo['mime']
]);
} catch (Exception $e) { } catch (Exception $e) {
EcomZoneLogger::log("Failed to import product image", "ERROR", [ EcomZoneLogger::log("Failed to import product image", "ERROR", [
"product" => $product->reference, "product" => $product->reference,
"image_url" => $imageUrl, "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 { try {
$imageTypes = ImageType::getImagesTypes('products'); $imageTypes = ImageType::getImagesTypes('products');
foreach ($imageTypes as $imageType) { $sourceFile = _PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '.jpg';
$dir = _PS_PROD_IMG_DIR_ . $image->getExistingImgPath();
if (!file_exists($dir)) { if (!file_exists($sourceFile)) {
mkdir($dir, 0755, true); throw new Exception("Source image file not found");
} }
ImageManager::resize(
_PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '.jpg', foreach ($imageTypes as $imageType) {
_PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '-' . stripslashes($imageType['name']) . '.jpg', $destination = _PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '-' . stripslashes($imageType['name']) . '.jpg';
$dir = dirname($destination);
if (!file_exists($dir)) {
if (!mkdir($dir, 0755, true)) {
throw new Exception("Failed to create thumbnail directory: " . $dir);
}
chmod($dir, 0755);
}
if (!ImageManager::resize(
$sourceFile,
$destination,
(int)$imageType['width'], (int)$imageType['width'],
(int)$imageType['height'] (int)$imageType['height']
); )) {
throw new Exception("Failed to generate thumbnail: " . $imageType['name']);
}
// Set proper permissions for the thumbnail
chmod($destination, 0644);
} }
} catch (Exception $e) { } catch (Exception $e) {
EcomZoneLogger::log("Failed to generate thumbnails", "WARNING", [ EcomZoneLogger::log("Failed to generate thumbnails", "WARNING", [
"image_id" => $image->id, "image_id" => $image->id,
"error" => $e->getMessage() "error" => $e->getMessage()
]); ]);
} throw $e;
}
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
);
}
} }
} }
@@ -716,6 +819,27 @@ class EcomZoneProductSync implements IProductSync
Media::clearCache(); Media::clearCache();
PrestaShopAutoload::getInstance()->generateIndex(); 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'); $token = Configuration::get('ECOMZONE_CRON_TOKEN');
@@ -5,6 +5,17 @@ if (!defined('_PS_VERSION_')) {
interface IProductSync 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; 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; public function importSingleProduct(array $productData): bool;
} }
-123
View File
@@ -1,123 +0,0 @@
<?php
ob_start(); // Start output buffering
// Include PrestaShop configuration
require_once dirname(__FILE__) . "/../../config/config.inc.php";
require_once dirname(__FILE__) . "/../../init.php";
// Autoloader for module classes
spl_autoload_register(function ($className) {
$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'
];
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);
}
+519 -90
View File
@@ -20,6 +20,9 @@ class Ecomzone extends Module
parent::__construct(); parent::__construct();
// Register autoloader
spl_autoload_register([$this, 'autoload']);
$this->displayName = $this->trans( $this->displayName = $this->trans(
"EcomZone Dropshipping", "EcomZone Dropshipping",
[], [],
@@ -37,57 +40,313 @@ class Ecomzone extends Module
); );
} }
public function install() /**
* Autoload EcomZone classes
*/
public function autoload($className)
{ {
if (!parent::install() // Define class mapping
|| !$this->registerHook('actionCronJob') $classMap = [
|| !$this->registerHook('actionProductUpdate') 'EcomZoneException' => 'classes/EcomZoneException.php',
|| !$this->registerHook('actionProductDelete') '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'
];
// 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; 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() 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 = [ $dirs = [
_PS_MODULE_DIR_ . $this->name . '/log', _PS_MODULE_DIR_ . $this->name . '/log',
_PS_MODULE_DIR_ . $this->name . '/tmp' _PS_MODULE_DIR_ . $this->name . '/tmp',
_PS_IMG_DIR_,
_PS_PROD_IMG_DIR_
]; ];
foreach ($dirs as $dir) { foreach ($dirs as $dir) {
if (!file_exists($dir)) { if (!file_exists($dir)) {
mkdir($dir, 0755, true); 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() public function uninstall()
{ {
return parent::uninstall() && EcomZoneLogger::log("Starting module uninstallation", "INFO");
Configuration::deleteByName("ECOMZONE_API_URL") &&
Configuration::deleteByName("ECOMZONE_API_TOKEN") && try {
Configuration::deleteByName("ECOMZONE_CRON_TOKEN") && // Remove configuration values
Configuration::deleteByName("ECOMZONE_LAST_SYNC"); $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() public function getContent()
{ {
// Ensure no output before AJAX responses
if (Tools::getValue('ajax')) {
ob_clean();
header('Content-Type: application/json');
}
$output = ""; $output = "";
// Get admin token
$token = Tools::getAdminTokenLite('AdminModules');
if (Tools::isSubmit("submitEcomZoneModule")) { if (Tools::isSubmit("submitEcomZoneModule")) {
$apiToken = Tools::getValue("ECOMZONE_API_TOKEN"); $apiToken = Tools::getValue("ECOMZONE_API_TOKEN");
if (Configuration::updateValue("ECOMZONE_API_TOKEN", $apiToken)) { $apiUrl = Tools::getValue("ECOMZONE_API_URL");
Configuration::updateValue("ECOMZONE_API_TOKEN", $apiToken);
Configuration::updateValue("ECOMZONE_API_URL", $apiUrl);
$output .= $this->displayConfirmation( $output .= $this->displayConfirmation(
$this->trans( $this->trans(
"Settings updated", "Settings updated",
@@ -96,19 +355,147 @@ class Ecomzone extends Module
) )
); );
} }
// 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.'));
} }
// Handle product fetch request $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("<br>", $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 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")) { if (Tools::isSubmit("fetchProducts") || Tools::getValue("page")) {
try { try {
$page = (int) Tools::getValue("page", 1); $page = (int) Tools::getValue("page", 1);
$perPage = 10; $perPage = 10;
$result = $this->makeApiRequest( $result = $this->makeApiRequest(
"catalog?page={$page}&per_page={$perPage}" "catalog?page={$page}&per_page={$perPage}",
"GET"
); );
if (!empty($result["data"])) { 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); $totalPages = ceil($result["total"] / $perPage);
$this->context->smarty->assign([ $this->context->smarty->assign([
@@ -131,15 +518,27 @@ class Ecomzone extends Module
} }
} }
} catch (Exception $e) { } catch (Exception $e) {
EcomZoneLogger::log("Product fetch failed", "ERROR", [
"error" => $e->getMessage()
]);
$output .= $this->displayError($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([ $this->context->smarty->assign([
"module_dir" => $this->_path, "module_dir" => $this->_path,
"ECOMZONE_API_TOKEN" => Configuration::get("ECOMZONE_API_TOKEN"), "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"), "ECOMZONE_LAST_SYNC" => Configuration::get("ECOMZONE_LAST_SYNC"),
"SYNC_PROGRESS" => $syncProgress,
"ACTIVITY_LOG" => $activityLog,
"token" => $token,
"current_url" => "current_url" =>
$this->context->link->getAdminLink("AdminModules", false) . $this->context->link->getAdminLink("AdminModules", false) .
"&configure=" . "&configure=" .
@@ -151,78 +550,77 @@ class Ecomzone extends Module
"shop_url" => $this->context->link->getBaseLink(), "shop_url" => $this->context->link->getBaseLink(),
]); ]);
return $output . return $output . $this->display(__FILE__, "views/templates/admin/configure.tpl");
$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) private function makeApiRequest($endpoint, $method = "GET", $data = null)
{ {
$apiUrl = Configuration::get("ECOMZONE_API_URL"); try {
$apiToken = Configuration::get("ECOMZONE_API_TOKEN"); // Use the EcomZoneClient for all API requests
$client = new EcomZoneClient();
if (empty($apiToken)) { // Add endpoint to URL
throw new Exception( $params = [];
$this->trans(
"API token not configured", // Extract query params if present in endpoint
[], if (strpos($endpoint, '?') !== false) {
"Modules.Ecomzone.Admin" list($endpoint, $queryString) = explode('?', $endpoint, 2);
) parse_str($queryString, $params);
);
} }
$curl = curl_init(); // Ensure method is a string and not an array (prevent 405 Method Not Allowed)
$url = rtrim($apiUrl, "/") . "/" . ltrim($endpoint, "/"); if (!is_string($method)) {
EcomZoneLogger::log("Invalid method type detected", "WARNING", [
$options = [ 'endpoint' => $endpoint,
CURLOPT_URL => $url, 'method_type' => gettype($method),
CURLOPT_RETURNTRANSFER => true, 'method' => $method
CURLOPT_ENCODING => "", ]);
CURLOPT_MAXREDIRS => 10, $method = "GET"; // Default to GET if method is not a string
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); // Make the request using our client
if ($method === 'GET') {
$response = curl_exec($curl); return $client->makeRequest($endpoint, $method, $params);
$err = curl_error($curl); } else {
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); return $client->makeRequest($endpoint, $method, [], $data);
curl_close($curl);
if ($err) {
throw new Exception("cURL Error: " . $err);
} }
} catch (Exception $e) {
if ($httpCode >= 400) { EcomZoneLogger::log("API Request Failed in module", "ERROR", [
$errorData = json_decode($response, true); 'endpoint' => $endpoint,
$errorMessage = isset($errorData['error']) 'method' => $method,
? $errorData['error'] 'error' => $e->getMessage(),
: "HTTP Error: " . $httpCode; 'trace' => $e->getTraceAsString()
throw new EcomZoneException( ]);
$errorMessage, throw $e;
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) public function hookActionCronJob($params)
@@ -245,7 +643,8 @@ class Ecomzone extends Module
do { do {
$result = $this->makeApiRequest( $result = $this->makeApiRequest(
"catalog?page={$page}&per_page={$perPage}" "catalog?page={$page}&per_page={$perPage}",
"GET"
); );
if (empty($result["data"])) { 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()
]);
}
}
} }
+53
View File
@@ -0,0 +1,53 @@
<?php
// Simple script to get the API token from the PrestaShop configuration
// Check if we're running from command line
$cli = php_sapi_name() === 'cli';
// Set content type for browser
if (!$cli) {
header('Content-Type: text/plain');
}
// Try to load PrestaShop configuration
$configFile = __DIR__ . '/../../config/config.inc.php';
if (file_exists($configFile)) {
include_once($configFile);
// If Configuration class exists, try to get token
if (class_exists('Configuration')) {
$token = Configuration::get('ECOMZONE_API_TOKEN');
echo "API Token from configuration: " . ($token ? $token : "Not set") . "\n";
exit;
}
}
// If PrestaShop config doesn't work, try direct DB connection using default credentials
$dbConfig = [
'host' => '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";
}
+1
View File
@@ -0,0 +1 @@
+174
View File
@@ -0,0 +1,174 @@
<?php
// Script to test API connectivity
// Display errors
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// Define required constants if not defined
if (!defined('_PS_VERSION_')) {
define('_PS_VERSION_', '8.0.0');
}
if (!defined('_PS_MODULE_DIR_')) {
define('_PS_MODULE_DIR_', __DIR__ . '/../../modules/');
}
if (!defined('_PS_ROOT_DIR_')) {
define('_PS_ROOT_DIR_', dirname(__DIR__, 2));
}
// Load required classes directly
require_once(dirname(__FILE__) . '/classes/EcomZoneLogger.php');
require_once(dirname(__FILE__) . '/classes/EcomZoneException.php');
require_once(dirname(__FILE__) . '/classes/EcomZoneClient.php');
// Create log directory if it doesn't exist
$logDir = __DIR__ . '/log';
if (!file_exists($logDir)) {
mkdir($logDir, 0755, true);
}
// Mock PrestaShop classes
if (!class_exists('Configuration')) {
class Configuration {
public static function get($key, $default = null) {
$config = [
'ECOMZONE_API_URL' => '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";
}
+194
View File
@@ -0,0 +1,194 @@
<?php
// Test script for image downloading capabilities
// Display errors
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// Create necessary directories
$directories = [
'tmp',
'images',
'images/products',
'log'
];
echo "Setting up test directories...\n";
foreach ($directories as $dir) {
if (!file_exists($dir)) {
if (mkdir($dir, 0777, true)) {
echo "Created directory: $dir\n";
} else {
echo "Failed to create directory: $dir\n";
}
} else {
echo "Directory exists: $dir\n";
}
chmod($dir, 0777);
}
// Define test image URL
$testImageUrl = 'https://dropship.ecomzone.eu/storage/products/2023/10/05/2442_911_01_Xmas_bag_2_6BFQAO9yv8.jpg';
echo "\nTesting image download...\n";
echo "Image URL: $testImageUrl\n\n";
// Method 1: Using file_get_contents
echo "Method 1: Using file_get_contents\n";
try {
$tempFile = 'tmp/test_image_1.jpg';
$imageData = @file_get_contents($testImageUrl);
if ($imageData === false) {
echo "FAILED: Could not download image using file_get_contents\n";
echo "Error: " . error_get_last()['message'] . "\n";
} else {
file_put_contents($tempFile, $imageData);
$fileSize = filesize($tempFile);
echo "SUCCESS: Downloaded " . number_format($fileSize) . " bytes to $tempFile\n";
// Check if it's a valid image
$imageInfo = getimagesize($tempFile);
if ($imageInfo) {
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 "\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";
+318
View File
@@ -0,0 +1,318 @@
<?php
// Test script for downloading images with API token authentication
// Display errors
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// Create necessary directories
$directories = [
'tmp',
'images',
'images/products',
'log'
];
echo "Setting up test directories...\n";
foreach ($directories as $dir) {
if (!file_exists($dir)) {
if (mkdir($dir, 0777, true)) {
echo "Created directory: $dir\n";
} else {
echo "Failed to create directory: $dir\n";
}
} else {
echo "Directory exists: $dir\n";
}
chmod($dir, 0777);
}
// Prompt for API token if not set
if (!isset($argv[1]) || empty($argv[1])) {
echo "Please enter your EcomZone API token: ";
$apiToken = trim(fgets(STDIN));
} else {
$apiToken = $argv[1];
}
echo "\nTesting with API token: " . substr($apiToken, 0, 5) . '...' . substr($apiToken, -5) . "\n\n";
// Function to make API requests
function makeApiRequest($endpoint, $apiToken) {
$apiUrl = 'https://dropship.ecomzone.eu/api/' . ltrim($endpoint, '/');
echo "Requesting: $apiUrl\n";
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $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, '<!DOCTYPE html>') !== 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";
+278 -136
View File
@@ -1,28 +1,28 @@
{*
{* views/templates/admin/configure.tpl *} * @author EcomZone
* @copyright EcomZone
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0
*}
<div class="panel"> <div class="panel">
<h3><i class="icon icon-cogs"></i> {l s='EcomZone Configuration' mod='ecomzone'}</h3> <h3><i class="icon icon-cogs"></i> {l s='EcomZone Configuration' mod='ecomzone'}</h3>
<form method="post" class="form-horizontal">
<form id="configuration_form" class="defaultForm form-horizontal" action="{$current_url}" method="post">
<input type="hidden" name="token" value="{$token}" />
<div class="panel">
<div class="form-wrapper">
<div class="form-group"> <div class="form-group">
<label class="control-label col-lg-3">{l s='API Token' mod='ecomzone'}</label> <label class="control-label col-lg-3">{l s='API URL' mod='ecomzone'}</label>
<div class="col-lg-6"> <div class="col-lg-9">
<input type="text" name="ECOMZONE_API_TOKEN" <input type="text" name="ECOMZONE_API_URL" value="{$ECOMZONE_API_URL|escape:'html':'UTF-8'}" class="form-control" required="required" />
value="{$ECOMZONE_API_TOKEN|escape:'html':'UTF-8'}"
class="form-control" />
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-lg-3">{l s='Last Sync' mod='ecomzone'}</label> <label class="control-label col-lg-3">{l s='API Token' mod='ecomzone'}</label>
<div class="col-lg-6"> <div class="col-lg-9">
<p class="form-control-static"> <input type="text" name="ECOMZONE_API_TOKEN" value="{$ECOMZONE_API_TOKEN|escape:'html':'UTF-8'}" class="form-control" required="required" />
{if $ECOMZONE_LAST_SYNC} </div>
{$ECOMZONE_LAST_SYNC|escape:'html':'UTF-8'}
{else}
{l s='Never' mod='ecomzone'}
{/if}
</p>
</div> </div>
</div> </div>
@@ -31,17 +31,47 @@
<i class="process-icon-save"></i> {l s='Save' mod='ecomzone'} <i class="process-icon-save"></i> {l s='Save' mod='ecomzone'}
</button> </button>
</div> </div>
</div>
</form> </form>
</div>
<div class="panel"> <div class="panel">
<h3><i class="icon icon-cloud-download"></i> {l s='Product Import' mod='ecomzone'}</h3> <h3>{l s='Product Synchronization' mod='ecomzone'}</h3>
<div class="panel-body"> <div class="form-wrapper">
<form method="post" class="form-horizontal"> <p>{l s='Last synchronization:' mod='ecomzone'} <strong>{if $ECOMZONE_LAST_SYNC}{$ECOMZONE_LAST_SYNC}{else}{l s='Never' mod='ecomzone'}{/if}</strong></p>
<form action="{$current_url}" method="post" class="form-horizontal" id="sync-form">
<input type="hidden" name="token" value="{$token}" />
<div class="form-group"> <div class="form-group">
<div class="col-lg-9 col-lg-offset-3"> <div class="col-lg-9 col-lg-offset-3">
<button type="submit" name="fetchProducts" class="btn btn-primary"> <button type="submit" name="syncProducts" class="btn btn-primary" id="sync-button">
<i class="process-icon-download"></i> {l s='Fetch Products' mod='ecomzone'} <i class="process-icon-refresh"></i> {l s='Synchronize Products Now' mod='ecomzone'}
</button>
</div>
</div>
</form>
<div id="sync-progress" style="display: none;">
<div class="progress">
<div class="progress-bar" role="progressbar" style="width: 0%;">
0%
</div>
</div>
<p class="text-center">
<span id="sync-status">{l s='Processing...' mod='ecomzone'}</span>
</p>
</div>
</div>
</div>
<div class="panel">
<h3>{l s='Product Preview' mod='ecomzone'}</h3>
<div class="form-wrapper">
<form action="{$current_url}" method="post" class="form-horizontal">
<input type="hidden" name="token" value="{$token}" />
<div class="form-group">
<div class="col-lg-9 col-lg-offset-3">
<button type="submit" name="fetchProducts" class="btn btn-default">
<i class="process-icon-preview"></i> {l s='Preview Available Products' mod='ecomzone'}
</button> </button>
</div> </div>
</div> </div>
@@ -49,14 +79,14 @@
{if isset($API_PRODUCTS) && $API_PRODUCTS} {if isset($API_PRODUCTS) && $API_PRODUCTS}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table">
<thead> <thead>
<tr> <tr>
<th>{l s='SKU' mod='ecomzone'}</th> <th>{l s='SKU' mod='ecomzone'}</th>
<th>{l s='Name' mod='ecomzone'}</th> <th>{l s='Name' mod='ecomzone'}</th>
<th>{l s='Price' mod='ecomzone'}</th> <th>{l s='Price' mod='ecomzone'}</th>
<th>{l s='Stock' mod='ecomzone'}</th> <th>{l s='Stock' mod='ecomzone'}</th>
<th class="text-right">{l s='Actions' mod='ecomzone'}</th> <th>{l s='Status' mod='ecomzone'}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -66,16 +96,12 @@
<td>{$product.product_name|escape:'html':'UTF-8'}</td> <td>{$product.product_name|escape:'html':'UTF-8'}</td>
<td>{$product.product_price|escape:'html':'UTF-8'}</td> <td>{$product.product_price|escape:'html':'UTF-8'}</td>
<td>{$product.stock|escape:'html':'UTF-8'}</td> <td>{$product.stock|escape:'html':'UTF-8'}</td>
<td class="text-right"> <td>
<form method="post" style="display:inline;"> {if isset($product.sync_status)}
<input type="hidden" name="import_sku" <span class="badge badge-{if $product.sync_status == 'success'}success{elseif $product.sync_status == 'error'}danger{else}info{/if}">
value="{$product.sku|escape:'html':'UTF-8'}" /> {$product.sync_status|escape:'html':'UTF-8'}
<button type="submit" name="importSingleProduct" </span>
class="btn btn-xs btn-primary" {/if}
title="{l s='Import this product' mod='ecomzone'}">
<i class="icon-download"></i> {l s='Import' mod='ecomzone'}
</button>
</form>
</td> </td>
</tr> </tr>
{/foreach} {/foreach}
@@ -84,132 +110,248 @@
</div> </div>
{if isset($PAGINATION)} {if isset($PAGINATION)}
<div class="row"> <div class="text-center">
<div class="col-lg-12 text-center">
<ul class="pagination"> <ul class="pagination">
{if $PAGINATION.current_page > 1} {for $page=1 to $PAGINATION.total_pages}
<li> <li {if $page == $PAGINATION.current_page}class="active"{/if}>
<a href="{$current_url|escape:'html':'UTF-8'}&page=1" title="{l s='First page' mod='ecomzone'}"> <a href="{$current_url}&token={$token}&page={$page}">{$page}</a>
<i class="icon-double-angle-left"></i>
</a>
</li> </li>
<li> {/for}
<a href="{$current_url|escape:'html':'UTF-8'}&page={$PAGINATION.current_page-1}"
title="{l s='Previous page' mod='ecomzone'}">
<i class="icon-angle-left"></i>
</a>
</li>
{/if}
{assign var=p value=$PAGINATION.current_page-2}
{if $p < 1}
{assign var=p value=1}
{/if}
{assign var=stop value=$p+4}
{if $stop > $PAGINATION.total_pages}
{assign var=stop value=$PAGINATION.total_pages}
{assign var=p value=$stop-4}
{if $p < 1}
{assign var=p value=1}
{/if}
{/if}
{while $p <= $stop}
<li {if $p == $PAGINATION.current_page}class="active"{/if}>
<a href="{$current_url|escape:'html':'UTF-8'}&page={$p}">{$p}</a>
</li>
{assign var=p value=$p+1}
{/while}
{if $PAGINATION.current_page < $PAGINATION.total_pages}
<li>
<a href="{$current_url|escape:'html':'UTF-8'}&page={$PAGINATION.current_page+1}"
title="{l s='Next page' mod='ecomzone'}">
<i class="icon-angle-right"></i>
</a>
</li>
<li>
<a href="{$current_url|escape:'html':'UTF-8'}&page={$PAGINATION.total_pages}"
title="{l s='Last page' mod='ecomzone'}">
<i class="icon-double-angle-right"></i>
</a>
</li>
{/if}
</ul> </ul>
</div> </div>
{/if}
{/if}
</div> </div>
{/if}
{/if}
</div> </div>
</div> </div>
{if isset($SYNC_PROGRESS)}
<div class="panel"> <div class="panel">
<h3><i class="icon icon-clock-o"></i> {l s='Automatic Sync' mod='ecomzone'}</h3> <h3><i class="icon icon-refresh"></i> {l s='Sync Progress' mod='ecomzone'}</h3>
<div class="panel-body"> <div class="panel-body">
<div class="form-horizontal"> <div class="progress">
<div class="form-group"> <div class="progress-bar" role="progressbar" style="width: {$SYNC_PROGRESS.percentage}%;">
<label class="control-label col-lg-3">{l s='Cron URL' mod='ecomzone'}</label> {$SYNC_PROGRESS.percentage}%
<div class="col-lg-9">
<div class="input-group">
<input type="text" class="form-control" readonly="readonly"
value="{$shop_url}modules/ecomzone/cron.php?token={$ECOMZONE_CRON_TOKEN|escape:'html':'UTF-8'}" />
<span class="input-group-btn">
<button class="btn btn-default" type="button" onclick="selectText(this.parentElement.previousElementSibling)">
<i class="icon-copy"></i> {l s='Copy' mod='ecomzone'}
</button>
</span>
</div> </div>
<p class="help-block"> </div>
{l s='Add this URL to your server\'s crontab to run every 24 hours.' mod='ecomzone'} <p class="text-center">
{l s='Processed:' mod='ecomzone'} {$SYNC_PROGRESS.processed} / {$SYNC_PROGRESS.total}
</p> </p>
</div> </div>
</div>
</div>
</div>
</div> </div>
{/if}
{if isset($ECOMZONE_LOGS) && $ECOMZONE_LOGS} <div class="panel">
<div class="panel"> <h3><i class="icon icon-list"></i> {l s='Recent Activity Log' mod='ecomzone'}</h3>
<h3><i class="icon icon-list"></i> {l s='Recent Logs' mod='ecomzone'}</h3>
<div class="panel-body"> <div class="panel-body">
<div class="log-container" style="max-height: 400px; overflow-y: auto;"> <div class="table-responsive">
<table class="table table-striped"> <table class="table">
<tbody> <thead>
{foreach from=$ECOMZONE_LOGS item=log}
<tr> <tr>
<td style="font-family: monospace;">{$log|escape:'html':'UTF-8'}</td> <th>{l s='Time' mod='ecomzone'}</th>
<th>{l s='Level' mod='ecomzone'}</th>
<th>{l s='Message' mod='ecomzone'}</th>
<th>{l s='Details' mod='ecomzone'}</th>
</tr>
</thead>
<tbody>
{if isset($ACTIVITY_LOG) && $ACTIVITY_LOG}
{foreach from=$ACTIVITY_LOG item=log}
<tr class="{if $log.level == 'ERROR'}danger{elseif $log.level == 'WARNING'}warning{/if}">
<td>{$log.timestamp|escape:'html':'UTF-8'}</td>
<td><span class="badge badge-{if $log.level == 'ERROR'}danger{elseif $log.level == 'WARNING'}warning{else}info{/if}">{$log.level|escape:'html':'UTF-8'}</span></td>
<td>{$log.message|escape:'html':'UTF-8'}</td>
<td>
{if $log.details}
<button type="button" class="btn btn-xs btn-default" data-toggle="popover" data-content="{$log.details|escape:'html':'UTF-8'}">
<i class="icon-info-circle"></i>
</button>
{/if}
</td>
</tr> </tr>
{/foreach} {/foreach}
{else}
<tr>
<td colspan="4" class="text-center">{l s='No activity logs available' mod='ecomzone'}</td>
</tr>
{/if}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
{/if}
<script type="text/javascript">
function selectText(element) {
element.select();
try {
document.execCommand('copy');
showSuccessMessage('{l s='Copied to clipboard!' mod='ecomzone'}');
} catch (err) {
showErrorMessage('{l s='Failed to copy. Please copy manually.' mod='ecomzone'}');
}
}
</script>
<style type="text/css"> <style type="text/css">
.log-container { .progress {
background: #f8f8f8; margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 3px;
} }
.log-container table { .badge-success {
margin-bottom: 0; background-color: #72C279;
} }
.input-group .form-control { .badge-warning {
font-family: monospace; background-color: #fbbb22;
font-size: 12px; }
.badge-danger {
background-color: #E08F95;
}
.badge-info {
background-color: #25B9D7;
} }
</style> </style>
<script type="text/javascript">
$(document).ready(function(){
$('[data-toggle="popover"]').popover({
placement: 'left',
trigger: 'click'
});
});
$(document).ready(function() {
var syncInProgress = false;
var currentRequest = null;
function showErrorMessage(message) {
if (typeof $.growl !== 'undefined') {
$.growl.error({ message: message });
} else {
alert(message);
}
}
function showSuccessMessage(message) {
if (typeof $.growl !== 'undefined') {
$.growl.notice({ message: message });
}
}
function handleAjaxError(xhr, status, error) {
var errorMessage = '{l s='Connection error' mod='ecomzone'}';
if (xhr.responseText) {
try {
var response = JSON.parse(xhr.responseText);
if (response.error) {
errorMessage = response.error;
}
} catch (e) {
console.error('Failed to parse error response:', e);
}
}
showErrorMessage(errorMessage);
syncInProgress = false;
$('#sync-button').prop('disabled', false);
$('#sync-progress').hide();
}
function updateProgress(progress) {
if (!progress) return;
$('#sync-progress').show();
$('.progress-bar').css('width', progress.percentage + '%').text(progress.percentage + '%');
$('#sync-status').text('{l s='Processed:' mod='ecomzone'} ' + progress.processed + ' / ' + progress.total);
}
function continueSyncProcess() {
if (!syncInProgress) return false;
if (currentRequest) {
currentRequest.abort();
}
currentRequest = $.ajax({
url: '{$current_url|escape:'javascript':'UTF-8'}',
type: 'POST',
dataType: 'json',
data: {
ajax: 1,
action: 'continueSyncProducts',
token: '{$token|escape:'javascript':'UTF-8'}'
},
success: function(result) {
if (!result) {
showErrorMessage('{l s='Invalid server response' mod='ecomzone'}');
syncInProgress = false;
$('#sync-button').prop('disabled', false);
$('#sync-progress').hide();
return;
}
if (result.success) {
updateProgress(result.progress);
if (!result.progress.is_complete) {
setTimeout(continueSyncProcess, 1000);
} else {
syncInProgress = false;
$('#sync-button').prop('disabled', false);
showSuccessMessage('{l s='Synchronization completed successfully' mod='ecomzone'}');
window.location.reload();
}
} else {
showErrorMessage(result.error || '{l s='Sync failed' mod='ecomzone'}');
syncInProgress = false;
$('#sync-button').prop('disabled', false);
$('#sync-progress').hide();
}
},
error: handleAjaxError
});
}
$('#sync-form').on('submit', function(e) {
e.preventDefault();
if (syncInProgress) return false;
syncInProgress = true;
$('#sync-button').prop('disabled', true);
$('#sync-progress').show();
if (currentRequest) {
currentRequest.abort();
}
currentRequest = $.ajax({
url: $(this).attr('action'),
type: 'POST',
dataType: 'json',
data: $(this).serialize() + '&ajax=1&syncProducts=1',
success: function(result) {
if (!result) {
showErrorMessage('{l s='Invalid server response' mod='ecomzone'}');
syncInProgress = false;
$('#sync-button').prop('disabled', false);
$('#sync-progress').hide();
return;
}
if (result.success) {
updateProgress(result.progress);
if (!result.progress.is_complete) {
setTimeout(continueSyncProcess, 1000);
} else {
syncInProgress = false;
$('#sync-button').prop('disabled', false);
showSuccessMessage('{l s='Synchronization completed successfully' mod='ecomzone'}');
window.location.reload();
}
} else {
showErrorMessage(result.error || '{l s='Sync failed' mod='ecomzone'}');
syncInProgress = false;
$('#sync-button').prop('disabled', false);
$('#sync-progress').hide();
}
},
error: handleAjaxError
});
return false;
});
$(window).on('beforeunload', function() {
if (currentRequest) {
currentRequest.abort();
}
});
});
</script>