Initial commit

This commit is contained in:
root
2026-01-19 17:44:46 +01:00
commit 823af8b11d
8721 changed files with 1130846 additions and 0 deletions

757
install/core/Installer.php Normal file
View File

@@ -0,0 +1,757 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* 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<string, mixed> $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<string, mixed>
*/
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';
}
}