. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ use Doctrine\DBAL\Exception as DBALException; use Espo\Core\Application; use Espo\Core\Container; use Espo\Core\DataManager; use Espo\Core\Exceptions\Error; use Espo\Core\InjectableFactory; use Espo\Core\ORM\DatabaseParamsFactory; use Espo\Core\Utils\Database\ConfigDataProvider; use Espo\Core\Utils\Database\Dbal\ConnectionFactoryFactory; use Espo\Core\Utils\Id\RecordIdGenerator; use Espo\Core\Utils\ScheduledJob as ScheduledJobUtil; use Espo\Core\Utils\Util; use Espo\Core\Utils\Config\ConfigFileManager; use Espo\Core\Utils\Config; use Espo\Core\Utils\Config\ConfigWriter; use Espo\Core\Utils\Config\ConfigWriterFileManager; use Espo\Core\Utils\Database\Helper as DatabaseHelper; use Espo\Core\Utils\PasswordHash; use Espo\Core\Utils\SystemRequirements; use Espo\Core\Utils\Metadata; use Espo\Core\Utils\File\Manager as FileManager; use Espo\Core\Utils\Language; use Espo\Core\Binding\BindingContainerBuilder; use Espo\Core\ORM\EntityManager; use Espo\Entities\Job; use Espo\Entities\ScheduledJob; use Espo\Entities\User; use Espo\ORM\Query\SelectBuilder; use Espo\Tools\Installer\DatabaseConfigDataProvider; class Installer { private SystemHelper $systemHelper; private DatabaseHelper $databaseHelper; private InstallerConfig $installerConfig; private DatabaseParamsFactory $databaseParamsFactory; private ?Application $app = null; private ?Language $language = null; private ?PasswordHash $passwordHash; private bool $isAuth = false; private ?array $defaultSettings = null; private $permittedSettingList = [ 'dateFormat', 'timeFormat', 'timeZone', 'weekStart', 'defaultCurrency', 'language', 'thousandSeparator', 'decimalMark', 'outboundEmailFromName', 'outboundEmailFromAddress', 'outboundEmailIsShared', 'theme', ]; public function __construct( private Application\ApplicationParams $applicationParams = new Application\ApplicationParams(), ) { $this->initialize(); require_once('install/core/InstallerConfig.php'); $this->installerConfig = new InstallerConfig(); require_once('install/core/SystemHelper.php'); $this->systemHelper = new SystemHelper(); $this->databaseHelper = $this->getInjectableFactory()->create(DatabaseHelper::class); $this->databaseParamsFactory = $this->getInjectableFactory()->create(DatabaseParamsFactory::class); } private function initialize(): void { $fileManager = new ConfigFileManager(); $config = new Config($fileManager); $configPath = $config->getConfigPath(); if (!file_exists($configPath)) { $fileManager->putPhpContents($configPath, []); $config->update(); } $defaultData = include('application/Espo/Resources/defaults/config.php'); $configData = []; foreach (array_keys($defaultData) as $key) { if (!$config->has($key)) { continue; } $configData[$key] = $config->get($key); } $configWriterFileManager = new ConfigWriterFileManager( null, $config->get('defaultPermissions') ?? null ); $injectableFactory = (new Application($this->applicationParams)) ->getContainer() ->getByClass(InjectableFactory::class); $configWriter = $injectableFactory->createWithBinding( ConfigWriter::class, BindingContainerBuilder::create() ->bindInstance(Config::class, $config) ->bindInstance(ConfigWriterFileManager::class, $configWriterFileManager) ->build() ); // Save default data if it does not exist. if (!Util::arrayKeysExists(array_keys($defaultData), $configData)) { $defaultData = array_replace_recursive($defaultData, $configData); $configWriter->setMultiple($defaultData); $configWriter->save(); } $this->app = new Application($this->applicationParams); } private function getContainer(): Container { return $this->app->getContainer(); } private function getEntityManager(): EntityManager { return $this->getContainer()->getByClass(EntityManager::class); } public function getMetadata(): Metadata { return $this->app->getContainer()->getByClass(Metadata::class); } public function getInjectableFactory(): InjectableFactory { return $this->app->getContainer()->getByClass(InjectableFactory::class); } public function getConfig(): Config { return $this->app->getContainer()->getByClass(Config::class); } public function createConfigWriter(): ConfigWriter { return $this->getInjectableFactory()->create(ConfigWriter::class); } private function getSystemHelper(): SystemHelper { return $this->systemHelper; } private function getInstallerConfig(): InstallerConfig { return $this->installerConfig; } private function getFileManager(): FileManager { return $this->app->getContainer()->getByClass(FileManager::class); } private function getPasswordHash(): PasswordHash { if (!isset($this->passwordHash)) { $this->passwordHash = $this->getInjectableFactory()->create(PasswordHash::class); } return $this->passwordHash; } public function getVersion(): ?string { return $this->getConfig()->get('version'); } private function auth(): void { if (!$this->isAuth) { $this->app->setupSystemUser(); $this->isAuth = true; } } public function isInstalled(): bool { $installerConfig = $this->getInstallerConfig(); if ($installerConfig->get('isInstalled')) { return true; } return $this->app->isInstalled(); } public function createLanguage(string $language): Language { return $this->getInjectableFactory() ->createWith(Language::class, ['language' => $language]); } public function getLanguage(): Language { if (!isset($this->language)) { try { $language = $this->app->getContainer()->get('defaultLanguage'); if (!$language instanceof Language) { throw new RuntimeException("Can't get default language."); } $this->language = $language; } catch (Throwable $e) { echo "Error: " . $e->getMessage(); $GLOBALS['log']->error($e->getMessage()); die; } } return $this->language; } public function getThemeList(): array { return [ 'Espo', 'Dark', 'Light', 'Glass', 'Violet', 'Sakura', 'Hazyblue', ]; } public function getLanguageList($isTranslated = true): array { $languageList = $this->getMetadata()->get(['app', 'language', 'list']); if ($isTranslated) { return $this->getLanguage()->translate('language', 'options', 'Global', $languageList); } return $languageList; } private function getCurrencyList(): array { return $this->getMetadata()->get('app.currency.list'); } public function getInstallerConfigData() { return $this->getInstallerConfig()->getAllData(); } public function getSystemRequirementList($type, $requiredOnly = false, array $additionalData = null) { $platform = $additionalData['databaseParams']['platform'] ?? 'Mysql'; $dbConfigDataProvider = new DatabaseConfigDataProvider($platform); /** @var SystemRequirements $systemRequirementManager */ $systemRequirementManager = $this->app ->getContainer() ->getByClass(InjectableFactory::class) ->createWithBinding( SystemRequirements::class, BindingContainerBuilder::create() ->bindInstance(ConfigDataProvider::class, $dbConfigDataProvider) ->build() ); return $systemRequirementManager->getRequiredListByType($type, $requiredOnly, $additionalData); } /** * @throws DBALException * @throws Exception */ public function checkDatabaseConnection(array $rawParams, bool $createDatabase = false): void { $params = $this->databaseParamsFactory->createWithMergedAssoc($rawParams); $dbname = $params->getName(); try { $this->databaseHelper->createPDO($params); } catch (Exception $e) { if (!$createDatabase) { throw $e; } if ((int) $e->getCode() !== 1049) { throw $e; } /** @noinspection RegExpRedundantEscape */ if ($dbname !== preg_replace('/[^A-Za-z0-9_\-@$#\(\)]+/', '', $dbname)) { throw new Exception("Bad database name."); } $params = $params->withName(null); $pdo = $this->databaseHelper->createPDO($params); $connectionFactoryFactory = $this->getInjectableFactory()->create(ConnectionFactoryFactory::class); $connection = $connectionFactoryFactory ->create($params->getPlatform(), $pdo) ->create($params); $schemaManager = $connection->createSchemaManager(); $platform = $connection->getDatabasePlatform(); $schemaManager->createDatabase($platform->quoteIdentifier($dbname)); $this->checkDatabaseConnection($rawParams); } } /** * Save data. * * @param array $saveData * [ * 'driver' => 'pdo_mysql', * 'host' => 'localhost', * 'dbname' => 'espocrm_test', * 'user' => 'root', * 'password' => '', * ] * @return bool */ public function saveData(array $saveData) { $initData = include('install/core/afterInstall/config.php'); $databaseDefaults = $this->app ->getContainer() ->getByClass(Config::class) ->get('database'); $siteUrl = !empty($saveData['siteUrl']) ? $saveData['siteUrl'] : $this->getSystemHelper()->getBaseUrl(); $data = [ 'database' => array_merge($databaseDefaults, $saveData['database']), 'language' => $saveData['language'] ?? 'en_US', 'siteUrl' => $siteUrl, 'cryptKey' => Util::generateSecretKey(), 'hashSecretKey' => Util::generateSecretKey(), 'theme' => $saveData['theme'] ?? 'Espo', ]; if (empty($saveData['defaultPermissions']['user'])) { $saveData['defaultPermissions']['user'] = $this->getFileManager() ->getPermissionUtils() ->getDefaultOwner(true); } if (empty($saveData['defaultPermissions']['group'])) { $saveData['defaultPermissions']['group'] = $this->getFileManager() ->getPermissionUtils() ->getDefaultGroup(true); } if (!empty($saveData['defaultPermissions']['user'])) { $data['defaultPermissions']['user'] = $saveData['defaultPermissions']['user']; } if (!empty($saveData['defaultPermissions']['group'])) { $data['defaultPermissions']['group'] = $saveData['defaultPermissions']['group']; } return $this->saveConfig(array_merge($data, $initData)); } public function saveConfig($data) { $configWriter = $this->createConfigWriter(); $configWriter->setMultiple($data); $configWriter->save(); return true; } /** * @throws Error */ public function rebuild(): void { try { $this->app->getContainer()->getByClass(DataManager::class)->rebuild(); } catch (Exception) { $this->auth(); $this->app->getContainer()->getByClass(DataManager::class)->rebuild(); } } public function savePreferences(array $rawPreferences) { $preferences = $this->normalizeSettingParams($rawPreferences); $currencyList = $this->getConfig()->get('currencyList', []); if ( isset($preferences['defaultCurrency']) && !in_array($preferences['defaultCurrency'], $currencyList) ) { $preferences['currencyList'] = [$preferences['defaultCurrency']]; $preferences['baseCurrency'] = $preferences['defaultCurrency']; } return $this->saveConfig($preferences); } private function createRecords(): void { $records = include('install/core/afterInstall/records.php'); foreach ($records as $entityName => $recordList) { foreach ($recordList as $data) { $this->createRecord($entityName, $data); } } } private function createRecord(string $entityType, array $data): void { $id = $data['id'] ?? null; $entity = null; $em = $this->getEntityManager(); if ($id) { $entity = $em->getEntityById($entityType, $id); if (!$entity) { $selectQuery = $em->getQueryBuilder() ->select('id') ->from($entityType) ->withDeleted() ->where(['id' => $id]) ->build(); $entity = $em->getRDBRepository($entityType) ->clone($selectQuery) ->findOne(); if ($entity) { $updateQuery = $em->getQueryBuilder() ->update() ->in($entityType) ->set(['deleted' => false]) ->where(['id' => $id]) ->build(); $em->getQueryExecutor()->execute($updateQuery); $em->refreshEntity($entity); } } } if (!$entity) { if (isset($data['name'])) { $entity = $this->getEntityManager() ->getRDBRepository($entityType) ->where(['name' => $data['name']]) ->findOne(); } if (!$entity) { $entity = $this->getEntityManager()->getNewEntity($entityType); } } $entity->set($data); $this->getEntityManager()->saveEntity($entity); } public function createUser(string $userName, string $password): bool { $this->auth(); $password = $this->getPasswordHash()->hash($password); $user = $this->getEntityManager() ->getRDBRepositoryByClass(User::class) ->clone( SelectBuilder::create() ->from(User::ENTITY_TYPE) ->withDeleted() ->build() ) ->where(['userName' => $userName]) ->findOne(); if ($user) { $user->set([ 'password' => $password, 'deleted' => false, ]); $this->getEntityManager()->saveEntity($user); } if (!$user) { $id = $this->getInjectableFactory() ->createResolved(RecordIdGenerator::class) ->generate(); $this->createRecord(User::ENTITY_TYPE, [ 'id' => $id, 'userName' => $userName, 'password' => $password, 'lastName' => 'Admin', 'type' => User::TYPE_ADMIN, ]); } return true; } public function checkPermission(): bool { return $this->getFileManager()->getPermissionUtils()->setMapPermission(); } public function getLastPermissionError() { return $this->getFileManager()->getPermissionUtils()->getLastErrorRules(); } public function setSuccess(): void { $this->auth(); $this->createRecords(); $this->executeFinalScript(); $installerConfig = $this->getInstallerConfig(); $installerConfig->set('isInstalled', true); $installerConfig->save(); $configWriter = $this->createConfigWriter(); $configWriter->set('isInstalled', true); $configWriter->save(); } /** * @return array */ public function getDefaultSettings(): array { if (!$this->defaultSettings) { $settingDefs = $this->getMetadata()->get('entityDefs.Settings.fields'); $defaults = []; foreach ($this->permittedSettingList as $fieldName) { if (!isset($settingDefs[$fieldName])) { continue; } switch ($fieldName) { case 'defaultCurrency': $settingDefs['defaultCurrency']['options'] = $this->getCurrencyList(); break; case 'language': $settingDefs['language']['options'] = $this->getLanguageList(false); break; case 'theme': $settingDefs['theme']['options'] = $this->getThemeList(); break; } $defaults[$fieldName] = $this->translateSetting($fieldName, $settingDefs[$fieldName]); } $this->defaultSettings = $defaults; } return $this->defaultSettings; } private function normalizeSettingParams(array $params) { $defaultSettings = $this->getDefaultSettings(); $normalizedParams = []; foreach ($params as $name => $value) { if (!isset($defaultSettings[$name])) { continue; } $paramDefs = $defaultSettings[$name]; $paramType = $paramDefs['type'] ?? 'varchar'; switch ($paramType) { case 'enum': if ( isset($paramDefs['options']) && array_key_exists($value, $paramDefs['options']) || !isset($paramDefs['options']) ) { $normalizedParams[$name] = $value; } else if (array_key_exists('default', $paramDefs)) { $normalizedParams[$name] = $paramDefs['default']; $GLOBALS['log']->warning( 'Incorrect value ['. $value .'] for Settings parameter ['. $name .']. ' . 'Use default value ['. $paramDefs['default'] .'].' ); } break; case 'bool': $normalizedParams[$name] = (bool) $value; break; case 'int': case 'enumInt': $normalizedParams[$name] = (int) $value; break; case 'varchar': default: $normalizedParams[$name] = $value; break; } } return $normalizedParams; } private function translateSetting($name, array $settingDefs) { if (!isset($settingDefs['options'])) { return $settingDefs; } $translation = !empty($settingDefs['translation']) ? explode('.', $settingDefs['translation']) : null; $label = $translation[2] ?? $name; $category = $translation[1] ?? 'options'; $scope = $translation[0] ?? 'Settings'; $translatedOptions = $this->getLanguage() ->translate($label, $category, $scope, $settingDefs['options']); if ($translatedOptions == $name) { $translatedOptions = $this->getLanguage() ->translate($name, 'options', 'Global', $settingDefs['options']); } if ($translatedOptions == $name) { $translatedOptions = []; foreach ($settingDefs['options'] as $value) { $translatedOptions[$value] = $value; } } $settingDefs['options'] = $translatedOptions; return $settingDefs; } public function getCronMessage(): array { return $this->getInjectableFactory() ->create(ScheduledJobUtil::class) ->getSetupMessage(); } private function executeFinalScript(): void { $this->prepareDummyJob(); } private function prepareDummyJob(): void { $scheduledJob = $this->getEntityManager() ->getRDBRepository(ScheduledJob::ENTITY_TYPE) ->where(['job' => 'Dummy']) ->findOne(); if (!$scheduledJob) { return; } $this->getEntityManager()->createEntity(Job::ENTITY_TYPE, [ 'name' => 'Dummy', 'scheduledJobId' => $scheduledJob->getId(), ]); } public function getLogoSrc(string $theme): string { return $this->getMetadata()->get(['themes', $theme, 'logo']) ?? 'client/img/logo.svg'; } }