diff --git a/classes/Module.php b/classes/Module.php index 05ba887a7..369bd66ab 100644 --- a/classes/Module.php +++ b/classes/Module.php @@ -33,6 +33,12 @@ abstract class ModuleCore /** @var float Version */ public $version; + /** @var array filled with known compliant PS versions */ + public $ps_versions_compliancy = array('min' => '1.4', 'max' => '1.6'); + + /** @var array filled with modules needed for install */ + public $dependencies = array(); + /** @var string Unique name */ public $name; @@ -54,50 +60,49 @@ abstract class ModuleCore /** @var boolean Status */ public $active = false; + /** @var string Fill it if the module is installed but not yet set up */ + public $warning; + + /** @var array to store the limited country */ + public $limited_countries = array(); + + /** @var array used by AdminTab to determine which lang file to use (admin.php or module lang file) */ + public static $classInModule = array(); + /** @var array current language translations */ protected $_lang = array(); /** @var string Module web path (eg. '/shop/modules/modulename/') */ protected $_path = NULL; - /** @var string Fill it if the module is installed but not yet set up */ - public $warning; - - /** @var string Message display before uninstall a module */ - public $beforeUninstall = NULL; - + /** @var protected array filled with module errors */ protected $_errors = false; + /** @var protected string main table used for modules installed */ protected $table = 'module'; + /** @var protected string identifier of the main table */ protected $identifier = 'id_module'; - public static $_db; - - /** @var array to store the limited country */ - public $limited_countries = array(); - - protected static $modulesCache; + /** @var protected array cache filled with modules informations */ + protected static $modules_cache; + /** @var protected array cache filled with modules instances */ protected static $_INSTANCE = array(); - protected static $_generateConfigXmlMode = false; + /** @var protected boolean filled with config xml generation mode */ + protected static $_generate_config_xml_mode = false; + /** @var protected array filled with cache translations */ protected static $l_cache = array(); + /** @var protected array filled with cache permissions (modules / employee profiles) */ protected static $cache_permissions = array(); - /** - * @var array used by AdminTab to determine which lang file to use (admin.php or module lang file) - */ - public static $classInModule = array(); - /** @var Context */ protected $context; - /** - * @var Smarty_Data - */ + /** @var Smarty_Data */ protected $smarty; /** @@ -108,36 +113,41 @@ abstract class ModuleCore */ public function __construct($name = null, Context $context = null) { + // Load context and smarty $this->context = $context ? $context : Context::getContext(); $this->smarty = $this->context->smarty->createData($this->context->smarty); + // If the module has no name we gave him its id as name if ($this->name == NULL) $this->name = $this->id; + + // If the module has the name we load the corresponding data from the cache if ($this->name != NULL) { - if (self::$modulesCache == NULL AND !is_array(self::$modulesCache)) + // If cache is not generated, we generate it + if (self::$modules_cache == NULL AND !is_array(self::$modules_cache)) { - $list = $this->context->shop->getListOfID(); - // Join clause is done to check if the module is activated in current shop context - $sql = 'SELECT m.id_module, m.name, ( - SELECT COUNT(*) FROM '._DB_PREFIX_.'module_shop ms WHERE m.id_module = ms.id_module AND ms.id_shop IN ('.implode(',', $list).') - ) as total - FROM '._DB_PREFIX_.'module m'; - self::$modulesCache = array(); + $list = $this->context->shop->getListOfID(); + $sqlLimitShop = 'SELECT COUNT(*) FROM `'._DB_PREFIX_.'module_shop` ms WHERE m.`id_module` = ms.`id_module` AND ms.`id_shop` IN ('.implode(',', $list).')'; + $sql = 'SELECT m.`id_module`, m.`name`, ('.$sqlLimitShop.') as total FROM `'._DB_PREFIX_.'module` m'; + + // Result is cached + self::$modules_cache = array(); $result = Db::getInstance()->executeS($sql); foreach ($result as $row) { - self::$modulesCache[$row['name']] = $row; - self::$modulesCache[$row['name']]['active'] = ($row['total'] == count($list)) ? true : false; + self::$modules_cache[$row['name']] = $row; + self::$modules_cache[$row['name']]['active'] = ($row['total'] == count($list)) ? true : false; } } - if (isset(self::$modulesCache[$this->name])) + // We load configuration from the cache + if (isset(self::$modules_cache[$this->name])) { - $this->active = self::$modulesCache[$this->name]['active']; - $this->id = self::$modulesCache[$this->name]['id_module']; - foreach (self::$modulesCache[$this->name] AS $key => $value) + $this->active = self::$modules_cache[$this->name]['active']; + $this->id = self::$modules_cache[$this->name]['id_module']; + foreach (self::$modules_cache[$this->name] AS $key => $value) if (key_exists($key, $this)) $this->{$key} = $value; $this->_path = __PS_BASE_URI__.'modules/'.$this->name.'/'; @@ -145,6 +155,9 @@ abstract class ModuleCore } } + /** + * Get SQL modules restriction by shop + */ protected function sqlShopRestriction($share = false, $alias = null) { return $this->context->shop->addSqlRestriction($share, $alias, 'shop'); @@ -155,20 +168,47 @@ abstract class ModuleCore */ public function install() { + // Check module name validation if (!Validate::isModuleName($this->name)) die(Tools::displayError()); - $result = Db::getInstance()->getRow(' - SELECT `id_module` - FROM `'._DB_PREFIX_.'module` - WHERE `name` = \''.pSQL($this->name).'\''); - if ($result) - return false; - $result = Db::getInstance()->AutoExecute(_DB_PREFIX_.$this->table, array('name' => $this->name, 'active' => 1), 'INSERT'); - if (!$result) + // Check PS version compliancy + if (version_compare(_PS_VERSION_, $this->ps_versions_compliancy['min']) < 0 || version_compare(_PS_VERSION_, $this->ps_versions_compliancy['max']) > 0) + { + $this->_errors[] = $this->l('The version of your module is not compliant with your PrestaShop version.'); return false; + } + + // Check module dependencies + if (count($this->dependencies) > 0) + foreach ($this->dependencies as $dependency) + if (!Db::getInstance()->getRow('SELECT `id_module` FROM `'._DB_PREFIX_.'module` WHERE `name` = \''.pSQL($dependency).'\'')) + { + $error = $this->l('Before installing this module, you have to installed these/this module(s) first :').'
'; + foreach ($this->dependencies as $d) + $error .= '- '.$d.'
'; + $this->_errors[] = $error; + return false; + } + + // Check if module is installed + $result = Db::getInstance()->getRow('SELECT `id_module` FROM `'._DB_PREFIX_.'module` WHERE `name` = \''.pSQL($this->name).'\''); + if ($result) + { + $this->_errors[] = $this->l('This module has already been installed.'); + return false; + } + + // Install module and retrieve the installation id + $result = Db::getInstance()->autoExecute(_DB_PREFIX_.$this->table, array('name' => $this->name, 'active' => 1), 'INSERT'); + if (!$result) + { + $this->_errors[] = $this->l('Technical error : PrestaShop could not installed this module.'); + return false; + } $this->id = Db::getInstance()->Insert_ID(); + // Enable the module for all shops $this->enable(true); // Permissions management @@ -186,6 +226,7 @@ abstract class ModuleCore WHERE id_tab = (SELECT `id_tab` FROM '._DB_PREFIX_.'tab WHERE class_name = \'AdminModules\' LIMIT 1) AND a.`view` = 0 )'); + // Adding Restrictions for client groups Group::addRestrictionsForModule($this->id, Shop::getShops(true, null, true)); @@ -199,31 +240,31 @@ abstract class ModuleCore */ public function uninstall() { + // Check module installation id validation if (!Validate::isUnsignedId($this->id)) return false; - $sql = 'SELECT id_hook - FROM '._DB_PREFIX_.'hook_module hm - WHERE id_module = '.(int)$this->id; + // Retrieve hooks used by the module + $sql = 'SELECT `id_hook` FROM `'._DB_PREFIX_.'hook_module` WHERE `id_module` = '.(int)$this->id; $result = Db::getInstance()->executeS($sql); foreach ($result AS $row) { - $sql = 'DELETE FROM `'._DB_PREFIX_.'hook_module` - WHERE `id_module` = '.(int)$this->id.' - AND `id_hook` = '.(int)$row['id_hook']; + $sql = 'DELETE FROM `'._DB_PREFIX_.'hook_module` WHERE `id_module` = '.(int)$this->id.' AND `id_hook` = '.(int)$row['id_hook']; Db::getInstance()->execute($sql); $this->cleanPositions($row['id_hook']); } + + // Disable the module for all shops $this->disable(true); + // Delete permissions module access Db::getInstance()->execute('DELETE FROM `'._DB_PREFIX_.'module_access` WHERE `id_module` = '.(int)$this->id); // Remove restrictions for client groups Group::truncateRestrictionsByModule($this->id); - return Db::getInstance()->execute(' - DELETE FROM `'._DB_PREFIX_.'module` - WHERE `id_module` = '.(int)$this->id); + // Uninstall the module + return Db::getInstance()->execute('DELETE FROM `'._DB_PREFIX_.'module` WHERE `id_module` = '.(int)$this->id); } /** @@ -236,9 +277,11 @@ abstract class ModuleCore */ public static function enableByName($name) { + // If $name is not an array, we set it as an array if (!is_array($name)) $name = array($name); + // Enable each module foreach ($name as $k => $v) Module::getInstanceByName($name)->enable(); } @@ -250,16 +293,17 @@ abstract class ModuleCore */ public function enable($forceAll = false) { + // Retrieve all shops where the module is enabled $list = $this->context->shop->getListOfID(); - $sql = 'SELECT id_shop - FROM '._DB_PREFIX_.'module_shop - WHERE id_module = '.$this->id.' - '.((!$forceAll) ? 'AND id_shop IN('.implode(', ', $list).')' : ''); + $sql = 'SELECT `id_shop` FROM `'._DB_PREFIX_.'module_shop` WHERE `id_module` = '.$this->id.' '.((!$forceAll) ? 'AND `id_shop` IN('.implode(', ', $list).')' : ''); + + // Store the results in an array $items = array(); if ($results = Db::getInstance($sql)->executeS($sql)) foreach ($results as $row) $items[] = $row['id_shop']; + // Enable module in the shop where it is not enabled yet foreach ($list as $id) if (!in_array($id, $items)) Db::getInstance()->autoExecute(_DB_PREFIX_.'module_shop', array( @@ -280,9 +324,11 @@ abstract class ModuleCore */ public static function disableByName($name) { + // If $name is not an array, we set it as an array if (!is_array($name)) $name = array($name); + // Disable each module foreach ($name as $k => $v) Module::getInstanceByName($name)->disable(); @@ -296,9 +342,8 @@ abstract class ModuleCore */ public function disable($forceAll = false) { - $sql = 'DELETE FROM '._DB_PREFIX_.'module_shop - WHERE id_module = '.$this->id.' - '.((!$forceAll) ? ' AND id_shop IN('.implode(', ', $this->context->shop->getListOfID()).')' : ''); + // Disable module for all shops + $sql = 'DELETE FROM `'._DB_PREFIX_.'module_shop` WHERE `id_module` = '.$this->id.' '.((!$forceAll) ? ' AND `id_shop` IN('.implode(', ', $this->context->shop->getListOfID()).')' : ''); Db::getInstance()->execute($sql); } @@ -343,6 +388,7 @@ abstract class ModuleCore */ public function registerHook($hook_name, $shopList = null) { + // Check hook name validation and if module is installed if (!Validate::isHookName($hook_name)) die(Tools::displayError()); if (!isset($this->id) OR !is_numeric($this->id)) @@ -354,10 +400,10 @@ abstract class ModuleCore $hook_name = Hook::$_hook_alias[$hook_name]; // Get hook id - $sql = 'SELECT `id_hook` - FROM `'._DB_PREFIX_.'hook` - WHERE `name` = \''.pSQL($hook_name).'\''; + $sql = 'SELECT `id_hook` FROM `'._DB_PREFIX_.'hook` WHERE `name` = \''.pSQL($hook_name).'\''; $hookID = Db::getInstance()->getValue($sql); + + // If hook does not exist, we create it if (!$hookID) { $newHook = new Hook(); @@ -369,6 +415,7 @@ abstract class ModuleCore return false; } + // If shop lists is null, we fill it with all shops if (is_null($shopList)) $shopList = Shop::getShops(true, null, true); @@ -377,34 +424,33 @@ abstract class ModuleCore { // Check if already register $sql = 'SELECT hm.`id_module` - FROM `'._DB_PREFIX_.'hook_module` hm, `'._DB_PREFIX_.'hook` h - WHERE hm.`id_module` = '.(int)($this->id).' - AND h.id_hook = '.$hookID.' - AND h.`id_hook` = hm.`id_hook` - AND id_shop = '.$shopID; + FROM `'._DB_PREFIX_.'hook_module` hm, `'._DB_PREFIX_.'hook` h + WHERE hm.`id_module` = '.(int)($this->id).' AND h.`id_hook` = '.$hookID.' + AND h.`id_hook` = hm.`id_hook` AND `id_shop` = '.(int)($shopID); if (Db::getInstance()->getRow($sql)) continue; // Get module position in hook $sql = 'SELECT MAX(`position`) AS position - FROM `'._DB_PREFIX_.'hook_module` - WHERE `id_hook` = '.$hookID - .' AND id_shop = '.$shopID; + FROM `'._DB_PREFIX_.'hook_module` + WHERE `id_hook` = '.(int)$hookID.' AND `id_shop` = '.(int)$shopID; if (!$position = Db::getInstance()->getValue($sql)) $position = 0; // Register module in hook $result = Db::getInstance()->autoExecute(_DB_PREFIX_.'hook_module', array( - 'id_module' => $this->id, - 'id_hook' => $hookID, - 'id_shop' => $shopID, - 'position' => $position + 1, + 'id_module' => (int)$this->id, + 'id_hook' => (int)$hookID, + 'id_shop' => (int)$shopID, + 'position' => (int)($position + 1), ), 'INSERT'); if (!$result) $return &= false; } + // Clean modules position $this->cleanPositions($hookID, $shopList); + return $return; } @@ -425,20 +471,24 @@ abstract class ModuleCore if (isset(Hook::$_hook_alias[$hook_id])) $hook_id = Hook::$_hook_alias[$hook_id]; + // Unregister module on hook by name $sql = 'SELECT `id_hook` - FROM `'._DB_PREFIX_.'hook` - WHERE `name` = \''.pSQL($hook_id).'\''; + FROM `'._DB_PREFIX_.'hook` + WHERE `name` = \''.pSQL($hook_id).'\''; $hook_id = Db::getInstance()->getValue($sql); if (!$hook_id) return false; } + // Unregister module on hook by id $sql = 'DELETE FROM `'._DB_PREFIX_.'hook_module` - WHERE `id_module` = '.(int)$this->id.' - AND `id_hook` = '.(int)$hook_id - .(($shopList) ? ' AND id_shop IN('.implode(', ', $shopList).')' : ''); + WHERE `id_module` = '.(int)$this->id.' AND `id_hook` = '.(int)$hook_id + .(($shopList) ? ' AND `id_shop` IN('.implode(', ', $shopList).')' : ''); $result = Db::getInstance()->execute($sql); + + // Clean modules position $this->cleanPositions($hook_id, $shopList); + return $result; } @@ -451,11 +501,9 @@ abstract class ModuleCore */ public function unregisterExceptions($hook_id, $shopList = null) { - $sql = 'DELETE - FROM `'._DB_PREFIX_.'hook_module_exceptions` - WHERE `id_module` = '.(int)$this->id.' - AND `id_hook` = '.(int)$hook_id - .(($shopList) ? ' AND id_shop IN('.implode(', ', $shopList).')' : ''); + $sql = 'DELETE FROM `'._DB_PREFIX_.'hook_module_exceptions` + WHERE `id_module` = '.(int)$this->id.' AND `id_hook` = '.(int)$hook_id + .(($shopList) ? ' AND `id_shop` IN('.implode(', ', $shopList).')' : ''); return Db::getInstance()->execute($sql); } @@ -469,9 +517,11 @@ abstract class ModuleCore */ public function registerExceptions($id_hook, $excepts, $shopList = null) { + // If shop lists is null, we fill it with all shops if (is_null($shopList)) Context::getContext()->shop->getListOfID(); + // Save modules exception for each shop foreach ($shopList as $shopID) { foreach ($excepts AS $except) @@ -480,10 +530,10 @@ abstract class ModuleCore continue; $result = Db::getInstance()->autoExecute(_DB_PREFIX_.'hook_module_exceptions', array( - 'id_module' => $this->id, - 'id_hook' => (int)$id_hook, - 'id_shop' => $shopID, - 'file_name' => pSQL($except), + 'id_module' => (int)$this->id, + 'id_hook' => (int)$id_hook, + 'id_shop' => (int)$shopID, + 'file_name' => pSQL($except), ), 'INSERT'); if (!$result) return false; @@ -492,6 +542,13 @@ abstract class ModuleCore return true; } + /** + * Edit exceptions for module->Hook + * + * @param int $hookID Hook id + * @param array $excepts List of shopID and file name + * @return boolean result + */ public function editExceptions($hookID, $excepts) { $result = true; @@ -713,10 +770,10 @@ abstract class ModuleCore $moduleList[] = $item; if (!$xml_exist OR $needNewConfigFile) { - self::$_generateConfigXmlMode = true; + self::$_generate_config_xml_mode = true; $tmpModule = new $module; $tmpModule->_generateConfigXml(); - self::$_generateConfigXmlMode = false; + self::$_generate_config_xml_mode = false; } } else @@ -985,7 +1042,7 @@ abstract class ModuleCore */ public function l($string, $specific = false, $id_lang = null) { - if (self::$_generateConfigXmlMode) + if (self::$_generate_config_xml_mode) return $string; global $_MODULES, $_MODULE; @@ -1318,5 +1375,7 @@ abstract class ModuleCore { return Db::getInstance()->getValue('SELECT id_module FROM `'._DB_PREFIX_.'module` WHERE name = "'.pSQL($name).'"'); } + + public function getErrors() { return $this->_errors; } } diff --git a/controllers/admin/AdminModulesController.php b/controllers/admin/AdminModulesController.php index 962ea12d4..4b8d68831 100644 --- a/controllers/admin/AdminModulesController.php +++ b/controllers/admin/AdminModulesController.php @@ -529,7 +529,7 @@ class AdminModulesControllerCore extends AdminController } } - if (((method_exists($module, $method) && ($echo = $module->{$method}())) || ($echo = ' ')) AND $key == 'configure' AND Module::isInstalled($module->name)) + if (((method_exists($module, $method) && ($echo = $module->{$method}()))) AND $key == 'configure' AND Module::isInstalled($module->name)) { $backlink = self::$currentIndex.'&token='.$this->token.'&tab_module='.$module->tab.'&module_name='.$module->name; $hooklink = 'index.php?tab=AdminModulesPositions&token='.Tools::getAdminTokenLite('AdminModulesPositions').'&show_modules='.(int)$module->id; @@ -573,10 +573,10 @@ class AdminModulesControllerCore extends AdminController // TODO : Make something cleaner $this->context->smarty->assign('module_content', $toolbar.'
 
'.$echo.'
 
'.$toolbar); } - elseif($echo) + elseif($echo === true) $return = ($method == 'install' ? 12 : 13); elseif ($echo === false) - $module_errors[] = $name; + $module_errors[] = array('name' => $name, 'errors' => $module->getErrors()); if (Shop::isFeatureActive() && Context::shop() != Shop::CONTEXT_ALL && isset(Context::getContext()->tmpOldShop)) { @@ -590,9 +590,15 @@ class AdminModulesControllerCore extends AdminController if (count($module_errors)) { // If error during module installation, no redirection - $html_error = '