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

No files matched your search

+542 -113
View File
@@ -20,6 +20,9 @@ class Ecomzone extends Module
parent::__construct();
// Register autoloader
spl_autoload_register([$this, 'autoload']);
$this->displayName = $this->trans(
"EcomZone Dropshipping",
[],
@@ -37,78 +40,462 @@ class Ecomzone extends Module
);
}
public function install()
/**
* Autoload EcomZone classes
*/
public function autoload($className)
{
if (!parent::install()
|| !$this->registerHook('actionCronJob')
|| !$this->registerHook('actionProductUpdate')
|| !$this->registerHook('actionProductDelete')
) {
return false;
}
// Set default configuration values
Configuration::updateValue('ECOMZONE_API_URL', 'https://dropship.ecomzone.eu/api');
Configuration::updateValue('ECOMZONE_CRON_TOKEN', Tools::encrypt(uniqid()));
Configuration::updateValue('ECOMZONE_LAST_SYNC', '');
// Create required directories
$this->createRequiredDirectories();
return true;
}
private function createRequiredDirectories()
{
$dirs = [
_PS_MODULE_DIR_ . $this->name . '/log',
_PS_MODULE_DIR_ . $this->name . '/tmp'
// Define class mapping
$classMap = [
'EcomZoneException' => 'classes/EcomZoneException.php',
'EcomZoneClient' => 'classes/EcomZoneClient.php',
'EcomZoneLogger' => 'classes/EcomZoneLogger.php',
'EcomZoneProductSync' => 'classes/EcomZoneProductSync.php',
'EcomZoneProductImport' => 'classes/EcomZoneProductImport.php',
'EcomZoneCategoryHandler' => 'classes/EcomZoneCategoryHandler.php',
'IProductSync' => 'classes/interfaces/IProductSync.php',
'ICategory' => 'classes/interfaces/ICategory.php'
];
foreach ($dirs as $dir) {
if (!file_exists($dir)) {
mkdir($dir, 0755, true);
// Check if the class exists in our map
if (isset($classMap[$className])) {
$file = _PS_MODULE_DIR_ . $this->name . '/' . $classMap[$className];
if (file_exists($file)) {
require_once($file);
}
}
}
private function setImageDirectoryPermissions(): bool
{
try {
$directories = [
_PS_IMG_DIR_,
_PS_PROD_IMG_DIR_,
_PS_MODULE_DIR_ . $this->name . '/tmp/',
_PS_MODULE_DIR_ . $this->name . '/log/'
];
EcomZoneLogger::log("Setting up directory permissions", "INFO");
foreach ($directories as $dir) {
if (!file_exists($dir)) {
EcomZoneLogger::log("Creating directory", "INFO", ['directory' => $dir]);
if (!@mkdir($dir, 0755, true)) {
$error = error_get_last();
throw new Exception("Failed to create directory {$dir}: " . ($error['message'] ?? 'Unknown error'));
}
}
if (!is_writable($dir)) {
// Get current permissions
$currentPerms = fileperms($dir) & 0777;
EcomZoneLogger::log("Directory not writable, setting permissions", "INFO", [
'directory' => $dir,
'current_perms' => decoct($currentPerms),
'target_perms' => '0755'
]);
if (!@chmod($dir, 0755)) {
$error = error_get_last();
throw new Exception("Failed to set permissions for {$dir}: " . ($error['message'] ?? 'Unknown error'));
}
}
// If this is the product image directory, ensure all subdirectories are writable
if (strpos($dir, _PS_PROD_IMG_DIR_) === 0) {
EcomZoneLogger::log("Processing product image subdirectories", "INFO", ['directory' => $dir]);
try {
if (!is_dir($dir)) {
continue; // Skip if directory doesn't exist
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $item) {
if ($item->isDir()) {
if (!is_writable($item->getPathname())) {
if (!@chmod($item->getPathname(), 0755)) {
$error = error_get_last();
EcomZoneLogger::log("Failed to set subdirectory permissions", "WARNING", [
'directory' => $item->getPathname(),
'error' => $error['message'] ?? 'Unknown error'
]);
}
}
} else {
if (!is_writable($item->getPathname())) {
if (!@chmod($item->getPathname(), 0644)) {
$error = error_get_last();
EcomZoneLogger::log("Failed to set file permissions", "WARNING", [
'file' => $item->getPathname(),
'error' => $error['message'] ?? 'Unknown error'
]);
}
}
}
}
} catch (UnexpectedValueException $e) {
// Log but don't fail if we can't read a subdirectory
EcomZoneLogger::log("Failed to process subdirectory", "WARNING", [
'directory' => $dir,
'error' => $e->getMessage()
]);
}
}
}
EcomZoneLogger::log("Directory permissions setup complete", "INFO");
return true;
} catch (Exception $e) {
EcomZoneLogger::log("Failed to set directory permissions", "ERROR", [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return false;
}
}
public function install()
{
EcomZoneLogger::log("Starting module installation", "INFO");
try {
// First check if module directory exists and is writable
if (!is_dir(_PS_MODULE_DIR_ . $this->name)) {
EcomZoneLogger::log("Creating module directory", "INFO");
if (!@mkdir(_PS_MODULE_DIR_ . $this->name, 0755, true)) {
$error = error_get_last();
throw new Exception('Failed to create module directory: ' . ($error['message'] ?? 'Unknown error'));
}
}
// Create required directories first
$dirs = [
_PS_MODULE_DIR_ . $this->name . '/log',
_PS_MODULE_DIR_ . $this->name . '/tmp',
_PS_IMG_DIR_,
_PS_PROD_IMG_DIR_
];
foreach ($dirs as $dir) {
if (!file_exists($dir)) {
EcomZoneLogger::log("Creating directory: " . $dir, "INFO");
if (!@mkdir($dir, 0755, true)) {
$error = error_get_last();
throw new Exception('Failed to create directory: ' . $dir . ' - ' . ($error['message'] ?? 'Unknown error'));
}
}
if (!is_writable($dir)) {
EcomZoneLogger::log("Setting directory writable: " . $dir, "INFO");
if (!@chmod($dir, 0755)) {
$error = error_get_last();
throw new Exception('Failed to set permissions for directory: ' . $dir . ' - ' . ($error['message'] ?? 'Unknown error'));
}
}
}
// Set up database tables
EcomZoneLogger::log("Setting up database tables", "INFO");
$this->createTables();
// Install parent module
EcomZoneLogger::log("Installing parent module", "INFO");
if (!parent::install()) {
throw new Exception('Failed to install parent module');
}
// Register hooks
EcomZoneLogger::log("Registering hooks", "INFO");
if (!$this->registerHook('actionProductUpdate')
|| !$this->registerHook('actionProductDelete')
|| !$this->registerHook('actionCronJob')) {
throw new Exception('Failed to register hooks');
}
// Set default configuration values
EcomZoneLogger::log("Setting default configuration values", "INFO");
$defaultConfig = [
'ECOMZONE_API_URL' => 'https://dropship.ecomzone.eu/api',
'ECOMZONE_API_TOKEN' => '',
'ECOMZONE_LAST_SYNC' => '',
'ECOMZONE_LOG_LEVEL' => 'INFO',
'ECOMZONE_CRON_TOKEN' => Tools::passwdGen(32) // Generate random token for cron
];
foreach ($defaultConfig as $key => $value) {
if (!Configuration::updateValue($key, $value)) {
throw new Exception('Failed to set configuration value: ' . $key);
}
}
// Set directory permissions
EcomZoneLogger::log("Setting directory permissions", "INFO");
if (!$this->setImageDirectoryPermissions()) {
throw new Exception('Failed to set directory permissions');
}
EcomZoneLogger::log("Module installation completed successfully", "INFO");
return true;
} catch (Exception $e) {
// Log the error
EcomZoneLogger::log('Module installation failed', 'ERROR', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
// Clean up any partial installation
$this->uninstall();
// Set error message that can be retrieved
$this->_errors[] = $this->l('Installation failed: ') . $e->getMessage();
return false;
}
}
private function createTables()
{
$sql = [];
// Add module logging table
$sql[] = 'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'ecomzone_sync_log` (
`id_sync_log` int(11) NOT NULL AUTO_INCREMENT,
`sync_date` datetime NOT NULL,
`status` varchar(32) NOT NULL,
`message` text,
`details` text,
PRIMARY KEY (`id_sync_log`)
) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8;';
// Add product sync table to track imported products
$sql[] = 'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'ecomzone_product` (
`id_product` int(11) NOT NULL,
`reference` varchar(64) NOT NULL,
`last_sync` datetime NOT NULL,
`sync_status` varchar(32) NOT NULL,
PRIMARY KEY (`id_product`),
UNIQUE KEY `reference` (`reference`)
) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8;';
foreach ($sql as $query) {
if (!Db::getInstance()->execute($query)) {
throw new Exception('Failed to create required database table: ' . Db::getInstance()->getMsgError());
}
}
}
public function uninstall()
{
return parent::uninstall() &&
Configuration::deleteByName("ECOMZONE_API_URL") &&
Configuration::deleteByName("ECOMZONE_API_TOKEN") &&
Configuration::deleteByName("ECOMZONE_CRON_TOKEN") &&
Configuration::deleteByName("ECOMZONE_LAST_SYNC");
EcomZoneLogger::log("Starting module uninstallation", "INFO");
try {
// Remove configuration values
$configKeys = [
'ECOMZONE_API_URL',
'ECOMZONE_API_TOKEN',
'ECOMZONE_LAST_SYNC',
'ECOMZONE_SYNC_STATE',
'ECOMZONE_SYNC_PROGRESS',
'ECOMZONE_LOG_LEVEL',
'ECOMZONE_CRON_TOKEN'
];
foreach ($configKeys as $key) {
Configuration::deleteByName($key);
}
// Uninstall parent module
if (!parent::uninstall()) {
throw new Exception('Failed to uninstall parent module');
}
EcomZoneLogger::log("Module uninstallation completed successfully", "INFO");
return true;
} catch (Exception $e) {
EcomZoneLogger::log('Module uninstallation failed', 'ERROR', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
$this->_errors[] = $this->l('Uninstallation failed: ') . $e->getMessage();
return false;
}
}
public function getContent()
{
$output = "";
// Ensure no output before AJAX responses
if (Tools::getValue('ajax')) {
ob_clean();
header('Content-Type: application/json');
}
$output = "";
// Get admin token
$token = Tools::getAdminTokenLite('AdminModules');
if (Tools::isSubmit("submitEcomZoneModule")) {
$apiToken = Tools::getValue("ECOMZONE_API_TOKEN");
if (Configuration::updateValue("ECOMZONE_API_TOKEN", $apiToken)) {
$output .= $this->displayConfirmation(
$this->trans(
"Settings updated",
[],
"Modules.Ecomzone.Admin"
)
);
$apiUrl = Tools::getValue("ECOMZONE_API_URL");
Configuration::updateValue("ECOMZONE_API_TOKEN", $apiToken);
Configuration::updateValue("ECOMZONE_API_URL", $apiUrl);
$output .= $this->displayConfirmation(
$this->trans(
"Settings updated",
[],
"Modules.Ecomzone.Admin"
)
);
}
// Handle manual sync request
if (Tools::isSubmit("syncProducts")) {
try {
// If this is an AJAX request, we need to handle it differently
$isAjax = Tools::getValue('ajax');
if (empty(Configuration::get("ECOMZONE_API_TOKEN"))) {
throw new Exception($this->l('API token not configured. Please configure the API token first.'));
}
$productSync = new EcomZoneProductSync();
// Reset sync state when starting new sync
if (!$isAjax) {
$productSync->resetSyncState();
}
$result = $productSync->importProducts(100);
if ($result['is_complete']) {
Configuration::updateValue("ECOMZONE_LAST_SYNC", date("Y-m-d H:i:s"));
}
// Store sync progress
$progress = [
'processed' => $result["imported"],
'total' => $result["total"],
'percentage' => ($result["total"] > 0) ? round(($result["imported"] / $result["total"]) * 100) : 0,
'is_complete' => $result['is_complete'],
'current_page' => $result['current_page']
];
Configuration::updateValue("ECOMZONE_SYNC_PROGRESS", json_encode($progress));
if ($isAjax) {
die(json_encode([
'success' => true,
'progress' => $progress
]));
}
if ($result['is_complete']) {
$output .= $this->displayConfirmation(
sprintf(
$this->trans(
"Products synchronized successfully. Imported: %d, Total Available: %d, Errors: %d",
[],
"Modules.Ecomzone.Admin"
),
$result["imported"],
$result["total"],
count($result["errors"])
)
);
if (!empty($result["errors"])) {
$errorMessages = array_map(function($error) {
return sprintf("SKU: %s - Error: %s", $error["sku"], $error["error"]);
}, $result["errors"]);
$output .= $this->displayWarning(
implode("<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 product fetch request
// Handle continue sync request (AJAX)
if (Tools::getValue('action') === 'continueSyncProducts' && Tools::getValue('ajax')) {
try {
if (empty(Configuration::get("ECOMZONE_API_TOKEN"))) {
throw new Exception($this->l('API token not configured. Please configure the API token first.'));
}
$productSync = new EcomZoneProductSync();
$result = $productSync->importProducts(100);
if ($result['is_complete']) {
Configuration::updateValue("ECOMZONE_LAST_SYNC", date("Y-m-d H:i:s"));
}
// Store sync progress
$progress = [
'processed' => $result["imported"],
'total' => $result["total"],
'percentage' => ($result["total"] > 0) ? round(($result["imported"] / $result["total"]) * 100) : 0,
'is_complete' => $result['is_complete'],
'current_page' => $result['current_page']
];
Configuration::updateValue("ECOMZONE_SYNC_PROGRESS", json_encode($progress));
die(json_encode([
'success' => true,
'progress' => $progress
]));
} catch (Exception $e) {
EcomZoneLogger::log("Sync continuation failed", "ERROR", [
"error" => $e->getMessage()
]);
die(json_encode([
'success' => false,
'error' => $e->getMessage()
]));
}
}
// Handle product fetch request for preview
if (Tools::isSubmit("fetchProducts") || Tools::getValue("page")) {
try {
$page = (int) Tools::getValue("page", 1);
$perPage = 10;
$result = $this->makeApiRequest(
"catalog?page={$page}&per_page={$perPage}"
"catalog?page={$page}&per_page={$perPage}",
"GET"
);
if (!empty($result["data"])) {
// Add sync status to products
foreach ($result["data"] as &$product) {
$productId = Product::getIdByReference($product["sku"]);
$product["sync_status"] = $productId ? "success" : "pending";
}
$totalPages = ceil($result["total"] / $perPage);
$this->context->smarty->assign([
@@ -131,15 +518,27 @@ class Ecomzone extends Module
}
}
} catch (Exception $e) {
EcomZoneLogger::log("Product fetch failed", "ERROR", [
"error" => $e->getMessage()
]);
$output .= $this->displayError($e->getMessage());
}
}
// Get sync progress
$syncProgress = json_decode(Configuration::get("ECOMZONE_SYNC_PROGRESS"), true);
// Get recent activity logs
$activityLog = $this->getRecentActivityLogs();
$this->context->smarty->assign([
"module_dir" => $this->_path,
"ECOMZONE_API_TOKEN" => Configuration::get("ECOMZONE_API_TOKEN"),
"ECOMZONE_CRON_TOKEN" => Configuration::get("ECOMZONE_CRON_TOKEN"),
"ECOMZONE_API_URL" => Configuration::get("ECOMZONE_API_URL"),
"ECOMZONE_LAST_SYNC" => Configuration::get("ECOMZONE_LAST_SYNC"),
"SYNC_PROGRESS" => $syncProgress,
"ACTIVITY_LOG" => $activityLog,
"token" => $token,
"current_url" =>
$this->context->link->getAdminLink("AdminModules", false) .
"&configure=" .
@@ -151,78 +550,77 @@ class Ecomzone extends Module
"shop_url" => $this->context->link->getBaseLink(),
]);
return $output .
$this->display(__FILE__, "views/templates/admin/configure.tpl");
return $output . $this->display(__FILE__, "views/templates/admin/configure.tpl");
}
private function getRecentActivityLogs($limit = 50)
{
$logFile = _PS_MODULE_DIR_ . $this->name . '/log/ecomzone.log';
$logs = [];
if (file_exists($logFile)) {
$lines = array_reverse(file($logFile));
$count = 0;
foreach ($lines as $line) {
if ($count >= $limit) break;
$logEntry = json_decode($line, true);
if ($logEntry) {
$logs[] = [
'timestamp' => $logEntry['timestamp'] ?? date('Y-m-d H:i:s'),
'level' => $logEntry['level'] ?? 'INFO',
'message' => $logEntry['message'] ?? '',
'details' => json_encode($logEntry['context'] ?? [])
];
$count++;
}
}
}
return $logs;
}
private function makeApiRequest($endpoint, $method = "GET", $data = null)
{
$apiUrl = Configuration::get("ECOMZONE_API_URL");
$apiToken = Configuration::get("ECOMZONE_API_TOKEN");
if (empty($apiToken)) {
throw new Exception(
$this->trans(
"API token not configured",
[],
"Modules.Ecomzone.Admin"
)
);
try {
// Use the EcomZoneClient for all API requests
$client = new EcomZoneClient();
// Add endpoint to URL
$params = [];
// Extract query params if present in endpoint
if (strpos($endpoint, '?') !== false) {
list($endpoint, $queryString) = explode('?', $endpoint, 2);
parse_str($queryString, $params);
}
// Ensure method is a string and not an array (prevent 405 Method Not Allowed)
if (!is_string($method)) {
EcomZoneLogger::log("Invalid method type detected", "WARNING", [
'endpoint' => $endpoint,
'method_type' => gettype($method),
'method' => $method
]);
$method = "GET"; // Default to GET if method is not a string
}
// Make the request using our client
if ($method === 'GET') {
return $client->makeRequest($endpoint, $method, $params);
} else {
return $client->makeRequest($endpoint, $method, [], $data);
}
} catch (Exception $e) {
EcomZoneLogger::log("API Request Failed in module", "ERROR", [
'endpoint' => $endpoint,
'method' => $method,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
throw $e;
}
$curl = curl_init();
$url = rtrim($apiUrl, "/") . "/" . ltrim($endpoint, "/");
$options = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . $apiToken,
"Accept: application/json",
"Content-Type: application/json",
],
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
];
if ($data !== null) {
$options[CURLOPT_POSTFIELDS] = json_encode($data);
}
curl_setopt_array($curl, $options);
$response = curl_exec($curl);
$err = curl_error($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
if ($err) {
throw new Exception("cURL Error: " . $err);
}
if ($httpCode >= 400) {
$errorData = json_decode($response, true);
$errorMessage = isset($errorData['error'])
? $errorData['error']
: "HTTP Error: " . $httpCode;
throw new EcomZoneException(
$errorMessage,
EcomZoneException::API_ERROR
);
}
$decodedResponse = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception("Invalid JSON response");
}
return $decodedResponse;
}
public function hookActionCronJob($params)
@@ -245,7 +643,8 @@ class Ecomzone extends Module
do {
$result = $this->makeApiRequest(
"catalog?page={$page}&per_page={$perPage}"
"catalog?page={$page}&per_page={$perPage}",
"GET"
);
if (empty($result["data"])) {
@@ -341,5 +740,35 @@ class Ecomzone extends Module
);
}
}
public function hookActionProductUpdate($params)
{
try {
$product = $params['product'];
EcomZoneLogger::log("Product updated", "INFO", [
"product_id" => $product->id,
"reference" => $product->reference
]);
} catch (Exception $e) {
EcomZoneLogger::log("Failed to handle product update", "ERROR", [
"error" => $e->getMessage()
]);
}
}
public function hookActionProductDelete($params)
{
try {
$product = $params['product'];
EcomZoneLogger::log("Product deleted", "INFO", [
"product_id" => $product->id,
"reference" => $product->reference
]);
} catch (Exception $e) {
EcomZoneLogger::log("Failed to handle product deletion", "ERROR", [
"error" => $e->getMessage()
]);
}
}
}