name = "ecomzone"; $this->tab = "shipping_logistics"; $this->version = "1.0.0"; $this->author = "EcomZone"; $this->need_instance = 0; $this->bootstrap = true; $this->ps_versions_compliancy = [ "min" => "8.0.0", "max" => "8.99.99", ]; parent::__construct(); // Register autoloader spl_autoload_register([$this, 'autoload']); $this->displayName = $this->trans( "EcomZone Dropshipping", [], "Modules.Ecomzone.Admin" ); $this->description = $this->trans( "Integration with EcomZone Dropshipping API", [], "Modules.Ecomzone.Admin" ); $this->confirmUninstall = $this->trans( "Are you sure you want to uninstall?", [], "Modules.Ecomzone.Admin" ); } /** * Autoload EcomZone classes */ public function autoload($className) { // 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' ]; // 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() { 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() { // 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"); $apiUrl = Tools::getValue("ECOMZONE_API_URL"); Configuration::updateValue("ECOMZONE_API_TOKEN", $apiToken); Configuration::updateValue("ECOMZONE_API_URL", $apiUrl); $output .= $this->displayConfirmation( $this->trans( "Settings updated", [], "Modules.Ecomzone.Admin" ) ); } // Handle manual sync request if (Tools::isSubmit("syncProducts")) { try { // If this is an AJAX request, we need to handle it differently $isAjax = Tools::getValue('ajax'); if (empty(Configuration::get("ECOMZONE_API_TOKEN"))) { throw new Exception($this->l('API token not configured. Please configure the API token first.')); } $productSync = new EcomZoneProductSync(); // Reset sync state when starting new sync if (!$isAjax) { $productSync->resetSyncState(); } $result = $productSync->importProducts(100); if ($result['is_complete']) { Configuration::updateValue("ECOMZONE_LAST_SYNC", date("Y-m-d H:i:s")); } // Store sync progress $progress = [ 'processed' => $result["imported"], 'total' => $result["total"], 'percentage' => ($result["total"] > 0) ? round(($result["imported"] / $result["total"]) * 100) : 0, 'is_complete' => $result['is_complete'], 'current_page' => $result['current_page'] ]; Configuration::updateValue("ECOMZONE_SYNC_PROGRESS", json_encode($progress)); if ($isAjax) { die(json_encode([ 'success' => true, 'progress' => $progress ])); } if ($result['is_complete']) { $output .= $this->displayConfirmation( sprintf( $this->trans( "Products synchronized successfully. Imported: %d, Total Available: %d, Errors: %d", [], "Modules.Ecomzone.Admin" ), $result["imported"], $result["total"], count($result["errors"]) ) ); if (!empty($result["errors"])) { $errorMessages = array_map(function($error) { return sprintf("SKU: %s - Error: %s", $error["sku"], $error["error"]); }, $result["errors"]); $output .= $this->displayWarning( implode("
", $errorMessages) ); } } } catch (Exception $e) { EcomZoneLogger::log("Sync failed", "ERROR", [ "error" => $e->getMessage(), "trace" => $e->getTraceAsString() ]); if ($isAjax) { die(json_encode([ 'success' => false, 'error' => $e->getMessage() ])); } $output .= $this->displayError($e->getMessage()); } } // Handle 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}", "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([ "API_PRODUCTS" => $result["data"], "PAGINATION" => [ "total_pages" => $totalPages, "current_page" => $page, "total_items" => $result["total"], ], ]); if (Tools::isSubmit("fetchProducts")) { $output .= $this->displayConfirmation( $this->trans( "Products fetched successfully", [], "Modules.Ecomzone.Admin" ) ); } } } 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_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=" . $this->name . "&tab_module=" . $this->tab . "&module_name=" . $this->name, "shop_url" => $this->context->link->getBaseLink(), ]); 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) { 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; } } public function hookActionCronJob($params) { try { $lastSync = Configuration::get("ECOMZONE_LAST_SYNC"); $syncInterval = 24 * 3600; // 24 hours if ( !empty($lastSync) && strtotime($lastSync) + $syncInterval > time() ) { return; } $page = 1; $perPage = 100; $totalImported = 0; $errors = []; do { $result = $this->makeApiRequest( "catalog?page={$page}&per_page={$perPage}", "GET" ); if (empty($result["data"])) { break; } foreach ($result["data"] as $product) { try { if ($this->importSingleProduct($product)) { $totalImported++; } } catch (Exception $e) { $errors[] = [ "sku" => $product["sku"] ?? "unknown", "error" => $e->getMessage() ]; } // Clear memory after each product gc_collect_cycles(); } $page++; } while (!empty($result["next_page_url"])); Configuration::updateValue( "ECOMZONE_LAST_SYNC", date("Y-m-d H:i:s") ); } catch (Exception $e) { EcomZoneLogger::log("Cron job execution failed", "ERROR", [ "error" => $e->getMessage(), "trace" => $e->getTraceAsString() ]); } } public function importSingleProduct(array $productData): bool { try { $importer = new EcomZoneProductImport(); return $importer->importProduct($productData); } catch (Exception $e) { EcomZoneLogger::log("Product import failed", "ERROR", [ "sku" => $productData['data']['sku'] ?? 'unknown', "error" => $e->getMessage() ]); return false; } } private function importProductImage( Product $product, string $imageUrl, bool $cover = false ): void { // Implementation of the method } private function updateBasicInfo(Product $product, array $data): void { try { // Basic information $product->name[$this->defaultLangId] = $data["product_name"]; $product->description[$this->defaultLangId] = $data["long_description"] ?? $data["description"]; $product->description_short[$this->defaultLangId] = $data["description"]; $product->price = (float) $data["product_price"]; $product->reference = $data["sku"]; $product->ean13 = $data["ean"] ?? ""; // Shop configuration $product->id_shop_default = $this->defaultShopId; $product->active = true; $product->visibility = "both"; $product->available_for_order = true; $product->show_price = true; $product->indexed = 1; // Critical: Save the product if (!$product->save()) { throw new EcomZoneException( "Failed to save product", EcomZoneException::INVALID_PRODUCT_DATA ); } // Add to default shop $product->addToShop($this->defaultShopId); } catch (Exception $e) { throw new EcomZoneException( "Failed to update product info: " . $e->getMessage(), EcomZoneException::INVALID_PRODUCT_DATA, $e ); } } 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() ]); } } }