name = 'addlivephoto'; $this->tab = 'front_office_features'; $this->version = '1.0.0'; $this->author = 'Panariga'; $this->need_instance = 0; $this->bootstrap = true; parent::__construct(); $this->displayName = $this->trans('Add Live Product Photos',[], 'Modules.Addlivephoto.Admin'); $this->description = $this->trans('Allows admin to add live photos of product details like expiry dates directly from their phone. Displays fresh images on the product page.',[], 'Modules.Addlivephoto.Admin'); $this->ps_versions_compliancy = array('min' => '8.0.0', 'max' => _PS_VERSION_); } /** * Module installation process. * @return bool */ public function install() { if ( !parent::install() || !$this->registerHook('displayProductPriceBlock') || !$this->registerHook('actionAdminControllerSetMedia') || !$this->installDb() || !$this->installAdminTab() || !$this->createImageDirectories() ) { return false; } return true; } /** * Module uninstallation process. * @return bool */ public function uninstall() { // Note: For safety, we are not deleting the /var/modules/addlivephoto directory // with user-uploaded images by default. You can add a configuration option for this. return parent::uninstall() && $this->uninstallDb() && $this->uninstallAdminTab(); } protected function installDb() { // Added image_type column $sql = 'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . self::TABLE_NAME . '` ( `id_add_live_photo` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, `id_product` INT(11) UNSIGNED NOT NULL, `image_name` VARCHAR(255) NOT NULL, `image_type` ENUM("expiry", "packaging") NOT NULL DEFAULT "expiry", `date_add` DATETIME NOT NULL, PRIMARY KEY (`id_add_live_photo`), INDEX `id_product_idx` (`id_product`) ) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8;'; return Db::getInstance()->execute($sql); } /** * Drop the database table. * @return bool */ protected function uninstallDb() { // return Db::getInstance()->execute('DROP TABLE IF EXISTS `' . _DB_PREFIX_ . self::TABLE_NAME . '`'); } /** * Install the link to our Admin Controller in the Quick Access menu. * @return bool */ protected function installAdminTab() { $tab = new Tab(); $tab->active = 1; $tab->class_name = 'AdminAddLivePhoto'; $tab->name = array(); foreach (Language::getLanguages(true) as $lang) { $tab->name[$lang['id_lang']] = $this->trans('Live Photo Uploader',[], 'Modules.Addlivephoto.Admin'); } $tab->id_parent = (int) Tab::getIdFromClassName('IMPROVE'); $tab->module = $this->name; return $tab->add(); } /** * Remove the Admin Controller link. * @return bool */ protected function uninstallAdminTab() { $id_tab = (int) Tab::getIdFromClassName('AdminAddLivePhoto'); if ($id_tab) { $tab = new Tab($id_tab); return $tab->delete(); } return true; } /** * Create directories for storing images. * @return bool */ protected function createImageDirectories() { if (!is_dir(self::IMG_DIR_VAR_PATH)) { // Create directory recursively with write permissions if (!mkdir(self::IMG_DIR_VAR_PATH, 0775, true)) { $this->_errors[] = $this->trans('Could not create image directory: ',[], 'Modules.Addlivephoto.Admin') . self::IMG_DIR_VAR_PATH; return false; } } // Add an index.php file for security if (!file_exists(self::IMG_DIR_VAR_PATH . 'index.php')) { @copy(_PS_MODULE_DIR_.$this->name.'/views/index.php', self::IMG_DIR_VAR_PATH . 'index.php'); } return true; } /** * Hook to display content on the product page. * @param array $params * @return string|void */ public function hookDisplayProductPriceBlock($params) { if (!isset($params['type']) || $params['type'] !== 'after_price') { return; } $id_product = (int) Tools::getValue('id_product'); if (!$id_product) return; // Complex Logic: // 1. Get 'packaging' photos (Always show, limit to newest 3) // 2. Get 'expiry' photos (Show only if newer than 3 months) $sql = "SELECT * FROM `" . _DB_PREFIX_ . self::TABLE_NAME . "` WHERE `id_product` = " . $id_product . " AND ( (`image_type` = 'packaging') OR (`image_type` = 'expiry' AND `date_add` >= DATE_SUB(NOW(), INTERVAL 3 MONTH)) ) ORDER BY `date_add` DESC"; $results = Db::getInstance()->executeS($sql); if (!$results) return; $live_photos = []; foreach ($results as $row) { $image_uri = $this->getProductImageUri($id_product, $row['image_name']); if ($image_uri) { // Customize text based on type $is_expiry = ($row['image_type'] === 'expiry'); $date_taken = date('Y-m-d', strtotime($row['date_add'])); $alt_text = $is_expiry ? sprintf($this->trans('Expiry date photo for %s, taken on %s',[], 'Modules.Addlivephoto.Shop'), $this->context->smarty->tpl_vars['product']->value['name'], $date_taken) : sprintf($this->trans('Real packaging photo for %s',[], 'Modules.Addlivephoto.Shop'), $this->context->smarty->tpl_vars['product']->value['name']); $live_photos[] = [ 'url' => $image_uri, 'type' => $row['image_type'], // 'expiry' or 'packaging' 'date' => $row['date_add'], 'alt' => $alt_text, ]; } } if (empty($live_photos)) return; $this->context->smarty->assign([ 'live_photos' => $live_photos, ]); return $this->display(__FILE__, 'views/templates/hook/displayProductPriceBlock.tpl'); } /** * Hook to add CSS/JS to the admin controller page. */ public function hookActionAdminControllerSetMedia() { // We only want to load these assets on our specific controller page if (Tools::getValue('controller') == 'AdminAddLivePhoto') { // $this->context->controller->addJS($this->_path . 'views/js/admin.js'); // $this->context->controller->addCSS($this->_path . 'views/css/admin.css'); } } /** * Gets the full server path to a product's image directory, creating it if necessary. * Follows the PrestaShop pattern (e.g., /1/2/3/ for ID 123). * * @param int $id_product * @return string|false The path to the directory or false on failure. */ public function getProductImageServerPath($id_product) { if (!is_numeric($id_product)) { return false; } $path = self::IMG_DIR_VAR_PATH . implode('/', str_split((string)$id_product)) . '/'; if (!is_dir($path)) { if (!mkdir($path, 0775, true)) { return false; } // Add an index.php file for security if (!file_exists($path . 'index.php')) { @copy(_PS_MODULE_DIR_.$this->name.'/views/index.php', $path . 'index.php'); } } return $path; } /** * Gets the public URI for a specific product image. * * @param int $id_product * @param string $image_name * @return string|false The public URI or false if file does not exist. */ public function getProductImageUri($id_product, $image_name) { if (!is_numeric($id_product) || empty($image_name)) { return false; } $path_parts = str_split((string)$id_product); $image_path = implode('/', $path_parts) . '/' . $image_name; $server_path_check = self::IMG_DIR_VAR_PATH . $image_path; // We check if the file actually exists before returning a URI if (!file_exists($server_path_check)) { return false; } return $this->context->link->getBaseLink() . 'modules/addlivephoto/photo/' . $image_path; } /** * Deletes a live photo record and its corresponding file. * @param int $id_product * @param string $image_name * @return bool */ public function deleteProductImage($id_product, $image_name) { if (!is_numeric($id_product) || empty($image_name)) { return false; } // Delete from database $deleted_from_db = Db::getInstance()->delete( self::TABLE_NAME, '`id_product` = ' . (int)$id_product . ' AND `image_name` = \'' . pSQL($image_name) . '\'' ); // Delete file from server $file_path = $this->getProductImageServerPath($id_product) . $image_name; $deleted_from_disk = false; if (file_exists($file_path) && is_writable($file_path)) { $deleted_from_disk = unlink($file_path); } // Return true if both operations were successful or if the file was already gone but DB entry was removed return $deleted_from_db && ($deleted_from_disk || !file_exists($file_path)); } public function isUsingNewTranslationSystem() { return true; } }