From d5929e838010081f4deab385c1c44022a654590f Mon Sep 17 00:00:00 2001 From: Vincent Garcia Date: Thu, 9 Feb 2023 16:30:49 +0100 Subject: [PATCH 01/11] :sparkles: Adding DependencyBuilder In order to correctly build the dependencies variable to instanciate the CDC, add an helper to ease the work of module devs. --- src/DependencyBuilder.php | 71 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/DependencyBuilder.php diff --git a/src/DependencyBuilder.php b/src/DependencyBuilder.php new file mode 100644 index 0000000..4cd6de9 --- /dev/null +++ b/src/DependencyBuilder.php @@ -0,0 +1,71 @@ +module = $module; + } + + /** + * Build the dependencies data array to be given to the CDC + * + * @return array + */ + public function buildDependencies() + { + $data = [ + 'module_display_name' => $this->module->displayName, + 'module_name' => $this->module->name, + 'module_version' => $this->module->version, + 'ps_version' => _PS_VERSION_, + 'php_version' => PHP_VERSION, + 'dependencies' => [], + ]; + + $dependencyFile = $this->module->getLocalPath() . self::DEPENDENCY_FILENAME; + if (!file_exists($dependencyFile)) { + throw new \Exception(self::DEPENDENCY_FILENAME . ' file is not found in ' . $this->module->getLocalPath()); + } + + $dependenciesContent = json_decode(file_get_contents($dependencyFile), true); + if (json_last_error() != JSON_ERROR_NONE) { + throw new \Exception(self::DEPENDENCY_FILENAME . ' file may be malformatted.'); + } + + if (empty($dependenciesContent['dependencies'])) { + return $data; + } + + foreach ($dependenciesContent['dependencies'] as $dependencyName => $dependencyMinVersion) { + $dependencyData = \DbCore::getInstance()->getRow('SELECT `id_module`, `active`, `version` FROM `' . _DB_PREFIX_ . 'module` WHERE `name` = "' . pSQL($dependencyName) . '"'); + if (!$dependencyData) { + $data['dependencies'][$dependencyName] = [ + 'min_version' => $dependencyMinVersion, + 'installed' => false, + ]; + continue; + } + $data['dependencies'][$dependencyName] = [ + 'min_version' => $dependencyMinVersion, + 'installed' => true, + 'enabled' => (bool) $dependencyData['active'], + 'current_version' => $dependencyData['version'], + ]; + } + + return $data; + } +} From 136882028543f13454836f8d89974872c544a846 Mon Sep 17 00:00:00 2001 From: Vincent Garcia Date: Thu, 9 Feb 2023 17:53:43 +0100 Subject: [PATCH 02/11] :sparkles: Adding locale in context of DependencyBuilder.php --- src/DependencyBuilder.php | 54 +++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/src/DependencyBuilder.php b/src/DependencyBuilder.php index 4cd6de9..3006013 100644 --- a/src/DependencyBuilder.php +++ b/src/DependencyBuilder.php @@ -22,47 +22,73 @@ public function __construct($module) /** * Build the dependencies data array to be given to the CDC * - * @return array + * @return array{ + * "module_display_name": string, + * "module_name": string, + * "module_version": string, + * "ps_version": string, + * "php_version": string, + * "locale": string, + * "dependencies": array + * } */ public function buildDependencies() { $data = [ - 'module_display_name' => $this->module->displayName, - 'module_name' => $this->module->name, - 'module_version' => $this->module->version, - 'ps_version' => _PS_VERSION_, - 'php_version' => PHP_VERSION, + 'module_display_name' => (string) $this->module->displayName, + 'module_name' => (string) $this->module->name, + 'module_version' => (string) $this->module->version, + 'ps_version' => (string) _PS_VERSION_, + 'php_version' => (string) PHP_VERSION, 'dependencies' => [], ]; + $context = \ContextCore::getContext(); + if ($context !== null && $context->employee !== null) { + $locale = \DbCore::getInstance()->getValue('SELECT `locale` FROM `' . _DB_PREFIX_ . 'lang` WHERE `id_lang`=' . pSQL((string) $context->employee->id_lang)); + } + + if (empty($locale)) { + $locale = 'en-GB'; + } + + $data['locale'] = (string) $locale; + $dependencyFile = $this->module->getLocalPath() . self::DEPENDENCY_FILENAME; if (!file_exists($dependencyFile)) { throw new \Exception(self::DEPENDENCY_FILENAME . ' file is not found in ' . $this->module->getLocalPath()); } - $dependenciesContent = json_decode(file_get_contents($dependencyFile), true); - if (json_last_error() != JSON_ERROR_NONE) { + if ($fileContent = file_get_contents($dependencyFile)) { + $dependenciesContent = json_decode($fileContent, true); + } + if (!isset($dependenciesContent) || json_last_error() != JSON_ERROR_NONE) { throw new \Exception(self::DEPENDENCY_FILENAME . ' file may be malformatted.'); } - if (empty($dependenciesContent['dependencies'])) { + if (!is_array($dependenciesContent) || empty($dependenciesContent['dependencies']) || !is_array($dependenciesContent['dependencies'])) { return $data; } foreach ($dependenciesContent['dependencies'] as $dependencyName => $dependencyMinVersion) { - $dependencyData = \DbCore::getInstance()->getRow('SELECT `id_module`, `active`, `version` FROM `' . _DB_PREFIX_ . 'module` WHERE `name` = "' . pSQL($dependencyName) . '"'); + $dependencyData = \DbCore::getInstance()->getRow('SELECT `id_module`, `active`, `version` FROM `' . _DB_PREFIX_ . 'module` WHERE `name` = "' . pSQL((string) $dependencyName) . '"'); if (!$dependencyData) { $data['dependencies'][$dependencyName] = [ - 'min_version' => $dependencyMinVersion, + 'min_version' => (string) $dependencyMinVersion, 'installed' => false, ]; continue; } $data['dependencies'][$dependencyName] = [ - 'min_version' => $dependencyMinVersion, + 'min_version' => (string) $dependencyMinVersion, 'installed' => true, - 'enabled' => (bool) $dependencyData['active'], - 'current_version' => $dependencyData['version'], + 'enabled' => isset($dependencyData['active']) && (bool) $dependencyData['active'], + 'current_version' => isset($dependencyData['version']) ? $dependencyData['version'] : null, ]; } From 120891d997d7db73a6f600e6459d0c1a92bac6ec Mon Sep 17 00:00:00 2001 From: Vincent Garcia Date: Fri, 10 Feb 2023 14:50:56 +0100 Subject: [PATCH 03/11] :sparkles: Adding routes to be called in context --- src/DependencyBuilder.php | 66 ++++++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/src/DependencyBuilder.php b/src/DependencyBuilder.php index 3006013..495968b 100644 --- a/src/DependencyBuilder.php +++ b/src/DependencyBuilder.php @@ -2,6 +2,8 @@ namespace Prestashop\ModuleLibMboInstaller; +use Symfony\Component\Routing\Router; + class DependencyBuilder { const DEPENDENCY_FILENAME = 'ps_dependencies.json'; @@ -11,12 +13,20 @@ class DependencyBuilder */ protected $module; + /** + * @var Router + */ + protected $router; + /** * @param \ModuleCore $module + * + * @throws \Exception */ public function __construct($module) { $this->module = $module; + $this->buildRouter(); } /** @@ -36,6 +46,8 @@ public function __construct($module) * "enabled"?: bool * }> * } + * + * @throws \Exception */ public function buildDependencies() { @@ -77,21 +89,61 @@ public function buildDependencies() foreach ($dependenciesContent['dependencies'] as $dependencyName => $dependencyMinVersion) { $dependencyData = \DbCore::getInstance()->getRow('SELECT `id_module`, `active`, `version` FROM `' . _DB_PREFIX_ . 'module` WHERE `name` = "' . pSQL((string) $dependencyName) . '"'); + + $data['dependencies'][$dependencyName] = array_merge(['min_version' => (string) $dependencyMinVersion], $this->buildRoutesForModule($dependencyName)); if (!$dependencyData) { - $data['dependencies'][$dependencyName] = [ - 'min_version' => (string) $dependencyMinVersion, - 'installed' => false, - ]; + $data['dependencies'][$dependencyName]['installed'] = false; continue; } - $data['dependencies'][$dependencyName] = [ - 'min_version' => (string) $dependencyMinVersion, + $data['dependencies'][$dependencyName] = array_merge($data['dependencies'][$dependencyName], [ 'installed' => true, 'enabled' => isset($dependencyData['active']) && (bool) $dependencyData['active'], 'current_version' => isset($dependencyData['version']) ? $dependencyData['version'] : null, - ]; + ]); } return $data; } + + /** + * @param string $moduleName + * + * @return array + */ + protected function buildRoutesForModule($moduleName) + { + $urls = []; + foreach (['install', 'enable', 'upgrade'] as $action) { + $urls[$action] = $this->router->generate('admin_module_manage_action', [ + 'action' => $action, + 'module_name' => $moduleName, + ]); + } + + return $urls; + } + + /** + * @return void + * + * @throws \Exception + */ + protected function buildRouter() + { + global $kernel; + if (!$kernel instanceof \AppKernel) { + throw new \Exception('Unable to retrieve Symfony AppKernel.'); + } + + $container = $kernel->getContainer(); + if (!$container instanceof \Symfony\Component\DependencyInjection\ContainerInterface) { + throw new \Exception('Unable to retrieve Symfony ContainerInterface.'); + } + + $router = $container->get('router'); + if (!$router instanceof Router) { + throw new \Exception('Unable to retrieve Symfony Router.'); + } + $this->router = $router; + } } From a6fadc2a520097a61bd50beab40e405ce2deabee Mon Sep 17 00:00:00 2001 From: Vincent Garcia Date: Thu, 9 Mar 2023 17:46:21 +0100 Subject: [PATCH 04/11] :sparkles: Handle the MBO module installation. Set up new context variable to interact with the CDC. Install or enable the MBO module if needed. --- src/DependencyBuilder.php | 113 ++++++++++++++++++++++++++++++++++---- src/Installer.php | 19 +++++++ 2 files changed, 121 insertions(+), 11 deletions(-) diff --git a/src/DependencyBuilder.php b/src/DependencyBuilder.php index 495968b..540cfaf 100644 --- a/src/DependencyBuilder.php +++ b/src/DependencyBuilder.php @@ -2,11 +2,18 @@ namespace Prestashop\ModuleLibMboInstaller; +use Prestashop\ModuleLibGuzzleAdapter\Interfaces\ClientExceptionInterface; use Symfony\Component\Routing\Router; class DependencyBuilder { const DEPENDENCY_FILENAME = 'ps_dependencies.json'; + const GET_PARAMETER = 'mbo_action_needed'; + const INSTALL_ACTION = 'install'; + const ENABLE_ACTION = 'enable'; + const APP_STATE_LAUNCHABLE = 'launchable'; + const APP_STATE_MBO_IN_PROGRESS = 'mbo_in_progress'; + const APP_STATE_AUTOSTART = 'autostart'; /** * @var \ModuleCore @@ -29,9 +36,66 @@ public function __construct($module) $this->buildRouter(); } + /** + * Handle dependencies behavior and return dependencies data array to be given to the CDC + * + * @return array{ + * "module_display_name": string, + * "module_name": string, + * "module_version": string, + * "ps_version": string, + * "php_version": string, + * "locale": string, + * "app_state": string, + * "dependencies": array{}|array{ps_mbo: array} + * } + * + * @throws \Exception|ClientExceptionInterface + */ + public function handleDependencies() + { + $appState = $this->handleMboInstallation(); + + return $this->buildDependenciesContext($appState); + } + + /** + * Install or enable the MBO depending on the action requested + * + * @return string + * + * @throws \Exception|ClientExceptionInterface + */ + protected function handleMboInstallation() + { + if (!isset($_GET[self::GET_PARAMETER])) { + return self::APP_STATE_LAUNCHABLE; + } + + $mboStatus = (new Presenter())->present(); + $installer = new Installer(_PS_VERSION_); + + if ($mboStatus['isInstalled'] && $mboStatus['isEnabled']) { + return self::APP_STATE_AUTOSTART; + } + + if (!$mboStatus['isInstalled']) { + $installer->installModule(); + } elseif (!$mboStatus['isEnabled']) { + $installer->enableModule(); + } + + // Force another refresh of the page to correctly clear the cache and load MBO configurations + header('Refresh:0'); + // To avoid wasting time rerendering the entire page, die immediately + return self::APP_STATE_MBO_IN_PROGRESS; + } + /** * Build the dependencies data array to be given to the CDC * + * @param string $appState + * * @return array{ * "module_display_name": string, * "module_name": string, @@ -39,17 +103,13 @@ public function __construct($module) * "ps_version": string, * "php_version": string, * "locale": string, - * "dependencies": array + * "app_state": string, + * "dependencies": array{}|array{ps_mbo: array} * } * * @throws \Exception */ - public function buildDependencies() + protected function buildDependenciesContext($appState = self::APP_STATE_LAUNCHABLE) { $data = [ 'module_display_name' => (string) $this->module->displayName, @@ -57,6 +117,7 @@ public function buildDependencies() 'module_version' => (string) $this->module->version, 'ps_version' => (string) _PS_VERSION_, 'php_version' => (string) PHP_VERSION, + 'app_state' => $appState, 'dependencies' => [], ]; @@ -84,13 +145,23 @@ public function buildDependencies() } if (!is_array($dependenciesContent) || empty($dependenciesContent['dependencies']) || !is_array($dependenciesContent['dependencies'])) { + $mboDependencyData = $this->addMboInDependencies(); + + if ($mboDependencyData) { + $data['dependencies'][Installer::MODULE_NAME] = $mboDependencyData; + } + return $data; } - foreach ($dependenciesContent['dependencies'] as $dependencyName => $dependencyMinVersion) { + if (!isset($dependenciesContent['dependencies'][Installer::MODULE_NAME])) { + $dependenciesContent['dependencies'][] = Installer::MODULE_NAME; + } + + foreach ($dependenciesContent['dependencies'] as $dependencyName) { $dependencyData = \DbCore::getInstance()->getRow('SELECT `id_module`, `active`, `version` FROM `' . _DB_PREFIX_ . 'module` WHERE `name` = "' . pSQL((string) $dependencyName) . '"'); - $data['dependencies'][$dependencyName] = array_merge(['min_version' => (string) $dependencyMinVersion], $this->buildRoutesForModule($dependencyName)); + $data['dependencies'][$dependencyName] = $this->buildRoutesForModule($dependencyName); if (!$dependencyData) { $data['dependencies'][$dependencyName]['installed'] = false; continue; @@ -137,13 +208,33 @@ protected function buildRouter() $container = $kernel->getContainer(); if (!$container instanceof \Symfony\Component\DependencyInjection\ContainerInterface) { - throw new \Exception('Unable to retrieve Symfony ContainerInterface.'); + throw new \Exception('Unable to retrieve Symfony container.'); } $router = $container->get('router'); if (!$router instanceof Router) { - throw new \Exception('Unable to retrieve Symfony Router.'); + throw new \Exception('Unable to retrieve Symfony router.'); } $this->router = $router; } + + /** + * @return array|null + */ + protected function addMboInDependencies() + { + $mboStatus = (new Presenter())->present(); + + if ((bool) $mboStatus['isEnabled']) { + return null; + } + + $mboRoutes = $this->buildRoutesForModule(Installer::MODULE_NAME); + + return array_merge([ + 'current_version' => (string) $mboStatus['version'], + 'installed' => (bool) $mboStatus['isInstalled'], + 'enabled' => false, + ], $mboRoutes); + } } diff --git a/src/Installer.php b/src/Installer.php index 70da181..4ea461c 100644 --- a/src/Installer.php +++ b/src/Installer.php @@ -4,6 +4,7 @@ use GuzzleHttp\Psr7\Request; use Prestashop\ModuleLibGuzzleAdapter\ClientFactory; +use Prestashop\ModuleLibGuzzleAdapter\Interfaces\ClientExceptionInterface; use Prestashop\ModuleLibGuzzleAdapter\Interfaces\HttpClientInterface; use PrestaShop\PrestaShop\Core\Addon\Module\ModuleManagerBuilder; @@ -30,6 +31,8 @@ class Installer /** * @param string $prestashopVersion + * + * @throws \Exception */ public function __construct($prestashopVersion) { @@ -47,6 +50,8 @@ public function __construct($prestashopVersion) * Installs ps_mbo module * * @return bool + * + * @throws ClientExceptionInterface */ public function installModule() { @@ -59,10 +64,24 @@ public function installModule() return $this->moduleManagerBuilder->build()->install(self::MODULE_NAME); } + /** + * Enable ps_mbo module + * + * @return bool + * + * @throws \Exception + */ + public function enableModule() + { + return $this->moduleManagerBuilder->build()->enable(self::MODULE_NAME); + } + /** * Downloads ps_mbo module source from addons, store it and returns the file name * * @return string + * + * @throws \Exception|ClientExceptionInterface */ private function downloadModule() { From 1282ad32ad18767be967eb1d170651ebe3fb6042 Mon Sep 17 00:00:00 2001 From: Vincent Garcia Date: Fri, 10 Mar 2023 10:40:40 +0100 Subject: [PATCH 05/11] :sparkles: Only add MBO if needed in PS --- src/DependencyBuilder.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/DependencyBuilder.php b/src/DependencyBuilder.php index 540cfaf..d828747 100644 --- a/src/DependencyBuilder.php +++ b/src/DependencyBuilder.php @@ -68,7 +68,7 @@ public function handleDependencies() */ protected function handleMboInstallation() { - if (!isset($_GET[self::GET_PARAMETER])) { + if (!isset($_GET[self::GET_PARAMETER]) || !$this->isMboNeeded()) { return self::APP_STATE_LAUNCHABLE; } @@ -154,7 +154,7 @@ protected function buildDependenciesContext($appState = self::APP_STATE_LAUNCHAB return $data; } - if (!isset($dependenciesContent['dependencies'][Installer::MODULE_NAME])) { + if ($this->isMboNeeded() && !isset($dependenciesContent['dependencies'][Installer::MODULE_NAME])) { $dependenciesContent['dependencies'][] = Installer::MODULE_NAME; } @@ -223,6 +223,10 @@ protected function buildRouter() */ protected function addMboInDependencies() { + if (!$this->isMboNeeded()) { + return null; + } + $mboStatus = (new Presenter())->present(); if ((bool) $mboStatus['isEnabled']) { @@ -237,4 +241,12 @@ protected function addMboInDependencies() 'enabled' => false, ], $mboRoutes); } + + /** + * @return bool + */ + protected function isMboNeeded() + { + return version_compare(_PS_VERSION_, '1.7.5', '>='); + } } From d54a753584ff861f3259f09cca67cfd4a11f57fd Mon Sep 17 00:00:00 2001 From: Vincent Garcia Date: Fri, 10 Mar 2023 12:54:10 +0100 Subject: [PATCH 06/11] :sparkles: Make the mbo installation through ajax process --- src/DependencyBuilder.php | 54 +++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/src/DependencyBuilder.php b/src/DependencyBuilder.php index d828747..d7179b8 100644 --- a/src/DependencyBuilder.php +++ b/src/DependencyBuilder.php @@ -2,18 +2,12 @@ namespace Prestashop\ModuleLibMboInstaller; -use Prestashop\ModuleLibGuzzleAdapter\Interfaces\ClientExceptionInterface; use Symfony\Component\Routing\Router; class DependencyBuilder { const DEPENDENCY_FILENAME = 'ps_dependencies.json'; const GET_PARAMETER = 'mbo_action_needed'; - const INSTALL_ACTION = 'install'; - const ENABLE_ACTION = 'enable'; - const APP_STATE_LAUNCHABLE = 'launchable'; - const APP_STATE_MBO_IN_PROGRESS = 'mbo_in_progress'; - const APP_STATE_AUTOSTART = 'autostart'; /** * @var \ModuleCore @@ -46,56 +40,62 @@ public function __construct($module) * "ps_version": string, * "php_version": string, * "locale": string, - * "app_state": string, * "dependencies": array{}|array{ps_mbo: array} * } * - * @throws \Exception|ClientExceptionInterface + * @throws \Exception */ public function handleDependencies() { - $appState = $this->handleMboInstallation(); + $this->handleMboInstallation(); - return $this->buildDependenciesContext($appState); + return $this->buildDependenciesContext(); } /** * Install or enable the MBO depending on the action requested * - * @return string - * - * @throws \Exception|ClientExceptionInterface + * @return void */ protected function handleMboInstallation() { if (!isset($_GET[self::GET_PARAMETER]) || !$this->isMboNeeded()) { - return self::APP_STATE_LAUNCHABLE; + return; } $mboStatus = (new Presenter())->present(); $installer = new Installer(_PS_VERSION_); if ($mboStatus['isInstalled'] && $mboStatus['isEnabled']) { - return self::APP_STATE_AUTOSTART; + return; } - if (!$mboStatus['isInstalled']) { - $installer->installModule(); - } elseif (!$mboStatus['isEnabled']) { - $installer->enableModule(); + $data = [Installer::MODULE_NAME => [ + 'status' => true, + ]]; + + try { + if (!$mboStatus['isInstalled']) { + $installer->installModule(); + } elseif (!$mboStatus['isEnabled']) { + $installer->enableModule(); + } + } catch (\Exception $e) { + $data[Installer::MODULE_NAME] = [ + 'status' => false, + 'msg' => $e->getMessage(), + ]; } - // Force another refresh of the page to correctly clear the cache and load MBO configurations - header('Refresh:0'); - // To avoid wasting time rerendering the entire page, die immediately - return self::APP_STATE_MBO_IN_PROGRESS; + // This call is done in ajax by the CDC, bypass the normal return + header('Content-type: application/json'); + echo json_encode($data); + exit; } /** * Build the dependencies data array to be given to the CDC * - * @param string $appState - * * @return array{ * "module_display_name": string, * "module_name": string, @@ -103,13 +103,12 @@ protected function handleMboInstallation() * "ps_version": string, * "php_version": string, * "locale": string, - * "app_state": string, * "dependencies": array{}|array{ps_mbo: array} * } * * @throws \Exception */ - protected function buildDependenciesContext($appState = self::APP_STATE_LAUNCHABLE) + protected function buildDependenciesContext() { $data = [ 'module_display_name' => (string) $this->module->displayName, @@ -117,7 +116,6 @@ protected function buildDependenciesContext($appState = self::APP_STATE_LAUNCHAB 'module_version' => (string) $this->module->version, 'ps_version' => (string) _PS_VERSION_, 'php_version' => (string) PHP_VERSION, - 'app_state' => $appState, 'dependencies' => [], ]; From 607d1344518fb9bee395be421497ec4da7c1e514 Mon Sep 17 00:00:00 2001 From: Vincent Garcia Date: Fri, 10 Mar 2023 17:21:44 +0100 Subject: [PATCH 07/11] :sparkles: Adding id to the dependencies_file tpl --- src/DependencyBuilder.php | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/DependencyBuilder.php b/src/DependencyBuilder.php index d7179b8..85483fc 100644 --- a/src/DependencyBuilder.php +++ b/src/DependencyBuilder.php @@ -40,7 +40,7 @@ public function __construct($module) * "ps_version": string, * "php_version": string, * "locale": string, - * "dependencies": array{}|array{ps_mbo: array} + * "dependencies": array{}|array{ps_mbo: array} * } * * @throws \Exception @@ -103,7 +103,7 @@ protected function handleMboInstallation() * "ps_version": string, * "php_version": string, * "locale": string, - * "dependencies": array{}|array{ps_mbo: array} + * "dependencies": array{}|array{ps_mbo: array} * } * * @throws \Exception @@ -153,18 +153,21 @@ protected function buildDependenciesContext() } if ($this->isMboNeeded() && !isset($dependenciesContent['dependencies'][Installer::MODULE_NAME])) { - $dependenciesContent['dependencies'][] = Installer::MODULE_NAME; + $dependenciesContent['dependencies'][] = [ + 'name' => Installer::MODULE_NAME, + 'id' => Installer::MODULE_ID, + ]; } - foreach ($dependenciesContent['dependencies'] as $dependencyName) { - $dependencyData = \DbCore::getInstance()->getRow('SELECT `id_module`, `active`, `version` FROM `' . _DB_PREFIX_ . 'module` WHERE `name` = "' . pSQL((string) $dependencyName) . '"'); + foreach ($dependenciesContent['dependencies'] as $dependency) { + $dependencyData = \DbCore::getInstance()->getRow('SELECT `id_module`, `active`, `version` FROM `' . _DB_PREFIX_ . 'module` WHERE `name` = "' . pSQL((string) $dependency['name']) . '"'); - $data['dependencies'][$dependencyName] = $this->buildRoutesForModule($dependencyName); + $data['dependencies'][$dependency['name']] = array_merge($dependency, $this->buildRoutesForModule($dependency['name'])); if (!$dependencyData) { - $data['dependencies'][$dependencyName]['installed'] = false; + $data['dependencies'][$dependency['name']]['installed'] = false; continue; } - $data['dependencies'][$dependencyName] = array_merge($data['dependencies'][$dependencyName], [ + $data['dependencies'][$dependency['name']] = array_merge($data['dependencies'][$dependency['name']], [ 'installed' => true, 'enabled' => isset($dependencyData['active']) && (bool) $dependencyData['active'], 'current_version' => isset($dependencyData['version']) ? $dependencyData['version'] : null, @@ -217,7 +220,7 @@ protected function buildRouter() } /** - * @return array|null + * @return array|null */ protected function addMboInDependencies() { @@ -237,6 +240,8 @@ protected function addMboInDependencies() 'current_version' => (string) $mboStatus['version'], 'installed' => (bool) $mboStatus['isInstalled'], 'enabled' => false, + 'id' => Installer::MODULE_ID, + 'name' => Installer::MODULE_NAME, ], $mboRoutes); } From 95468d336771346ca0611902cdc095a1cc3a7f8e Mon Sep 17 00:00:00 2001 From: Ibrahima SOW Date: Mon, 16 Oct 2023 14:00:55 +0000 Subject: [PATCH 08/11] Avoid using deprecated methods --- src/Installer.php | 17 +++++++++++------ src/Presenter.php | 16 ++++++++++++++-- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/Installer.php b/src/Installer.php index 4ea461c..a9ce3a6 100644 --- a/src/Installer.php +++ b/src/Installer.php @@ -6,6 +6,7 @@ use Prestashop\ModuleLibGuzzleAdapter\ClientFactory; use Prestashop\ModuleLibGuzzleAdapter\Interfaces\ClientExceptionInterface; use Prestashop\ModuleLibGuzzleAdapter\Interfaces\HttpClientInterface; +use PrestaShop\PrestaShop\Core\Addon\AddonManagerInterface; use PrestaShop\PrestaShop\Core\Addon\Module\ModuleManagerBuilder; class Installer @@ -20,9 +21,9 @@ class Installer protected $marketplaceClient; /** - * @var ModuleManagerBuilder + * @var AddonManagerInterface */ - protected $moduleManagerBuilder; + protected $moduleManager; /** * @var string @@ -41,8 +42,12 @@ public function __construct($prestashopVersion) throw new \Exception('ModuleManagerBuilder::getInstance() failed'); } + $this->moduleManager = $moduleManagerBuilder->build(); + if (is_null($this->moduleManager)) { + throw new \Exception('ModuleManagerBuilder::build() failed'); + } + $this->marketplaceClient = (new ClientFactory())->getClient(['base_uri' => self::ADDONS_URL]); - $this->moduleManagerBuilder = $moduleManagerBuilder; $this->prestashopVersion = $prestashopVersion; } @@ -58,10 +63,10 @@ public function installModule() // On PrestaShop 1.7, the signature is install($source), with $source a module name or a path to an archive. // On PrestaShop 8, the signature is install(string $name, $source = null). if (version_compare($this->prestashopVersion, '8.0.0', '>=')) { - return $this->moduleManagerBuilder->build()->install(self::MODULE_NAME, $this->downloadModule()); + return $this->moduleManager->install(self::MODULE_NAME, $this->downloadModule()); } - return $this->moduleManagerBuilder->build()->install(self::MODULE_NAME); + return $this->moduleManager->install(self::MODULE_NAME); } /** @@ -73,7 +78,7 @@ public function installModule() */ public function enableModule() { - return $this->moduleManagerBuilder->build()->enable(self::MODULE_NAME); + return $this->moduleManager->enable(self::MODULE_NAME); } /** diff --git a/src/Presenter.php b/src/Presenter.php index c2233aa..68b3096 100644 --- a/src/Presenter.php +++ b/src/Presenter.php @@ -2,6 +2,8 @@ namespace Prestashop\ModuleLibMboInstaller; +use PrestaShop\PrestaShop\Core\Addon\Module\ModuleManagerBuilder; + class Presenter { /** @@ -19,10 +21,20 @@ public function present() $version = $mboModule->version; } + $moduleManagerBuilder = ModuleManagerBuilder::getInstance(); + if (is_null($moduleManagerBuilder)) { + throw new \Exception('ModuleManagerBuilder::getInstance() failed'); + } + + $moduleManager = $moduleManagerBuilder->build(); + if (is_null($moduleManager)) { + throw new \Exception('ModuleManagerBuilder::build() failed'); + } + return [ 'isPresentOnDisk' => (bool) $mboModule, - 'isInstalled' => ($mboModule && \Module::isInstalled(Installer::MODULE_NAME)), - 'isEnabled' => ($mboModule && \Module::isEnabled(Installer::MODULE_NAME)), + 'isInstalled' => ($mboModule && $moduleManager->isInstalled(Installer::MODULE_NAME)), + 'isEnabled' => ($mboModule && $moduleManager->isEnabled(Installer::MODULE_NAME)), 'version' => $version, ]; } From 02eae86ff3533cad76c1039173fdc3f9bdb9416d Mon Sep 17 00:00:00 2001 From: Ibrahima SOW Date: Mon, 16 Oct 2023 14:10:58 +0000 Subject: [PATCH 09/11] Rename dependencies config file --- phpstan.neon | 8 ++++++++ phpunit.xml | 3 +++ src/DependencyBuilder.php | 4 +--- src/Installer.php | 3 +-- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 87e159f..fc819e8 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -9,5 +9,13 @@ parameters: reportUnmatchedIgnoredErrors: false ignoreErrors: - '#Method PrestaShop\\PrestaShop\\Core\\Addon\\Module\\ModuleManager::install\(\) invoked with 2 parameters, 1 required.#' + - '#Call to function is_null\(\) with PrestaShop\\PrestaShop\\Core\\Addon\\Module\\ModuleManager will always evaluate to false.#' + - '#Call to function is_null\(\) with PrestaShop\\PrestaShop\\Core\\Module\\ModuleManager will always evaluate to false.#' + - '#Property Prestashop\\ModuleLibMboInstaller\\Installer::\$moduleManager has unknown class PrestaShop\\PrestaShop\\Core\\Addon\\Module\\ModuleManager as its type.#' + - '#Property Prestashop\\ModuleLibMboInstaller\\Installer::\$moduleManager has unknown class PrestaShop\\PrestaShop\\Core\\Module\\ModuleManager as its type.#' + - '#Call to method install\(\) on an unknown class PrestaShop\\PrestaShop\\Core\\Addon\\Module\\ModuleManager.#' + - '#Call to method install\(\) on an unknown class PrestaShop\\PrestaShop\\Core\\Module\\ModuleManager.#' + - '#Call to method enable\(\) on an unknown class PrestaShop\\PrestaShop\\Core\\Addon\\Module\\ModuleManager.#' + - '#Call to method enable\(\) on an unknown class PrestaShop\\PrestaShop\\Core\\Module\\ModuleManager.#' level: max diff --git a/phpunit.xml b/phpunit.xml index f6c3b38..ded6967 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -9,6 +9,9 @@ convertWarningsToExceptions="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" > + + + tests/Integration diff --git a/src/DependencyBuilder.php b/src/DependencyBuilder.php index 85483fc..1b4318a 100644 --- a/src/DependencyBuilder.php +++ b/src/DependencyBuilder.php @@ -6,7 +6,7 @@ class DependencyBuilder { - const DEPENDENCY_FILENAME = 'ps_dependencies.json'; + const DEPENDENCY_FILENAME = 'module_dependencies.json'; const GET_PARAMETER = 'mbo_action_needed'; /** @@ -155,7 +155,6 @@ protected function buildDependenciesContext() if ($this->isMboNeeded() && !isset($dependenciesContent['dependencies'][Installer::MODULE_NAME])) { $dependenciesContent['dependencies'][] = [ 'name' => Installer::MODULE_NAME, - 'id' => Installer::MODULE_ID, ]; } @@ -240,7 +239,6 @@ protected function addMboInDependencies() 'current_version' => (string) $mboStatus['version'], 'installed' => (bool) $mboStatus['isInstalled'], 'enabled' => false, - 'id' => Installer::MODULE_ID, 'name' => Installer::MODULE_NAME, ], $mboRoutes); } diff --git a/src/Installer.php b/src/Installer.php index a9ce3a6..dbca656 100644 --- a/src/Installer.php +++ b/src/Installer.php @@ -6,7 +6,6 @@ use Prestashop\ModuleLibGuzzleAdapter\ClientFactory; use Prestashop\ModuleLibGuzzleAdapter\Interfaces\ClientExceptionInterface; use Prestashop\ModuleLibGuzzleAdapter\Interfaces\HttpClientInterface; -use PrestaShop\PrestaShop\Core\Addon\AddonManagerInterface; use PrestaShop\PrestaShop\Core\Addon\Module\ModuleManagerBuilder; class Installer @@ -21,7 +20,7 @@ class Installer protected $marketplaceClient; /** - * @var AddonManagerInterface + * @var \PrestaShop\PrestaShop\Core\Module\ModuleManager|\PrestaShop\PrestaShop\Core\Addon\Module\ModuleManager */ protected $moduleManager; From 63bdd7bc144b5f42af14b9f55e0db6e0598b2e78 Mon Sep 17 00:00:00 2001 From: Ibrahima SOW Date: Mon, 13 Nov 2023 16:57:46 +0000 Subject: [PATCH 10/11] Add method to check if dependencies are met --- composer.json | 3 +- src/DependencyBuilder.php | 238 ++++++++++++++++++++++++++++---------- 2 files changed, 177 insertions(+), 64 deletions(-) diff --git a/composer.json b/composer.json index 0992030..7c435e6 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,8 @@ "license": "AFL-3.0", "require": { "php": ">=5.6", - "prestashop/module-lib-guzzle-adapter": "^0.6" + "prestashop/module-lib-guzzle-adapter": "^0.6", + "ext-json": "*" }, "config": { "platform": { diff --git a/src/DependencyBuilder.php b/src/DependencyBuilder.php index 1b4318a..341fc8f 100644 --- a/src/DependencyBuilder.php +++ b/src/DependencyBuilder.php @@ -2,6 +2,8 @@ namespace Prestashop\ModuleLibMboInstaller; +use Prestashop\ModuleLibGuzzleAdapter\Interfaces\ClientExceptionInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Routing\Router; class DependencyBuilder @@ -44,6 +46,7 @@ public function __construct($module) * } * * @throws \Exception + * @throws ClientExceptionInterface */ public function handleDependencies() { @@ -52,10 +55,35 @@ public function handleDependencies() return $this->buildDependenciesContext(); } + /** + * @return bool + * + * @throws \Exception + */ + public function areDependenciesMet() + { + $dependencies = $this->getDependencies(false); + + foreach ($dependencies as $dependencyName => $dependency) { + if ( + !array_key_exists('installed', $dependency) + || !array_key_exists('enabled', $dependency) + || false === $dependency['installed'] + || false === $dependency['enabled'] + ) { + return false; + } + } + + return true; + } + /** * Install or enable the MBO depending on the action requested * * @return void + * + * @throws \Exception|ClientExceptionInterface */ protected function handleMboInstallation() { @@ -110,70 +138,38 @@ protected function handleMboInstallation() */ protected function buildDependenciesContext() { - $data = [ + return [ 'module_display_name' => (string) $this->module->displayName, 'module_name' => (string) $this->module->name, 'module_version' => (string) $this->module->version, 'ps_version' => (string) _PS_VERSION_, 'php_version' => (string) PHP_VERSION, - 'dependencies' => [], + 'locale' => $this->getLocale(), + 'dependencies' => $this->getDependencies(true), ]; + } + /** + * @return string + */ + private function getLocale() + { $context = \ContextCore::getContext(); if ($context !== null && $context->employee !== null) { - $locale = \DbCore::getInstance()->getValue('SELECT `locale` FROM `' . _DB_PREFIX_ . 'lang` WHERE `id_lang`=' . pSQL((string) $context->employee->id_lang)); + $locale = \DbCore::getInstance()->getValue( + sprintf( + 'SELECT `locale` FROM `%slang` WHERE `id_lang`=%s', + _DB_PREFIX_, + pSQL((string) $context->employee->id_lang) + ) + ); } if (empty($locale)) { - $locale = 'en-GB'; - } - - $data['locale'] = (string) $locale; - - $dependencyFile = $this->module->getLocalPath() . self::DEPENDENCY_FILENAME; - if (!file_exists($dependencyFile)) { - throw new \Exception(self::DEPENDENCY_FILENAME . ' file is not found in ' . $this->module->getLocalPath()); + return 'en-GB'; } - if ($fileContent = file_get_contents($dependencyFile)) { - $dependenciesContent = json_decode($fileContent, true); - } - if (!isset($dependenciesContent) || json_last_error() != JSON_ERROR_NONE) { - throw new \Exception(self::DEPENDENCY_FILENAME . ' file may be malformatted.'); - } - - if (!is_array($dependenciesContent) || empty($dependenciesContent['dependencies']) || !is_array($dependenciesContent['dependencies'])) { - $mboDependencyData = $this->addMboInDependencies(); - - if ($mboDependencyData) { - $data['dependencies'][Installer::MODULE_NAME] = $mboDependencyData; - } - - return $data; - } - - if ($this->isMboNeeded() && !isset($dependenciesContent['dependencies'][Installer::MODULE_NAME])) { - $dependenciesContent['dependencies'][] = [ - 'name' => Installer::MODULE_NAME, - ]; - } - - foreach ($dependenciesContent['dependencies'] as $dependency) { - $dependencyData = \DbCore::getInstance()->getRow('SELECT `id_module`, `active`, `version` FROM `' . _DB_PREFIX_ . 'module` WHERE `name` = "' . pSQL((string) $dependency['name']) . '"'); - - $data['dependencies'][$dependency['name']] = array_merge($dependency, $this->buildRoutesForModule($dependency['name'])); - if (!$dependencyData) { - $data['dependencies'][$dependency['name']]['installed'] = false; - continue; - } - $data['dependencies'][$dependency['name']] = array_merge($data['dependencies'][$dependency['name']], [ - 'installed' => true, - 'enabled' => isset($dependencyData['active']) && (bool) $dependencyData['active'], - 'current_version' => isset($dependencyData['version']) ? $dependencyData['version'] : null, - ]); - } - - return $data; + return $locale; } /** @@ -185,10 +181,14 @@ protected function buildRoutesForModule($moduleName) { $urls = []; foreach (['install', 'enable', 'upgrade'] as $action) { - $urls[$action] = $this->router->generate('admin_module_manage_action', [ + $route = $this->router->generate('admin_module_manage_action', [ 'action' => $action, 'module_name' => $moduleName, ]); + + if (is_string($route)) { + $urls[$action] = $route; + } } return $urls; @@ -207,7 +207,7 @@ protected function buildRouter() } $container = $kernel->getContainer(); - if (!$container instanceof \Symfony\Component\DependencyInjection\ContainerInterface) { + if (!$container instanceof ContainerInterface) { throw new \Exception('Unable to retrieve Symfony container.'); } @@ -219,9 +219,18 @@ protected function buildRouter() } /** - * @return array|null + * @param bool $addRoutes + * + * @return array{ + * "name": string, + * "installed": bool, + * "enabled": bool, + * "current_version": string, + * }|non-empty-array|null + * + * @throws \Exception */ - protected function addMboInDependencies() + protected function addMboInDependencies($addRoutes = false) { if (!$this->isMboNeeded()) { return null; @@ -229,18 +238,21 @@ protected function addMboInDependencies() $mboStatus = (new Presenter())->present(); - if ((bool) $mboStatus['isEnabled']) { - return null; - } - - $mboRoutes = $this->buildRoutesForModule(Installer::MODULE_NAME); - - return array_merge([ + $specification = [ 'current_version' => (string) $mboStatus['version'], 'installed' => (bool) $mboStatus['isInstalled'], - 'enabled' => false, + 'enabled' => (bool) $mboStatus['isEnabled'], 'name' => Installer::MODULE_NAME, - ], $mboRoutes); + ]; + + if (!$addRoutes) { + return $specification; + } + + return array_merge( + $specification, + $this->buildRoutesForModule(Installer::MODULE_NAME) + ); } /** @@ -250,4 +262,104 @@ protected function isMboNeeded() { return version_compare(_PS_VERSION_, '1.7.5', '>='); } + + /** + * @param bool $addRoutes + * + * @return array|array> + * + * @throws \Exception + */ + private function getDependencies($addRoutes = false) + { + $dependenciesContent = $this->getDependenciesSpecification(); + + if (empty($dependenciesContent['dependencies'])) { + $mboDependency = $this->addMboInDependencies($addRoutes); + if (null === $mboDependency) { + return []; + } + + return [ + Installer::MODULE_NAME => $mboDependency, + ]; + } + + if ($this->isMboNeeded() && !isset($dependenciesContent['dependencies'][Installer::MODULE_NAME])) { + $dependenciesContent['dependencies'][] = [ + 'name' => Installer::MODULE_NAME, + ]; + } + + $dependencies = []; + foreach ($dependenciesContent['dependencies'] as $dependency) { + if (!is_array($dependency) || !array_key_exists('name', $dependency)) { + continue; + } + + $dependencyData = \DbCore::getInstance()->getRow( + sprintf( + 'SELECT `id_module`, `active`, `version` FROM `%smodule` WHERE `name` = "%s"', + _DB_PREFIX_, + pSQL((string) $dependency['name']) + ) + ); + + if ($addRoutes) { + $dependencies[$dependency['name']] = array_merge( + $dependency, + $this->buildRoutesForModule($dependency['name']) + ); + } else { + $dependencies[$dependency['name']] = $dependency; + } + + if (!$dependencyData) { + $dependencies[$dependency['name']]['installed'] = false; + $dependencies[$dependency['name']]['enabled'] = false; + continue; + } + $dependencies[$dependency['name']] = array_merge($dependencies[$dependency['name']], [ + 'installed' => true, + 'enabled' => isset($dependencyData['active']) && (bool) $dependencyData['active'], + 'current_version' => isset($dependencyData['version']) ? $dependencyData['version'] : null, + ]); + } + + return $dependencies; + } + + /** + * @return array{ + * "dependencies": array + * } + * + * @throws \Exception + */ + private function getDependenciesSpecification() + { + $dependencyFile = $this->module->getLocalPath() . self::DEPENDENCY_FILENAME; + if (!file_exists($dependencyFile)) { + throw new \Exception(self::DEPENDENCY_FILENAME . ' file is not found in ' . $this->module->getLocalPath()); + } + + if ($fileContent = file_get_contents($dependencyFile)) { + $dependenciesContent = json_decode($fileContent, true); + } + if ( + !isset($dependenciesContent) + || !is_array($dependenciesContent) + || !array_key_exists('dependencies', $dependenciesContent) + || json_last_error() != JSON_ERROR_NONE + ) { + throw new \Exception(self::DEPENDENCY_FILENAME . ' file may be malformed.'); + } + + return $dependenciesContent; + } } From 4ca20b17fe3ac3dce2dd40741dc80aaf8f6ad76e Mon Sep 17 00:00:00 2001 From: Ibrahima SOW Date: Mon, 27 Nov 2023 11:02:57 +0000 Subject: [PATCH 11/11] Fix for module activation check --- src/DependencyBuilder.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/DependencyBuilder.php b/src/DependencyBuilder.php index 341fc8f..8e80488 100644 --- a/src/DependencyBuilder.php +++ b/src/DependencyBuilder.php @@ -310,6 +310,25 @@ private function getDependencies($addRoutes = false) ) ); + if ($dependencyData && is_array($dependencyData) && !empty($dependencyData['id_module'])) { + // For PS < 8.0, enable/disable for a module is decided by the shop association + // We assume that if the module is disabled for one shop, i's considered as disabled + $isModuleActiveForAllShops = (bool) \DbCore::getInstance()->getValue( + sprintf("SELECT id_module + FROM `%smodule_shop` + WHERE id_module=%d AND id_shop IN ('%s') + GROUP BY id_module + HAVING COUNT(*)=%d", + _DB_PREFIX_, + (int) $dependencyData['id_module'], + implode(',', array_map('intval', \Shop::getContextListShopID())), + (int) count(\Shop::getContextListShopID()) + ) + ); + + $dependencyData['active'] = $isModuleActiveForAllShops; + } + if ($addRoutes) { $dependencies[$dependency['name']] = array_merge( $dependency,