* @copyright Since 2007 PrestaShop SA and Contributors * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) */ namespace PrestaShop\PrestaShop\Adapter\Module\Configuration; use Doctrine\DBAL\Connection; use Exception; use PrestaShop\PrestaShop\Adapter\Configuration; use PrestaShop\PrestaShop\Core\Module\ModuleRepository; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; use Symfony\Component\Process\Exception\InvalidArgumentException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Yaml; /** * This class allow system users and developers to configure their module * with a single config file. * * Use validate() to check everything is ready to run. * Use configure() to run the configuration with the provided parameters. */ class ModuleSelfConfigurator { /** * @var string|null the module name */ protected $module; /** * @var string|null */ protected $configFile; /** * @var array */ protected $configs = []; /** * @var string */ protected $defaultConfigFile = 'self_config.yml'; /** * @var ModuleRepository */ protected $moduleRepository; /** * @var Configuration */ protected $configuration; /** * @var Connection */ protected $connection; /** * @var Filesystem */ protected $filesystem; public function __construct( ModuleRepository $moduleRepository, Configuration $configuration, Connection $connection, Filesystem $filesystem ) { $this->module = null; $this->configFile = null; $this->moduleRepository = $moduleRepository; $this->configuration = $configuration; $this->connection = $connection; $this->filesystem = $filesystem; } /** * Alias for $module setter. * * @param string $name * * @return $this */ public function module($name) { return $this->setModule($name); } /** * Set the module to be updated with its name. * * @param string $name * * @return $this * * @throws UnexpectedTypeException */ public function setModule($name) { if (!is_string($name)) { throw new UnexpectedTypeException($name, 'string'); } $this->module = $name; return $this; } /** * If defined, get the config file path or if possible, guess it. * * @return string|null * * @throws InvalidArgumentException */ public function getFile() { // If set, return it if ($this->configFile) { return $this->configFile; } // If we do not know in which module to search, we cannot go further if (!$this->module) { return null; } // Find and store the first config file we find $files = Finder::create() ->files() ->in(_PS_MODULE_DIR_ . $this->module) ->name($this->defaultConfigFile); foreach ($files as $file) { $this->configFile = $file->getRealPath(); return $this->configFile; } return null; } /** * Alias for config file setter. * * @param string $filepath * * @return $this */ public function file($filepath) { return $this->setFile($filepath); } /** * Set the config file to parse. * * @param string $filepath * * @return $this * * @throws UnexpectedTypeException */ public function setFile($filepath) { if (!is_string($filepath)) { throw new UnexpectedTypeException($filepath, 'string'); } $this->configFile = $filepath; return $this; } /** * In order to prevent some failure, we can check all pre-requesites are respected. * Any error will be reported in the array. * * @return array */ public function validate() { $errors = []; if ($this->module === null) { $errors[] = 'Module name not specified'; } try { $file = $this->getFile(); } catch (InvalidArgumentException $e) { $errors[] = $e->getMessage(); $file = null; } if ($file === null) { $errors[] = 'No config file to apply'; } elseif (!file_exists($file)) { $errors[] = 'Specified config file is not found'; } else { try { $config = $this->loadYmlFile($file); } catch (ParseException $e) { $errors[] = $e->getMessage(); } if (empty($config)) { $errors[] = 'Parsed config file is empty'; } } if (!$this->module || !$this->moduleRepository->getModule($this->module)->hasValidInstance()) { $errors[] = 'The module specified is invalid'; } return $errors; } /** * Launch the self configuration with all the context previously set! * * @return bool */ public function configure() { if (count($this->validate())) { return false; } $config = $this->loadYmlFile($this->getFile()); $this->runConfigurationStep($config); $this->runFilesStep($config); $this->runSqlStep($config); $this->runPhpStep($config); return true; } // PROTECTED ZONE /** * Helper function which adds the relative path from the YML config file. * Do not alter URLs. * * @param string $file * * @return string */ protected function convertRelativeToAbsolutePaths($file) { // If we do not deal with any kind of URL, add the path to the YML config file if (!filter_var($file, FILTER_VALIDATE_URL)) { $file = dirname($this->getFile()) . '/' . $file; } return $file; } /** * Finds and returns filepath from a config key in the YML config file. * Can be a string of a value of "file" key. * * @param array|string $data * * @return string * * @throws Exception if file data not provided */ protected function extractFilePath($data) { if (is_scalar($data)) { $file = $data; } elseif (is_array($data) && !empty($data['file'])) { $file = $data['file']; } else { throw new Exception('Missing file path'); } return $this->convertRelativeToAbsolutePaths($file); } /** * Require a PHP file and instanciate the class of the same name in it. * * @param string $file * * @return object */ protected function loadPhpFile($file) { // Load file require_once $file; // Load class of same name as the file $className = pathinfo($file, PATHINFO_FILENAME); return new $className(); } /** * Parse and return the YML content. * * @param string $file * * @return array */ protected function loadYmlFile($file) { if (array_key_exists($file, $this->configs)) { return $this->configs[$file]; } $this->configs[$file] = Yaml::parse(file_get_contents($file)); return $this->configs[$file]; } /** * Run configuration for "configuration" step. * * @param array $config */ protected function runConfigurationStep($config) { if (empty($config['configuration'])) { return; } if (array_key_exists('update', $config['configuration'])) { $this->runConfigurationUpdate($config['configuration']['update']); } if (array_key_exists('delete', $config['configuration'])) { $this->runConfigurationDelete($config['configuration']['delete']); } } /** * Run configuration for "file" step. * * @param array $config */ protected function runFilesStep($config) { if (empty($config['files'])) { return; } foreach ($config['files'] as $copy) { if (empty($copy['source'])) { throw new Exception('Missing source file'); } if (empty($copy['dest'])) { throw new Exception('Missing destination file'); } // If we get a relative path from the yml, add the original path foreach (['source', 'dest'] as $prop) { $copy[$prop] = $this->convertRelativeToAbsolutePaths($copy[$prop]); } $this->filesystem->copy( $copy['source'], $copy['dest'] ); } } /** * Run configuration for "php" step. * * @param array $config */ protected function runPhpStep($config) { if (empty($config['php'])) { return; } foreach ($config['php'] as $data) { $file = $this->extractFilePath($data); $module = $this->moduleRepository->getModule($this->module); $params = !empty($data['params']) ? $data['params'] : []; $this->loadPhpFile($file)->run($module, $params); } } /** * Run configuration for "sql" step. * * @param array $config */ protected function runSqlStep($config) { if (empty($config['sql'])) { return; } // Avoid unconsistant state with transactions $this->connection->beginTransaction(); try { foreach ($config['sql'] as $data) { $this->runSqlFile($data); } $this->connection->commit(); } catch (Exception $e) { $this->connection->rollBack(); throw $e; } } /** * Subtask of Sql step. Get and prepare all SQL requests from a file. * * @param array $data */ protected function runSqlFile($data) { $content = file_get_contents($this->extractFilePath($data)); foreach (explode(';', $content) as $sql) { $sql = trim($sql); if (empty($sql)) { continue; } // Set _DB_PREFIX_ $sql = str_replace( [ 'PREFIX_', 'DB_NAME', ], [ $this->configuration->get('_DB_PREFIX_'), $this->configuration->get('_DB_NAME_'), ], $sql ); $stmt = $this->connection->prepare($sql); $stmt->execute(); } } /** * Subtask of configuration step, for all configuration key to update. * * @param array $config * * @throws Exception */ protected function runConfigurationUpdate($config) { foreach ($config as $key => $data) { if (is_array($data) && isset($data['value'])) { $value = $data['value']; } elseif (is_scalar($data)) { // string / integer / decimal / bool $value = $data; } else { throw new Exception(sprintf('No value given for key %s', $key)); } $this->configuration->set($key, $value); } } /** * Subtask of configuration step, for all configuration keys to delete. * * @param array $config */ protected function runConfigurationDelete($config) { foreach ($config as $key) { $this->configuration->remove($key); } } }