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/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
new file mode 100644
index 0000000..8e80488
--- /dev/null
+++ b/src/DependencyBuilder.php
@@ -0,0 +1,384 @@
+module = $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,
+ * "dependencies": array{}|array{ps_mbo: array}
+ * }
+ *
+ * @throws \Exception
+ * @throws ClientExceptionInterface
+ */
+ public function handleDependencies()
+ {
+ $this->handleMboInstallation();
+
+ 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()
+ {
+ if (!isset($_GET[self::GET_PARAMETER]) || !$this->isMboNeeded()) {
+ return;
+ }
+
+ $mboStatus = (new Presenter())->present();
+ $installer = new Installer(_PS_VERSION_);
+
+ if ($mboStatus['isInstalled'] && $mboStatus['isEnabled']) {
+ return;
+ }
+
+ $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(),
+ ];
+ }
+
+ // 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
+ *
+ * @return array{
+ * "module_display_name": string,
+ * "module_name": string,
+ * "module_version": string,
+ * "ps_version": string,
+ * "php_version": string,
+ * "locale": string,
+ * "dependencies": array{}|array{ps_mbo: array}
+ * }
+ *
+ * @throws \Exception
+ */
+ protected function buildDependenciesContext()
+ {
+ 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,
+ '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(
+ sprintf(
+ 'SELECT `locale` FROM `%slang` WHERE `id_lang`=%s',
+ _DB_PREFIX_,
+ pSQL((string) $context->employee->id_lang)
+ )
+ );
+ }
+
+ if (empty($locale)) {
+ return 'en-GB';
+ }
+
+ return $locale;
+ }
+
+ /**
+ * @param string $moduleName
+ *
+ * @return array
+ */
+ protected function buildRoutesForModule($moduleName)
+ {
+ $urls = [];
+ foreach (['install', 'enable', 'upgrade'] as $action) {
+ $route = $this->router->generate('admin_module_manage_action', [
+ 'action' => $action,
+ 'module_name' => $moduleName,
+ ]);
+
+ if (is_string($route)) {
+ $urls[$action] = $route;
+ }
+ }
+
+ 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 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.');
+ }
+ $this->router = $router;
+ }
+
+ /**
+ * @param bool $addRoutes
+ *
+ * @return array{
+ * "name": string,
+ * "installed": bool,
+ * "enabled": bool,
+ * "current_version": string,
+ * }|non-empty-array|null
+ *
+ * @throws \Exception
+ */
+ protected function addMboInDependencies($addRoutes = false)
+ {
+ if (!$this->isMboNeeded()) {
+ return null;
+ }
+
+ $mboStatus = (new Presenter())->present();
+
+ $specification = [
+ 'current_version' => (string) $mboStatus['version'],
+ 'installed' => (bool) $mboStatus['isInstalled'],
+ 'enabled' => (bool) $mboStatus['isEnabled'],
+ 'name' => Installer::MODULE_NAME,
+ ];
+
+ if (!$addRoutes) {
+ return $specification;
+ }
+
+ return array_merge(
+ $specification,
+ $this->buildRoutesForModule(Installer::MODULE_NAME)
+ );
+ }
+
+ /**
+ * @return bool
+ */
+ 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 ($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,
+ $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;
+ }
+}
diff --git a/src/Installer.php b/src/Installer.php
index 70da181..dbca656 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;
@@ -19,9 +20,9 @@ class Installer
protected $marketplaceClient;
/**
- * @var ModuleManagerBuilder
+ * @var \PrestaShop\PrestaShop\Core\Module\ModuleManager|\PrestaShop\PrestaShop\Core\Addon\Module\ModuleManager
*/
- protected $moduleManagerBuilder;
+ protected $moduleManager;
/**
* @var string
@@ -30,6 +31,8 @@ class Installer
/**
* @param string $prestashopVersion
+ *
+ * @throws \Exception
*/
public function __construct($prestashopVersion)
{
@@ -38,8 +41,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;
}
@@ -47,22 +54,38 @@ public function __construct($prestashopVersion)
* Installs ps_mbo module
*
* @return bool
+ *
+ * @throws ClientExceptionInterface
*/
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);
+ }
+
+ /**
+ * Enable ps_mbo module
+ *
+ * @return bool
+ *
+ * @throws \Exception
+ */
+ public function enableModule()
+ {
+ return $this->moduleManager->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()
{
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,
];
}