diff --git a/application/Espo/Controllers/EntityManager.php b/application/Espo/Controllers/EntityManager.php index 4a29e8c7fe..4db088f53d 100644 --- a/application/Espo/Controllers/EntityManager.php +++ b/application/Espo/Controllers/EntityManager.php @@ -41,6 +41,7 @@ use Espo\Core\{ Api\Request, DataManager, Utils\Config, + Utils\Config\ConfigWriter, }; class EntityManager @@ -49,13 +50,20 @@ class EntityManager protected $dataManager; protected $config; protected $entityManagerTool; + protected $configWriter; - public function __construct(User $user, DataManager $dataManager, Config $config, EntityManagerTool $entityManagerTool) - { + public function __construct( + User $user, + DataManager $dataManager, + Config $config, + EntityManagerTool $entityManagerTool, + ConfigWriter $configWriter + ) { $this->user = $user; $this->dataManager = $dataManager; $this->config = $config; $this->entityManagerTool = $entityManagerTool; + $this->configWriter = $configWriter; $this->checkControllerAccess(); } @@ -131,8 +139,10 @@ class EntityManager if (!in_array($name, $tabList)) { $tabList[] = $name; - $this->config->set('tabList', $tabList); - $this->config->save(); + + $this->configWriter->set('tabList', $tabList); + + $this->configWriter->save(); } $this->dataManager->rebuild(); @@ -184,15 +194,19 @@ class EntityManager if ($result) { $tabList = $this->config->get('tabList', []); + if (($key = array_search($name, $tabList)) !== false) { unset($tabList[$key]); $tabList = array_values($tabList); } - $this->config->set('tabList', $tabList); - $this->config->save(); + + $this->configWriter->set('tabList', $tabList); + + $this->configWriter->save(); $this->dataManager->clearCache(); - } else { + } + else { throw new Error(); } @@ -206,11 +220,11 @@ class EntityManager $data = get_object_vars($data); $paramList = [ - 'entity', - 'link', - 'linkForeign', - 'label', - 'linkType', + 'entity', + 'link', + 'linkForeign', + 'label', + 'linkType', ]; $additionalParamList = [ @@ -222,10 +236,11 @@ class EntityManager $params = []; foreach ($paramList as $item) { - if (empty($data[$item])) { - throw new BadRequest(); - } - $params[$item] = filter_var($data[$item], \FILTER_SANITIZE_STRING); + if (empty($data[$item])) { + throw new BadRequest(); + } + + $params[$item] = filter_var($data[$item], \FILTER_SANITIZE_STRING); } foreach ($additionalParamList as $item) { @@ -272,12 +287,12 @@ class EntityManager $data = get_object_vars($data); $paramList = [ - 'entity', - 'entityForeign', - 'link', - 'linkForeign', - 'label', - 'labelForeign', + 'entity', + 'entityForeign', + 'link', + 'linkForeign', + 'label', + 'labelForeign', ]; $additionalParamList = []; @@ -331,8 +346,8 @@ class EntityManager $data = get_object_vars($data); $paramList = [ - 'entity', - 'link', + 'entity', + 'link', ]; $d = []; @@ -382,6 +397,7 @@ class EntityManager } $this->entityManagerTool->resetToDefaults($data->scope); + $this->dataManager->clearCache(); return true; diff --git a/application/Espo/Controllers/Integration.php b/application/Espo/Controllers/Integration.php index 837fc63460..d9f6b31995 100644 --- a/application/Espo/Controllers/Integration.php +++ b/application/Espo/Controllers/Integration.php @@ -29,55 +29,57 @@ namespace Espo\Controllers; -use Espo\Core\Exceptions\Forbidden; -use Espo\Core\Exceptions\BadRequest; +use Espo\Core\{ + Exceptions\Forbidden, + ServiceFactory, + Api\Request, +}; -class Integration extends \Espo\Core\Controllers\Record +use Espo\{ + Entities\User, +}; + +class Integration { + protected $serviceFactory; + + protected $user; + + public function __construct(ServiceFactory $serviceFactory, User $user) + { + $this->serviceFactory = $serviceFactory; + $this->user = $user; + + $this->checkControllerAccess(); + } + protected function checkControllerAccess() { - if (!$this->getUser()->isAdmin()) { + if (!$this->user->isAdmin()) { throw new Forbidden(); } } - public function actionIndex($params, $data, $request) + public function getActionRead(Request $request) { - return false; + $entity = $this->serviceFactory + ->create('Integration') + ->read($request->getRouteParam('id')); + + return $entity->getValueMap(); } - public function actionRead($params, $data, $request) + public function putActionUpdate(Request $request) { - $entity = $this->getEntityManager()->getEntity('Integration', $params['id']); - return $entity->toArray(); + return $this->patchActionPatch($request); } - public function actionUpdate($params, $data, $request) + public function patchActionPatch(Request $request) { - return $this->actionPatch($params, $data, $request); - } + $entity = $this->serviceFactory + ->create('Integration') + ->update($request->getRouteParam('id'), $request->getParsedBody()); - public function actionPatch($params, $data, $request) - { - if (!$request->isPut() && !$request->isPatch()) { - throw new BadRequest(); - } - $entity = $this->getEntityManager()->getEntity('Integration', $params['id']); - $entity->set($data); - $this->getEntityManager()->saveEntity($entity); - - $integrationsConfigData = $this->getConfig()->get('integrations'); - - if (!$integrationsConfigData || !($integrationsConfigData instanceof \StdClass)) { - $integrationsConfigData = (object)[]; - } - $integrationName = $params['id']; - - $integrationsConfigData->$integrationName = $entity->get('enabled'); - $this->getConfig()->set('integrations', $integrationsConfigData); - - $this->getConfig()->save(); - - return $entity->toArray(); + return $entity->getValueMap(); } } diff --git a/application/Espo/Core/DataManager.php b/application/Espo/Core/DataManager.php index 682f0aaa81..6a8c7ed986 100644 --- a/application/Espo/Core/DataManager.php +++ b/application/Espo/Core/DataManager.php @@ -35,6 +35,7 @@ use Espo\Core\{ Utils\Metadata, Utils\Util, Utils\Config, + Utils\Config\ConfigWriter, Utils\File\Manager as FileManager, Utils\Metadata\OrmMetadataData, HookManager, @@ -50,6 +51,7 @@ use Throwable; class DataManager { protected $config; + protected $configWriter; protected $entityManager; protected $fileManager; protected $metadata; @@ -62,6 +64,7 @@ class DataManager public function __construct( EntityManager $entityManager, Config $config, + ConfigWriter $configWriter, FileManager $fileManager, Metadata $metadata, OrmMetadataData $ormMetadataData, @@ -70,6 +73,7 @@ class DataManager ) { $this->entityManager = $entityManager; $this->config = $config; + $this->configWriter = $configWriter; $this->fileManager = $fileManager; $this->metadata = $metadata; $this->ormMetadataData = $ormMetadataData; @@ -125,8 +129,8 @@ class DataManager $result = false; $GLOBALS['log']->error( - 'Fault to rebuild database schema. Details: '. $e->getMessage() . - ' at ' . $e->getFile() . ':' . $e->getLine() + "Fault to rebuild database schema. Details: ". $e->getMessage() . + " at " . $e->getFile() . ":" . $e->getLine() ); } @@ -134,21 +138,27 @@ class DataManager throw new Error("Error while rebuilding database. See log file for details."); } - $config = $this->config; - $databaseType = strtolower($schema->getDatabaseHelper()->getDatabaseType()); - if (!$config->get('actualDatabaseType') || $config->get('actualDatabaseType') != $databaseType) { - $config->set('actualDatabaseType', $databaseType); + if ( + !$this->config->get('actualDatabaseType') || + $this->config->get('actualDatabaseType') !== $databaseType + ) { + $this->configWriter->set('actualDatabaseType', $databaseType); } $databaseVersion = $schema->getDatabaseHelper()->getDatabaseVersion(); - if (!$config->get('actualDatabaseVersion') || $config->get('actualDatabaseVersion') != $databaseVersion) { - $config->set('actualDatabaseVersion', $databaseVersion); + if ( + !$this->config->get('actualDatabaseVersion') || + $this->config->get('actualDatabaseVersion') !== $databaseVersion + ) { + $this->configWriter->set('actualDatabaseVersion', $databaseVersion); } - $this->updateCacheTimestamp(); + $this->configWriter->updateCacheTimestamp(); + + $this->configWriter->save(); } /** @@ -259,10 +269,9 @@ class DataManager */ public function updateCacheTimestamp() { - $this->config->updateCacheTimestamp(); + $this->configWriter->updateCacheTimestamp(); - /* Fix rebuildDatabase() method when remove this line. */ - $this->config->save(); + $this->configWriter->save(); } protected function populateConfigParameters() @@ -270,15 +279,13 @@ class DataManager $this->setFullTextConfigParameters(); $this->setCryptKeyConfigParameter(); - $this->config->save(); + $this->configWriter->save(); } protected function setFullTextConfigParameters() { - $config = $this->config; - - $platform = $config->get('database.platform') ?? null; - $driver = $config->get('database.driver') ?? ''; + $platform = $this->config->get('database.platform') ?? null; + $driver = $this->config->get('database.driver') ?? ''; if ($platform !== 'Mysql' && strpos($driver, 'mysql') === false) { return; @@ -300,7 +307,7 @@ class DataManager $fullTextSearchMinLength = intval($row['Value']); } - $config->set('fullTextSearchMinLength', $fullTextSearchMinLength); + $this->configWriter->set('fullTextSearchMinLength', $fullTextSearchMinLength); } protected function setCryptKeyConfigParameter() @@ -311,7 +318,7 @@ class DataManager $cryptKey = Util::generateSecretKey(); - $this->config->set('cryptKey', $cryptKey); + $this->configWriter->set('cryptKey', $cryptKey); } protected function disableHooks() diff --git a/application/Espo/Core/Portal/Application.php b/application/Espo/Core/Portal/Application.php index 388e4f73e9..aa7ec2a356 100644 --- a/application/Espo/Core/Portal/Application.php +++ b/application/Espo/Core/Portal/Application.php @@ -77,7 +77,10 @@ class Application extends BaseApplication $portal = $entityManager->getEntity('Portal', $portalId); if (!$portal) { - $portal = $entityManager->getRepository('Portal')->where(['customId' => $portalId])->findOne(); + $portal = $entityManager + ->getRepository('Portal') + ->where(['customId' => $portalId]) + ->findOne(); } if (!$portal) { diff --git a/application/Espo/Core/Portal/Container.php b/application/Espo/Core/Portal/Container.php index 9dc51248c5..fa78fa1828 100644 --- a/application/Espo/Core/Portal/Container.php +++ b/application/Espo/Core/Portal/Container.php @@ -31,18 +31,31 @@ namespace Espo\Core\Portal; use Espo\Entities\Portal as PortalEntity; -use Espo\Core\Container as BaseContainer; +use Espo\Core\{ + Container as BaseContainer, + Exceptions\Error, +}; class Container extends BaseContainer { + protected $portalIsSet = false; + public function setPortal(PortalEntity $portal) { + if ($this->portalIsSet) { + throw new Error("Can't set portal second time."); + } + + $this->portalIsSet = true; + $this->setForced('portal', $portal); $data = []; + foreach ($this->get('portal')->getSettingsAttributeList() as $attribute) { $data[$attribute] = $this->get('portal')->get($attribute); } + $this->get('config')->setPortalParameters($data); $this->get('aclManager')->setPortal($portal); diff --git a/application/Espo/Core/Portal/Utils/Config.php b/application/Espo/Core/Portal/Utils/Config.php index 1f4918d921..33a8ece659 100644 --- a/application/Espo/Core/Portal/Utils/Config.php +++ b/application/Espo/Core/Portal/Utils/Config.php @@ -29,37 +29,103 @@ namespace Espo\Core\Portal\Utils; -use Espo\Core\Utils\Config as BaseConfig; +use Espo\Core\{ + Exceptions\Error, + Utils\Config as BaseConfig, +}; class Config extends BaseConfig { + protected $portalParamsSet = false; + + protected $portalData = []; + + protected $portalParamList = [ + 'companyLogoId', + 'tabList', + 'quickCreateList', + 'dashboardLayout', + 'dashletsOptions', + 'theme', + 'language', + 'timeZone', + 'dateFormat', + 'timeFormat', + 'weekStart', + 'defaultCurrency', + ]; + + public function get(string $name, $default = null) + { + if (array_key_exists($name, $this->portalData)) { + return $this->portalData[$name]; + } + + return parent::get($name, $default); + } + + public function has(string $name) : bool + { + if (array_key_exists($name, $this->portalData)) { + return true; + } + + return parent::has($name); + } + /** - * Override parameters for a portal. + * Override parameters for a portal. Can be called only once. */ public function setPortalParameters(array $data = []) { - if (empty($data['language'])) unset($data['language']); - if (empty($data['theme'])) unset($data['theme']); - if (empty($data['timeZone'])) unset($data['timeZone']); - if (empty($data['dateFormat'])) unset($data['dateFormat']); - if (empty($data['timeFormat'])) unset($data['timeFormat']); - if (empty($data['defaultCurrency'])) unset($data['defaultCurrency']); - if (isset($data['weekStart']) && $data['weekStart'] === -1) unset($data['weekStart']); - if (array_key_exists('weekStart', $data) && is_null($data['weekStart'])) unset($data['weekStart']); + if ($this->portalParamsSet) { + throw new Error("Can't set portal params second time."); + } + + $this->portalParamsSet = true; + + if (empty($data['language'])) { + unset($data['language']); + } + + if (empty($data['theme'])) { + unset($data['theme']); + } + + if (empty($data['timeZone'])) { + unset($data['timeZone']); + } + + if (empty($data['dateFormat'])) { + unset($data['dateFormat']); + } + + if (empty($data['timeFormat'])) { + unset($data['timeFormat']); + } + + if (empty($data['defaultCurrency'])) { + unset($data['defaultCurrency']); + } + + if (isset($data['weekStart']) && $data['weekStart'] === -1) { + unset($data['weekStart']); + } + + if (array_key_exists('weekStart', $data) && is_null($data['weekStart'])) { + unset($data['weekStart']); + } if ($this->get('webSocketInPortalDisabled')) { - $this->set('useWebSocket', false, true); + $this->portalData['useWebSocket'] = false; } foreach ($data as $attribute => $value) { - $this->set($attribute, $value, true); + if (!in_array($attribute, $this->portalParamList)) { + continue; + } + + $this->portalData[$attribute] = $value; } } - - /** - * Save is disabled. - */ - public function save() - { - } } diff --git a/application/Espo/Core/Upgrades/Actions/Base.php b/application/Espo/Core/Upgrades/Actions/Base.php index 98a05a6c64..efddabe62f 100644 --- a/application/Espo/Core/Upgrades/Actions/Base.php +++ b/application/Espo/Core/Upgrades/Actions/Base.php @@ -34,6 +34,14 @@ use Espo\Core\Utils\System; use Espo\Core\Utils\Json; use Espo\Core\Exceptions\Error; +use Espo\Core\{ + Container, + Upgrades\ActionManager, + Utils\File\ZipArchive, + Utils\Config\ConfigWriter, + Utils\Database\Helper as DatabaseHelper, +}; + use Composer\Semver\Semver; use Throwable; @@ -89,13 +97,13 @@ abstract class Base protected $vendorDirName = 'vendor'; - public function __construct(\Espo\Core\Container $container, \Espo\Core\Upgrades\ActionManager $actionManager) + public function __construct(Container $container, ActionManager $actionManager) { $this->container = $container; $this->actionManager = $actionManager; $this->params = $actionManager->getParams(); - $this->zipUtil = new \Espo\Core\Utils\File\ZipArchive($container->get('fileManager')); + $this->zipUtil = new ZipArchive($container->get('fileManager')); } public function __destruct() @@ -136,7 +144,7 @@ abstract class Base protected function getDatabaseHelper() { if (!isset($this->databaseHelper)) { - $this->databaseHelper = new \Espo\Core\Utils\Database\Helper($this->getConfig()); + $this->databaseHelper = new DatabaseHelper($this->getConfig()); } return $this->databaseHelper; @@ -157,6 +165,11 @@ abstract class Base return $this->getContainer()->get('entityManager'); } + public function createConfigWriter() : ConfigWriter + { + return $this->getContainer()->get('injectableFactory')->create(ConfigWriter::class); + } + public function throwErrorAndRemovePackage($errorMessage = '', $deletePackage = true, $systemRebuild = true) { if ($deletePackage) { @@ -224,7 +237,10 @@ abstract class Base //check php version if (isset($manifest['php'])) { - $res &= $this->checkVersions($manifest['php'], System::getPhpVersion(), 'Your PHP version ({version}) is not supported. Required version: {requiredVersion}.'); + $res &= $this->checkVersions( + $manifest['php'], System::getPhpVersion(), + 'Your PHP version ({version}) is not supported. Required version: {requiredVersion}.' + ); } //check database version @@ -235,15 +251,25 @@ abstract class Base if (isset($manifest['database'][$databaseTypeLc])) { $databaseVersion = $databaseHelper->getDatabaseVersion(); + if ($databaseVersion) { - $res &= $this->checkVersions($manifest['database'][$databaseTypeLc], $databaseVersion, 'Your '. $databaseType .' version ({version}) is not supported. Required version: {requiredVersion}.'); + $res &= $this->checkVersions( + $manifest['database'][$databaseTypeLc], + $databaseVersion, + 'Your '. $databaseType . + ' version ({version}) is not supported. Required version: {requiredVersion}.' + ); } } } //check acceptableVersions if (isset($manifest['acceptableVersions'])) { - $res &= $this->checkVersions($manifest['acceptableVersions'], $this->getConfig()->get('version'), 'Your EspoCRM version ({version}) is not supported. Required version: {requiredVersion}.'); + $res &= $this->checkVersions( + $manifest['acceptableVersions'], + $this->getConfig()->get('version'), + 'Your EspoCRM version ({version}) is not supported. Required version: {requiredVersion}.' + ); } //check dependencies @@ -291,6 +317,7 @@ abstract class Base /** check package type */ $type = strtolower( $this->getParams('name') ); + $manifestType = isset($manifest['type']) ? strtolower($manifest['type']) : $this->defaultPackageType; if (!in_array($manifestType, $this->packageTypes)) { @@ -298,7 +325,9 @@ abstract class Base } if ($type != $manifestType) { - $this->throwErrorAndRemovePackage('Wrong package type. You cannot install '.$manifestType.' package via '.ucfirst($type).' Manager.'); + $this->throwErrorAndRemovePackage( + 'Wrong package type. You cannot install '.$manifestType.' package via '.ucfirst($type).' Manager.' + ); } return true; @@ -352,11 +381,13 @@ abstract class Base $scriptNames = $this->getParams('scriptNames'); $scriptName = $scriptNames[$type]; + if (!isset($scriptName)) { return; } $beforeInstallScript = Util::concatPath( array($packagePath, self::SCRIPTS, $scriptName) ) . '.php'; + if (file_exists($beforeInstallScript)) { return $beforeInstallScript; } @@ -411,13 +442,19 @@ abstract class Base protected function getDeleteFileList() { if (!isset($this->data['deleteFileList'])) { - $deleteFileList = array(); + $deleteFileList = []; + + $deleteList = array_merge( + $this->getDeleteList('delete'), + $this->getDeleteList('deleteBeforeCopy'), + $this->getDeleteList('vendor') + ); - $deleteList = array_merge($this->getDeleteList('delete'), $this->getDeleteList('deleteBeforeCopy'), $this->getDeleteList('vendor')); foreach ($deleteList as $key => $itemPath) { if (is_dir($itemPath)) { $fileList = $this->getFileManager()->getFileList($itemPath, true, '', true, true); $fileList = $this->concatStringWithArray($itemPath, $fileList); + $deleteFileList = array_merge($deleteFileList, $fileList); continue; @@ -452,6 +489,7 @@ abstract class Base { if (!isset($this->data['fileList'])) { $packagePath = $this->getPackagePath(); + $this->data['fileList'] = $this->getFileList($packagePath); } @@ -462,6 +500,7 @@ abstract class Base { if (!isset($this->data['restoreFileList'])) { $backupPath = $this->getPath('backupPath'); + $this->data['restoreFileList'] = $this->getFileList($backupPath, true); } @@ -478,6 +517,7 @@ abstract class Base protected function getFileDirs($parentDirPath = null) { $dirNames = $this->getParams('customDirNames'); + $paths = array(self::FILES, $dirNames['before'], $dirNames['after']); if (isset($parentDirPath)) { @@ -501,6 +541,7 @@ abstract class Base $fileList = array(); $paths = $this->getFileDirs($dirPath); + foreach ($paths as $filesPath) { if (file_exists($filesPath)) { $files = $this->getFileManager()->getFileList($filesPath, true, '', true, true); @@ -518,8 +559,9 @@ abstract class Base return $fileList; } - protected function copy($sourcePath, $destPath, $recursively = false, array $fileList = null, $copyOnlyFiles = false) - { + protected function copy( + $sourcePath, $destPath, $recursively = false, array $fileList = null, $copyOnlyFiles = false + ) { try { $res = $this->getFileManager()->copy($sourcePath, $destPath, $recursively, $fileList, $copyOnlyFiles); } @@ -565,18 +607,23 @@ abstract class Base case 'before': case 'after': $dirNames = $this->getParams('customDirNames'); + $dirPath = $dirNames[$type]; + break; case 'vendor': $dirNames = $this->getParams('customDirNames'); + if (isset($dirNames['vendor'])) { $dirPath = $dirNames['vendor']; } + break; default: $dirPath = self::FILES; + break; } @@ -596,11 +643,13 @@ abstract class Base $packagePath = $this->getPackagePath(); $dirNames = $this->getParams('customDirNames'); + if (!isset($dirNames['vendor'])) { return $list; } $filesPath = Util::concatPath($packagePath, $dirNames['vendor']); + if (!file_exists($filesPath)) { return $list; } @@ -608,10 +657,12 @@ abstract class Base switch ($type) { case 'copy': $list = $this->getFileManager()->getFileList($filesPath, true, '', true, true); + break; case 'delete': $list = $this->getFileManager()->getFileList($filesPath, false, '', null, true); + break; } @@ -628,11 +679,13 @@ abstract class Base $packagePath = $this->getPackagePath(); $manifestPath = Util::concatPath($packagePath, $this->manifestName); + if (!file_exists($manifestPath)) { $this->throwErrorAndRemovePackage('It\'s not an Installation package.'); } $manifestJson = $this->getFileManager()->getContents($manifestPath); + $this->data['manifest'] = Json::decode($manifestJson, true); if (!$this->data['manifest']) { @@ -686,7 +739,7 @@ abstract class Base } /** - * Unzip a package archieve + * Unzip a package archive. * * @return void */ @@ -706,7 +759,7 @@ abstract class Base } /** - * Delete temporary package files + * Delete temporary package files. * * @return boolean */ @@ -719,7 +772,7 @@ abstract class Base } /** - * Delete temporary package archive + * Delete temporary package archive. * * @return boolean */ @@ -750,7 +803,7 @@ abstract class Base } /** - * Execute an action. For ex., execute uninstall action in install + * Execute an action. For ex., execute uninstall action in install. * * @param string $actionName * @param string $data @@ -800,11 +853,15 @@ abstract class Base $fullFileList = array_merge([$backupPath], $this->getDeleteFileList(), $this->getCopyFileList()); $result = $this->getFileManager()->isWritableList($fullFileList); + if (!$result) { $permissionDeniedList = $this->getFileManager()->getLastPermissionDeniedList(); $delimiter = $this->isCli() ? "\n" : "
"; - $this->throwErrorAndRemovePackage("Permission denied: " . $delimiter . implode($delimiter, $permissionDeniedList), false, false); + + $this->throwErrorAndRemovePackage( + "Permission denied: " . $delimiter . implode($delimiter, $permissionDeniedList), false, false + ); } } @@ -842,6 +899,8 @@ abstract class Base protected function enableMaintenanceMode() { $config = $this->getConfig(); + $configWriter = $this->createConfigWriter(); + $configParamName = $this->getTemporaryConfigParamName(); $parentConfigParamName = $this->getTemporaryConfigParamName(true); @@ -855,36 +914,41 @@ abstract class Base 'useCache' => $config->get('useCache'), ]; - $config->set($configParamName, $actualParams); + // @todo Maybe to romove this line? + $configWriter->set($configParamName, $actualParams); $save = false; if (!$actualParams['maintenanceMode']) { - $config->set('maintenanceMode', true); + $configWriter->set('maintenanceMode', true); + $save = true; } if (!$actualParams['cronDisabled']) { - $config->set('cronDisabled', true); + $configWriter->set('cronDisabled', true); + $save = true; } if ($actualParams['useCache']) { - $config->set('useCache', false); + $configWriter->set('useCache', false); + $save = true; } if ($save) { - $config->save(); + $configWriter->save(); } } protected function disableMaintenanceMode($force = false) { $config = $this->getConfig(); + $configWriter = $this->createConfigWriter(); $configParamList = [ - $this->getTemporaryConfigParamName() + $this->getTemporaryConfigParamName(), ]; if ($force && $this->getTemporaryConfigParamName(true)) { @@ -901,16 +965,17 @@ abstract class Base foreach ($config->get($configParamName, []) as $paramName => $paramValue) { if ($config->get($paramName) != $paramValue) { - $config->set($paramName, $paramValue); + $configWriter->set($paramName, $paramValue); } } - $config->remove($configParamName); + $configWriter->remove($configParamName); + $save = true; } if ($save) { - $config->save(); + $configWriter->save(); } } @@ -920,6 +985,7 @@ abstract class Base if ($isParentProcess) { $processId = $this->getParentProcessId(); + if (!$processId) { return; } diff --git a/application/Espo/Core/Upgrades/Actions/Upgrade/Install.php b/application/Espo/Core/Upgrades/Actions/Upgrade/Install.php index 115e918cac..8ce9678b5a 100644 --- a/application/Espo/Core/Upgrades/Actions/Upgrade/Install.php +++ b/application/Espo/Core/Upgrades/Actions/Upgrade/Install.php @@ -45,8 +45,11 @@ class Install extends \Espo\Core\Upgrades\Actions\Base\Install { $manifest = $this->getManifest(); - $this->getConfig()->set('version', $manifest['version']); - $this->getConfig()->save(); + $configWriter = $this->createConfigWriter(); + + $configWriter->set('version', $manifest['version']); + + $configWriter->save(); } /** diff --git a/application/Espo/Core/Utils/ApiKey.php b/application/Espo/Core/Utils/ApiKey.php index b59224056c..f9bddf4eaf 100644 --- a/application/Espo/Core/Utils/ApiKey.php +++ b/application/Espo/Core/Utils/ApiKey.php @@ -29,15 +29,21 @@ namespace Espo\Core\Utils; -use Espo\Core\Utils\Config; +use Espo\Core\{ + Utils\Config, + Utils\Config\ConfigWriter, +}; class ApiKey { private $config; - public function __construct(Config $config) + private $configWriter; + + public function __construct(Config $config, ConfigWriter $configWriter) { $this->config = $config; + $this->configWriter = $configWriter; } public static function hash(string $secretKey, string $string = '') : string @@ -74,9 +80,9 @@ class ApiKey $apiSecretKeys->$id = $secretKey; - $this->config->set('apiSecretKeys', $apiSecretKeys); + $this->configWriter->set('apiSecretKeys', $apiSecretKeys); - $this->config->save(); + $this->configWriter->save(); } public function removeSecretKeyForUserId(string $id) @@ -89,7 +95,8 @@ class ApiKey unset($apiSecretKeys->$id); - $this->config->set('apiSecretKeys', $apiSecretKeys); - $this->config->save(); + $this->configWriter->set('apiSecretKeys', $apiSecretKeys); + + $this->configWriter->save(); } } diff --git a/application/Espo/Core/Utils/Config.php b/application/Espo/Core/Utils/Config.php index 058d9a2462..5227b7d60e 100644 --- a/application/Espo/Core/Utils/Config.php +++ b/application/Espo/Core/Utils/Config.php @@ -37,7 +37,7 @@ use Espo\Core\{ use StdClass; /** - * Reads and writes the main config file. + * Access to the application config parameters. */ class Config { @@ -69,6 +69,11 @@ class Config $this->fileManager = $fileManager; } + /** + * A path to the config file. + * + * @todo Move to ConfigData. + */ public function getConfigPath() : string { return $this->configPath; @@ -125,7 +130,18 @@ class Config } /** - * Set a parameter or multiple parameters. + * Re-load data. + * + * @todo Get rid of this method. Use ConfigData as a dependency. + * `$configData->update();` + */ + public function update() + { + $this->loadConfig(true); + } + + /** + * @deprecated since version 6.2.0 */ public function set($name, $value = null, bool $dontMarkDirty = false) { @@ -151,7 +167,7 @@ class Config } /** - * Remove a parameter.. + * @deprecated since version 6.2.0 */ public function remove(string $name) : bool { @@ -167,7 +183,7 @@ class Config } /** - * Save config changes to the file. + * @deprecated since version 6.2.0 */ public function save() { @@ -233,6 +249,9 @@ class Config /** * Get system default config parameters. + * + * @deprecated + * @todo Move to `Espo\Core\Utils\Config\ConfigDefaults`. */ public function getDefaults() : array { @@ -289,8 +308,7 @@ class Config /** * Update cache timestamp. * - * @param $returnOnlyValue - To return an array with timestamp. - * @return bool | array + * @deprecated */ public function updateCacheTimestamp(bool $returnOnlyValue = false) { @@ -305,31 +323,55 @@ class Config return $this->set($timestamp); } + /** + * @todo Remove. + * @deprecated + */ public function getAdminOnlyItemList() : array { return $this->get('adminItems', []); } + /** + * @todo Remove. + * @deprecated + */ public function getSuperAdminOnlyItemList() : array { return $this->get('superAdminItems', []); } + /** + * @todo Remove. + * @deprecated + */ public function getSystemOnlyItemList() : array { return $this->get('systemItems', []); } + /** + * @todo Remove. + * @deprecated + */ public function getSuperAdminOnlySystemItemList() : array { return $this->get('superAdminSystemItems', []); } + /** + * @todo Remove. + * @deprecated + */ public function getUserOnlyItemList() : array { return $this->get('userItems', []); } + /** + * @todo Move to another class `Espo\Core\Utils\Config\ApplicationConfigProvider`. + * @deprecated + */ public function getSiteUrl() : string { return rtrim($this->get('siteUrl'), '/'); diff --git a/application/Espo/Core/Utils/Config/ConfigFileManager.php b/application/Espo/Core/Utils/Config/ConfigFileManager.php index 5f28ea50f7..9f6d7c86be 100644 --- a/application/Espo/Core/Utils/Config/ConfigFileManager.php +++ b/application/Espo/Core/Utils/Config/ConfigFileManager.php @@ -34,6 +34,8 @@ use Espo\Core\{ Utils\Config, }; +use RuntimeException; + class ConfigFileManager { protected $fileManager; @@ -55,9 +57,23 @@ class ConfigFileManager return $this->fileManager->isFile($filePath); } - public function putPhpContents(string $path, $data, bool $useRenaming = false) + protected function putPhpContentsInternal(string $path, array $data, bool $useRenaming = false) { - return $this->fileManager->putPhpContents($path, $data, true, $useRenaming); + $result = $this->fileManager->putPhpContents($path, $data, true, $useRenaming); + + if ($result === false) { + throw new RuntimeException(); + } + } + + public function putPhpContents(string $path, array $data) + { + $this->putPhpContentsInternal($path, $data, true); + } + + public function putPhpContentsNoRenaming(string $path, array $data) + { + $this->putPhpContentsInternal($path, $data, false); } public function getPhpContents(string $path) diff --git a/application/Espo/Core/Utils/Config/ConfigWriter.php b/application/Espo/Core/Utils/Config/ConfigWriter.php new file mode 100644 index 0000000000..eb5e9b2392 --- /dev/null +++ b/application/Espo/Core/Utils/Config/ConfigWriter.php @@ -0,0 +1,182 @@ +config = $config; + $this->fileManager = $fileManager; + $this->helper = $helper; + } + + /** + * Set a parameter. + */ + public function set(string $name, $value) + { + if (in_array($name, $this->associativeArrayAttributeList) && is_object($value)) { + $value = (array) $value; + } + + $this->changedData[$name] = $value; + } + + /** + * Set multiple parameters. + */ + public function setMultiple(array $params) + { + foreach ($params as $name => $value) { + $this->set($name, $value); + } + } + + /** + * Remove a parameter. + */ + public function remove(string $name) + { + $this->removeParamList[] = $name; + } + + /** + * Save config changes to the file. + */ + public function save() + { + $changedData = $this->changedData; + + if (!isset($changedData[$this->cacheTimestampParam])) { + $changedData[$this->cacheTimestampParam] = $this->generateCacheTimestamp(); + } + + $configPath = $this->config->getConfigPath(); + + if (!$this->fileManager->isFile($configPath)) { + throw new Error("Config file '{$configPath}' was not found."); + } + + $data = $this->fileManager->getPhpContents($configPath); + + if (!is_array($data)) { + $data = $this->fileManager->getPhpContents($configPath); + } + + if (!is_array($data)) { + throw new Error("Could not read config."); + } + + foreach ($changedData as $key => $value) { + $data[$key] = $value; + } + + foreach ($this->removeParamList as $key) { + unset($data[$key]); + } + + if (!is_array($data)) { + throw new Error("Invalid config data while saving."); + } + + $data['microtime'] = $microtime = $this->helper->generateMicrotime(); + + try { + $this->fileManager->putPhpContents($configPath, $data); + } + catch (Exception $e) { + throw new Error("Could not save config."); + } + + $reloadedData = $this->fileManager->getPhpContents($configPath); + + if ( + !is_array($reloadedData) || + $microtime !== ($reloadedData['microtime'] ?? null) + ) { + try { + $this->fileManager->putPhpContentsNoRenaming($configPath, $data); + } + catch (Exception $e) { + throw new Error("Could not save config."); + } + } + + $this->changedData = []; + $this->removeParamList = []; + + $this->config->update(); + } + + /** + * Update the cache timestamp. + * + * @todo Remove? Saving re-writes the cache timestamp anyway. + */ + public function updateCacheTimestamp() + { + $this->set($this->cacheTimestampParam, $this->generateCacheTimestamp()); + } + + protected function generateCacheTimestamp() : int + { + return $this->helper->generateCacheTimestamp(); + } +} diff --git a/application/Espo/Core/Utils/Config/ConfigWriterFileManager.php b/application/Espo/Core/Utils/Config/ConfigWriterFileManager.php new file mode 100644 index 0000000000..b74e270bba --- /dev/null +++ b/application/Espo/Core/Utils/Config/ConfigWriterFileManager.php @@ -0,0 +1,93 @@ +get('defaultPermissions'); + } + + $this->fileManager = new FileManager($defaultPermissionsToSet); + } + + public function setConfig(Config $config) + { + $this->fileManager = new FileManager( + $config->get('defaultPermissions') + ); + } + + public function isFile(string $filePath) : bool + { + return $this->fileManager->isFile($filePath); + } + + protected function putPhpContentsInternal(string $path, array $data, bool $useRenaming = false) + { + $result = $this->fileManager->putPhpContents($path, $data, true, $useRenaming); + + if ($result === false) { + throw new RuntimeException(); + } + } + + public function putPhpContents(string $path, array $data) + { + $this->putPhpContentsInternal($path, $data, true); + } + + public function putPhpContentsNoRenaming(string $path, array $data) + { + $this->putPhpContentsInternal($path, $data, false); + } + + public function getPhpContents(string $path) + { + return $this->fileManager->getPhpContents($path); + } +} + diff --git a/application/Espo/Core/Utils/Config/ConfigWriterHelper.php b/application/Espo/Core/Utils/Config/ConfigWriterHelper.php new file mode 100644 index 0000000000..3c761e98ed --- /dev/null +++ b/application/Espo/Core/Utils/Config/ConfigWriterHelper.php @@ -0,0 +1,43 @@ +settingsAttributeList; } - } diff --git a/application/Espo/Hooks/Integration/GoogleMaps.php b/application/Espo/Hooks/Integration/GoogleMaps.php index d823927346..6c611136cf 100644 --- a/application/Espo/Hooks/Integration/GoogleMaps.php +++ b/application/Espo/Hooks/Integration/GoogleMaps.php @@ -32,28 +32,34 @@ namespace Espo\Hooks\Integration; use Espo\ORM\Entity; use Espo\Core\{ - Utils\Config, + Utils\Config\ConfigWriter, }; class GoogleMaps { - protected $config; + protected $configWriter; - public function __construct(Config $config) + public function __construct(ConfigWriter $configWriter) { - $this->config = $config; + $this->configWriter = $configWriter; } public function afterSave(Entity $entity) { - if ($entity->id === 'GoogleMaps') { - if (!$entity->get('enabled') || !$entity->get('apiKey')) { - $this->config->set('googleMapsApiKey', null); - $this->config->save(); - return; - } - $this->config->set('googleMapsApiKey', $entity->get('apiKey')); - $this->config->save(); + if ($entity->id !== 'GoogleMaps') { + return; } + + if (!$entity->get('enabled') || !$entity->get('apiKey')) { + $this->configWriter->set('googleMapsApiKey', null); + + $this->configWriter->save(); + + return; + } + + $this->configWriter->set('googleMapsApiKey', $entity->get('apiKey')); + + $this->configWriter->save(); } } diff --git a/application/Espo/Hooks/Portal/WriteConfig.php b/application/Espo/Hooks/Portal/WriteConfig.php new file mode 100644 index 0000000000..1d20f4f64a --- /dev/null +++ b/application/Espo/Hooks/Portal/WriteConfig.php @@ -0,0 +1,76 @@ +config = $config; + $this->configWriter = $configWriter; + } + + public function afterSave(Entity $entity) + { + if (!$entity->has('isDefault')) { + return; + } + + if ($entity->get('isDefault')) { + $defaultPortalId = $this->config->get('defaultPortalId'); + + if ($defaultPortalId === $entity->id) { + return; + } + + $this->configWriter->set('defaultPortalId', $entity->id); + + $this->configWriter->save(); + } + + if ($entity->isAttributeChanged('isDefault') && $entity->getFetched('isDefault')) { + + $this->configWriter->set('defaultPortalId', null); + + $this->configWriter->save(); + } + } +} diff --git a/application/Espo/Repositories/Portal.php b/application/Espo/Repositories/Portal.php index 39b47333ce..9261ab33d3 100644 --- a/application/Espo/Repositories/Portal.php +++ b/application/Espo/Repositories/Portal.php @@ -31,11 +31,12 @@ namespace Espo\Repositories; use Espo\ORM\Entity; -use Espo\Core\Exceptions\Error; +use Espo\Core\Repositories\Database; use Espo\Core\Di; -class Portal extends \Espo\Core\Repositories\Database implements +class Portal extends Database implements + Di\ConfigAware { use Di\ConfigSetter; @@ -67,26 +68,4 @@ class Portal extends \Espo\Core\Repositories\Database implements $entity->set('url', $url); } } - - protected function afterSave(Entity $entity, array $options = []) - { - parent::afterSave($entity, $options); - - if ($entity->has('isDefault')) { - if ($entity->get('isDefault')) { - $defaultPortalId = $this->config->get('defaultPortalId'); - if ($defaultPortalId !== $entity->id) { - $this->config->set('defaultPortalId', $entity->id); - $this->config->save(); - } - } else { - if ($entity->isAttributeChanged('isDefault')) { - if ($entity->getFetched('isDefault')) { - $this->config->set('defaultPortalId', null); - $this->config->save(); - } - } - } - } - } } diff --git a/application/Espo/Services/AdminNotifications.php b/application/Espo/Services/AdminNotifications.php index 0b96d4387b..1ecd90eff4 100644 --- a/application/Espo/Services/AdminNotifications.php +++ b/application/Espo/Services/AdminNotifications.php @@ -29,16 +29,28 @@ namespace Espo\Services; -use Espo\Core\Di; +use Espo\Core\{ + Utils\Config, + Utils\Config\ConfigWriter, + Di, +}; class AdminNotifications implements - Di\ConfigAware, Di\EntityManagerAware { - use Di\ConfigSetter; use Di\EntityManagerSetter; + protected $config; + + protected $configWriter; + + public function __construct(Config $config, ConfigWriter $configWriter) + { + $this->config = $config; + $this->configWriter = $configWriter; + } + /** * Job for checking a new version of EspoCRM. */ @@ -51,32 +63,36 @@ class AdminNotifications implements } $latestRelease = $this->getLatestRelease(); + if (empty($latestRelease['version'])) { - $config->set('latestVersion', $latestRelease['version']); - $config->save(); + $this->configWriter->set('latestVersion', $latestRelease['version']); + + $this->configWriter->save(); + return true; } if ($config->get('latestVersion') != $latestRelease['version']) { - $config->set('latestVersion', $latestRelease['version']); + $this->configWriter->set('latestVersion', $latestRelease['version']); if (!empty($latestRelease['notes'])) { - //todo: create notification + // @todo Create a notification. } - $config->save(); + $this->configWriter->save(); + return true; } if (!empty($latestRelease['notes'])) { - //todo: find and modify notification + // @todo Find and modify notification. } return true; } /** - * Job for cheking a new version of installed extensions. + * Job for checking a new version of installed extensions. */ public function jobCheckNewExtensionVersion() { @@ -103,6 +119,7 @@ class AdminNotifications implements while ($row = $sth->fetch()) { $url = !empty($row['checkVersionUrl']) ? $row['checkVersionUrl'] : null; + $extensionName = $row['name']; $latestRelease = $this->getLatestRelease($url, [ @@ -117,11 +134,13 @@ class AdminNotifications implements $latestExtensionVersions = $config->get('latestExtensionVersions', []); $save = false; + foreach ($latestReleases as $extensionName => $extensionData) { if (empty($latestExtensionVersions[$extensionName])) { $latestExtensionVersions[$extensionName] = $extensionData['version']; $save = true; + continue; } @@ -133,6 +152,7 @@ class AdminNotifications implements } $save = true; + continue; } @@ -142,37 +162,51 @@ class AdminNotifications implements } if ($save) { - $config->set('latestExtensionVersions', $latestExtensionVersions); - $config->save(); + $this->configWriter->set('latestExtensionVersions', $latestExtensionVersions); + + $this->configWriter->save(); } return true; } - protected function getLatestRelease(?string $url = null, array $requestData = [], string $urlPath = 'release/latest') - { - if (function_exists('curl_version')) { - $ch = curl_init(); + protected function getLatestRelease( + ?string $url = null, array $requestData = [], string $urlPath = 'release/latest' + ) : ?array { - $requestUrl = $url ? trim($url) : base64_decode('aHR0cHM6Ly9zLmVzcG9jcm0uY29tLw=='); - $requestUrl = (substr($requestUrl, -1) == '/') ? $requestUrl : $requestUrl . '/'; - $requestUrl .= empty($requestData) ? $urlPath . '/' : $urlPath . '/?' . http_build_query($requestData); - - curl_setopt($ch, CURLOPT_URL, $requestUrl); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 60); - - $result = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - if ($httpCode === 200) { - $data = json_decode($result, true); - if (is_array($data)) { - return $data; - } - } + if (!function_exists('curl_version')) { + return null; } - return null; + + $ch = curl_init(); + + $requestUrl = $url ? trim($url) : base64_decode('aHR0cHM6Ly9zLmVzcG9jcm0uY29tLw=='); + $requestUrl = (substr($requestUrl, -1) == '/') ? $requestUrl : $requestUrl . '/'; + + $requestUrl .= empty($requestData) ? + $urlPath . '/' : + $urlPath . '/?' . http_build_query($requestData); + + curl_setopt($ch, CURLOPT_URL, $requestUrl); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 60); + + $result = curl_exec($ch); + + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + curl_close($ch); + + if ($httpCode !== 200) { + return null; + } + + $data = json_decode($result, true); + + if (!is_array($data)) { + return null; + } + + return $data; } } diff --git a/application/Espo/Services/CurrencyRate.php b/application/Espo/Services/CurrencyRate.php index 4ffd9115b9..a0d34f1be8 100644 --- a/application/Espo/Services/CurrencyRate.php +++ b/application/Espo/Services/CurrencyRate.php @@ -37,6 +37,7 @@ use Espo\Core\Exceptions\{ use Espo\Core\{ DataManager, Utils\Config, + Utils\Config\ConfigWriter, Acl, }; @@ -45,55 +46,81 @@ use StdClass; class CurrencyRate { protected $config; + protected $configWriter; protected $dataManager; protected $acl; - public function __construct(Config $config, DataManager $dataManager, Acl $acl) { + public function __construct(Config $config, ConfigWriter $configWriter, DataManager $dataManager, Acl $acl) + { $this->config = $config; + $this->configWriter = $configWriter; $this->dataManager = $dataManager; $this->acl = $acl; } public function get() : StdClass { - if (!$this->acl->check('Currency')) throw new Forbidden(); - if ($this->acl->getLevel('Currency', 'read') !== 'yes') throw new Forbidden(); + if (!$this->acl->check('Currency')) { + throw new Forbidden(); + } - return (object) ($this->config->get('currencyRates') ?? []); + if ($this->acl->getLevel('Currency', 'read') !== 'yes') { + throw new Forbidden(); + } + + return (object) ( + $this->config->get('currencyRates') ?? [] + ); } public function set(StdClass $rates) : StdClass { - if (!$this->acl->check('Currency')) throw new Forbidden(); - if ($this->acl->getLevel('Currency', 'edit') !== 'yes') throw new Forbidden(); + if (!$this->acl->check('Currency')) { + throw new Forbidden(); + } + + if ($this->acl->getLevel('Currency', 'edit') !== 'yes') { + throw new Forbidden(); + } $config = $this->config; + $currencyList = $config->get('currencyList') ?? []; foreach (get_object_vars($rates) as $key => $value) { if (!is_string($key) || !in_array($key, $currencyList)) { unset($rates->$key); + continue; } - if (!is_numeric($value) || is_string($value)) throw new BadRequest(); - if ($value < 0) throw new BadRequest(); + + if (!is_numeric($value) || is_string($value)) { + throw new BadRequest(); + } + + if ($value < 0) { + throw new BadRequest(); + } } foreach ($currencyList as $currency) { - if ($currency == $config->get('baseCurrency')) continue; + if ($currency == $config->get('baseCurrency')) { + continue; + } if (!isset($rates->$currency)) { $rates->$currency = $config->get('currencyRates.' . $currency) ?? 1.0; } } - $data = (object) ['currencyRates' => $rates]; + $this->configWriter->set('currencyRates', $rates); - $config->setData($data); - $config->save(); + $this->configWriter->save(); $this->dataManager->rebuildDatabase([]); - return (object) ($config->get('currencyRates') ?? []); + return (object) ( + $config->get('currencyRates') ?? [] + ); } } diff --git a/application/Espo/Services/Integration.php b/application/Espo/Services/Integration.php new file mode 100644 index 0000000000..4a53d5bea7 --- /dev/null +++ b/application/Espo/Services/Integration.php @@ -0,0 +1,113 @@ +entityManager = $entityManager; + $this->user = $user; + $this->config = $config; + $this->configWriter = $configWriter; + } + + protected function processAccessCheck() + { + if (!$this->user->isAdmin()) { + throw new Forbidden(); + } + } + + public function read(string $id) : Entity + { + $this->processAccessCheck(); + + $entity = $this->entityManager->getEntity('Integration', $id); + + if (!$entity) { + throw new NotFound(); + } + + return $entity; + } + + public function update(string $id, StdClass $data) : Entity + { + $this->processAccessCheck(); + + $entity = $this->entityManager->getEntity('Integration', $id); + + if (!$entity) { + throw new NotFound(); + } + + $entity->set($data); + + $this->entityManager->saveEntity($entity); + + $integrationsConfigData = $this->config->get('integrations') ?? (object) []; + + if (!($integrationsConfigData instanceof StdClass)) { + $integrationsConfigData = (object) []; + } + + $integrationName = $id; + + $integrationsConfigData->$integrationName = $entity->get('enabled'); + + $this->configWriter->set('integrations', $integrationsConfigData); + + $this->configWriter->save(); + + return $entity; + } +} diff --git a/application/Espo/Services/Settings.php b/application/Espo/Services/Settings.php index 85eb3d844f..218bde5e43 100644 --- a/application/Espo/Services/Settings.php +++ b/application/Espo/Services/Settings.php @@ -30,7 +30,6 @@ namespace Espo\Services; use Espo\Core\Exceptions\Forbidden; -use Espo\Core\Exceptions\Error; use Espo\Core\Exceptions\BadRequest; use Espo\ORM\Entity; @@ -43,6 +42,7 @@ use Espo\Core\{ Utils\Metadata, Utils\FieldUtil, Utils\Config, + Utils\Config\ConfigWriter, DataManager, Utils\FieldValidatorManager, Currency\DatabasePopulator as CurrencyDatabasePopulator, @@ -52,6 +52,7 @@ class Settings { protected $applicationState; protected $config; + protected $configWriter; protected $fieldUtil; protected $metadata; protected $acl; @@ -63,6 +64,7 @@ class Settings public function __construct( ApplicationState $applicationState, Config $config, + ConfigWriter $configWriter, Metadata $metadata, Acl $acl, FieldUtil $fieldUtil, @@ -73,6 +75,7 @@ class Settings ) { $this->applicationState = $applicationState; $this->config = $config; + $this->configWriter = $configWriter; $this->metadata = $metadata; $this->acl = $acl; $this->fieldUtil = $fieldUtil; @@ -114,7 +117,9 @@ class Settings if ($this->applicationState->isPortal()) { $portal = $this->applicationState->getPortal(); + $this->entityManager->getRepository('Portal')->loadUrlField($portal); + $data->siteUrl = $portal->get('url'); } @@ -124,6 +129,7 @@ class Settings if ($user->isSystem()) { $globalItemList = $this->getGlobalItemList(); + foreach (get_object_vars($data) as $item => $value) { if (!in_array($item, $globalItemList)) { unset($data->$item); @@ -145,21 +151,30 @@ class Settings 'streamEmailNotificationsTypeList', 'emailKeepParentTeamsEntityList', ]; - $scopeList = array_keys($this->metadata->get(['entityDefs'], [])); - foreach ($scopeList as $scope) { - if (!$this->metadata->get(['scopes', $scope, 'acl'])) continue; - if (!$this->acl->check($scope)) { - foreach ($entityTypeListParamList as $param) { - $list = $data->$param ?? []; - foreach ($list as $i => $item) { - if ($item === $scope) { - unset($list[$i]); - } - } - $list = array_values($list); - $data->$param = $list; + $scopeList = array_keys($this->metadata->get(['entityDefs'], [])); + + foreach ($scopeList as $scope) { + if (!$this->metadata->get(['scopes', $scope, 'acl'])) { + continue; + } + + if ($this->acl->check($scope)) { + continue; + } + + foreach ($entityTypeListParamList as $param) { + $list = $data->$param ?? []; + + foreach ($list as $i => $item) { + if ($item === $scope) { + unset($list[$i]); + } } + + $list = array_values($list); + + $data->$param = $list; } } } @@ -203,6 +218,7 @@ class Settings foreach ($this->config->getSuperAdminOnlyItemList() as $item) { $ignoreItemList[] = $item; } + foreach ($this->config->getSuperAdminOnlySystemItemList() as $item) { $ignoreItemList[] = $item; } @@ -213,7 +229,9 @@ class Settings } $entity = $this->entityManager->getEntity('Settings'); + $entity->set($data); + $this->processValidation($entity, $data); if ( @@ -222,13 +240,11 @@ class Settings $this->dataManager->clearCache(); } - $this->config->setData($data); + $this->configWriter->setMultiple( + get_object_vars($data) + ); - $result = $this->config->save(); - - if ($result === false) { - throw new Error('Cannot save settings'); - } + $this->configWriter->save(); if (isset($data->personNameFormat)) { $this->dataManager->clearCache(); @@ -237,8 +253,6 @@ class Settings if (isset($data->defaultCurrency) || isset($data->baseCurrency) || isset($data->currencyRates)) { $this->populateDatabaseWithCurrencyRates(); } - - return $result; } protected function populateDatabaseWithCurrencyRates() @@ -254,9 +268,13 @@ class Settings unset($data->webSocketUrl); } - if ($user->isSystem()) return; + if ($user->isSystem()) { + return; + } - if ($user->isAdmin()) return; + if ($user->isAdmin()) { + return; + } if ( !$this->acl->checkScope('Email', 'create') @@ -290,6 +308,7 @@ class Settings $itemList = $this->config->getUserOnlyItemList(); $fieldDefs = $this->metadata->get(['entityDefs', 'Settings', 'fields']); + foreach ($fieldDefs as $field => $fieldParams) { if (!empty($fieldParams['onlyUser'])) { foreach ($this->fieldUtil->getAttributeList('Settings', $field) as $attribute) { @@ -306,6 +325,7 @@ class Settings $itemList = $this->config->getSystemOnlyItemList(); $fieldDefs = $this->metadata->get(['entityDefs', 'Settings', 'fields']); + foreach ($fieldDefs as $field => $fieldParams) { if (!empty($fieldParams['onlySystem'])) { foreach ($this->fieldUtil->getAttributeList('Settings', $field) as $attribute) { @@ -322,6 +342,7 @@ class Settings $itemList = $this->config->get('globalItems', []); $fieldDefs = $this->metadata->get(['entityDefs', 'Settings', 'fields']); + foreach ($fieldDefs as $field => $fieldParams) { if (!empty($fieldParams['global'])) { foreach ($this->fieldUtil->getAttributeList('Settings', $field) as $attribute) { @@ -338,7 +359,10 @@ class Settings $fieldList = $this->fieldUtil->getEntityTypeFieldList('Settings'); foreach ($fieldList as $field) { - if (!$this->isFieldSetInData($data, $field)) continue; + if (!$this->isFieldSetInData($data, $field)) { + continue; + } + $this->processValidationField($entity, $field, $data); } } @@ -352,7 +376,10 @@ class Settings foreach ($validationList as $type) { $value = $this->fieldUtil->getEntityTypeFieldParam('Settings', $field, $type); - if (is_null($value) && !in_array($type, $mandatoryValidationList)) continue; + + if (is_null($value) && !in_array($type, $mandatoryValidationList)) { + continue; + } if (!$fieldValidatorManager->check($entity, $field, $type, $data)) { throw new BadRequest("Not valid data. Field: '{$field}', type: {$type}."); @@ -363,13 +390,17 @@ class Settings protected function isFieldSetInData($data, $field) { $attributeList = $this->fieldUtil->getActualAttributeList('Settings', $field); + $isSet = false; + foreach ($attributeList as $attribute) { if (property_exists($data, $attribute)) { $isSet = true; + break; } } + return $isSet; } } diff --git a/install/core/Installer.php b/install/core/Installer.php index d728058677..65f897dfde 100644 --- a/install/core/Installer.php +++ b/install/core/Installer.php @@ -32,6 +32,9 @@ use Espo\Core\{ Utils\Util, Utils\Config\ConfigFileManager, Utils\Config, + Utils\Config\ConfigWriter, + Utils\Config\ConfigWriterFileManager, + Utils\Config\ConfigWriterHelper, Utils\Database\Helper as DatabaseHelper, Utils\PasswordHash, Utils\SystemRequirements, @@ -97,7 +100,9 @@ class Installer protected function initialize() { $fileManager = new ConfigFileManager(); + $config = new Config($fileManager); + $configPath = $config->getConfigPath(); if (!file_exists($configPath)) { @@ -105,13 +110,22 @@ class Installer } $data = include('data/config.php'); + + $configWriter = new ConfigWriter( + $config, + new ConfigWriterFileManager(null, $data['defaultPermissions'] ?? null), + new ConfigWriterHelper() + ); + $defaultData = $config->getDefaults(); //save default data if not exists, check by keys if (!Util::arrayKeysExists(array_keys($defaultData), $data)) { $defaultData = array_replace_recursive($defaultData, $data); - $config->set($defaultData); - $config->save(); + + $configWriter->setMultiple($defaultData); + + $configWriter->save(); } } @@ -130,6 +144,11 @@ class Installer return $this->app->getContainer()->get('config'); } + public function createConfigWriter() : ConfigWriter + { + return $this->app->getContainer()->get('injectableFactory')->create(ConfigWriter::class); + } + protected function getSystemHelper() { return $this->systemHelper; @@ -154,7 +173,9 @@ class Installer { if (!isset($this->passwordHash)) { $config = $this->getConfig(); - $this->passwordHash = new PasswordHash($config); + $configWriter = $this->createConfigWriter(); + + $this->passwordHash = new PasswordHash($config, $configWriter); } return $this->passwordHash; @@ -192,8 +213,10 @@ class Installer if (!isset($this->language)) { try { $this->language = $this->app->getContainer()->get('defaultLanguage'); - } catch (Throwable $e) { + } + catch (Throwable $e) { echo "Error: " . $e->getMessage(); + $GLOBALS['log']->error($e->getMessage()); die; @@ -227,6 +250,7 @@ class Installer public function getSystemRequirementList($type, $requiredOnly = false, array $additionalData = null) { $systemRequirementManager = new SystemRequirements($this->app->getContainer()); + return $systemRequirementManager->getRequiredListByType($type, $requiredOnly, $additionalData); } @@ -240,9 +264,11 @@ class Installer catch (Exception $e) { if ($isCreateDatabase && $e->getCode() == '1049') { $modParams = $params; + unset($modParams['dbname']); $pdo = $this->getDatabaseHelper()->createPdoConnection($modParams); + $pdo->query("CREATE DATABASE IF NOT EXISTS `". $params['dbname'] ."`"); return $this->checkDatabaseConnection($params, false); @@ -279,25 +305,31 @@ class Installer 'siteUrl' => !empty($saveData['siteUrl']) ? $saveData['siteUrl'] : $this->getSystemHelper()->getBaseUrl(), 'passwordSalt' => $this->getPasswordHash()->generateSalt(), 'cryptKey' => $this->getContainer()->get('crypt')->generateKey(), - 'hashSecretKey' => \Espo\Core\Utils\Util::generateSecretKey(), + 'hashSecretKey' => Util::generateSecretKey(), ]; if (empty($saveData['defaultPermissions']['user'])) { - $saveData['defaultPermissions']['user'] = $this->getFileManager()->getPermissionUtils()->getDefaultOwner(true); + $saveData['defaultPermissions']['user'] = $this->getFileManager() + ->getPermissionUtils() + ->getDefaultOwner(true); } if (empty($saveData['defaultPermissions']['group'])) { - $saveData['defaultPermissions']['group'] = $this->getFileManager()->getPermissionUtils()->getDefaultGroup(true); + $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']; } $data = array_merge($data, $initData); + $result = $this->saveConfig($data); return $result; @@ -305,12 +337,13 @@ class Installer public function saveConfig($data) { - $config = $this->app->getContainer()->get('config'); + $configWriter = $this->createConfigWriter(); - $config->set($data); - $result = $config->save(); + $configWriter->setMultiple($data); - return $result; + $configWriter->save(); + + return true; } public function buildDatabase() @@ -446,13 +479,16 @@ class Installer ); $data = array_intersect_key($preferences, array_flip($permittedSettingList)); + if (empty($data)) { return true; } $entity = $this->getEntityManager()->getEntity('Preferences', '1'); + if ($entity) { $entity->set($data); + return $this->getEntityManager()->saveEntity($entity); } @@ -479,12 +515,16 @@ class Installer /** END: afterInstall scripts */ $installerConfig = $this->getInstallerConfig(); + $installerConfig->set('isInstalled', true); + $installerConfig->save(); - $config = $this->app->getContainer()->get('config'); - $config->set('isInstalled', true); - $result &= $config->save(); + $configWriter = $this->createConfigWriter(); + + $configWriter->set('isInstalled', true); + + $configWriter->save(); return $result; } @@ -505,10 +545,12 @@ class Installer switch ($fieldName) { case 'defaultCurrency': $settingDefs['defaultCurrency']['options'] = $this->getCurrencyList(); + break; case 'language': $settingDefs['language']['options'] = $this->getLanguageList(false); + break; } @@ -526,8 +568,11 @@ class Installer $defaultSettings = $this->getDefaultSettings(); $normalizedParams = []; + foreach ($params as $name => $value) { - if (!isset($defaultSettings[$name])) continue; + if (!isset($defaultSettings[$name])) { + continue; + } $paramDefs = $defaultSettings[$name]; $paramType = isset($paramDefs['type']) ? $paramDefs['type'] : 'varchar'; @@ -539,23 +584,32 @@ class Installer case 'enum': if (isset($paramDefs['options']) && array_key_exists($value, $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'] .'].'); } + 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': $normalizedParams[$name] = (int) $value; + break; case 'varchar': default: $normalizedParams[$name] = $value; + break; } } @@ -573,7 +627,8 @@ class Installer } if ($optionLabel == $name) { - $optionLabel = array(); + $optionLabel = []; + foreach ($settingDefs['options'] as $key => $value) { $optionLabel[$value] = $value; } @@ -603,7 +658,8 @@ class Installer try { $result &= $sth->execute(); - } catch (Exception $e) { + } + catch (Exception $e) { $GLOBALS['log']->warning('Error executing the query: ' . $query); } diff --git a/tests/integration/Espo/Currency/CurrencyTest.php b/tests/integration/Espo/Currency/CurrencyTest.php new file mode 100644 index 0000000000..4808341184 --- /dev/null +++ b/tests/integration/Espo/Currency/CurrencyTest.php @@ -0,0 +1,64 @@ +createApplication(); + + $configWriter = $app->getContainer()->get('injectableFactory')->create(ConfigWriter::class); + + $configWriter->set('currencyList', ['USD', 'EUR']); + $configWriter->set('defaultCurrency', 'USD'); + $configWriter->set('baseCurrency', 'USD'); + + $configWriter->set('currencyRates', [ + 'EUR' => 1.2, + ]); + + $configWriter->save(); + + $service = $app->getContainer()->get('serviceFactory')->create('CurrencyRate'); + + $newRates = $service->set( + (object) [ + 'EUR' => 1.3, + ] + ); + + $this->assertEquals(1.3, $newRates->EUR); + } +} diff --git a/tests/unit/Espo/Core/Utils/Config/ConfigWriterTest.php b/tests/unit/Espo/Core/Utils/Config/ConfigWriterTest.php new file mode 100644 index 0000000000..2b8554d965 --- /dev/null +++ b/tests/unit/Espo/Core/Utils/Config/ConfigWriterTest.php @@ -0,0 +1,124 @@ +fileManager = $this->createMock(ConfigWriterFileManager::class); + + $this->config = $this->createMock(Config::class); + + $this->helper = $this->createMock(ConfigWriterHelper::class); + + $this->configWriter = new ConfigWriter($this->config, $this->fileManager, $this->helper); + + $this->configPath = 'somepath'; + + $this->config + ->expects($this->any()) + ->method('getConfigPath') + ->willReturn($this->configPath); + } + + public function testSave1() + { + $this->configWriter->set('k1', 'v1'); + + $this->configWriter->setMass([ + 'k2' => 'v2', + 'k3' => 'v3', + ]); + + $this->configWriter->remove('k4'); + + $previousData = [ + 'k3' => 'e3', + 'k4' => 'e4', + 'microtime' => 0.0, + 'cacheTimestamp' => 0, + ]; + + $newData = [ + 'k1' => 'v1', + 'k2' => 'v2', + 'k3' => 'v3', + 'microtime' => 1.0, + 'cacheTimestamp' => 1, + ]; + + $this->helper + ->expects($this->once()) + ->method('generateMicrotime') + ->willReturn(1.0); + + $this->helper + ->expects($this->once()) + ->method('generateCacheTimestamp') + ->willReturn(1); + + $this->config + ->expects($this->once()) + ->method('update'); + + $this->fileManager + ->expects($this->at(0)) + ->method('isFile') + ->with($this->configPath) + ->willReturn(true); + + $this->fileManager + ->expects($this->at(1)) + ->method('getPhpContents') + ->with($this->configPath) + ->willReturn($previousData); + + $this->fileManager + ->expects($this->at(2)) + ->method('putPhpContents') + ->with($this->configPath, $newData); + + $this->fileManager + ->expects($this->at(3)) + ->method('getPhpContents') + ->with($this->configPath) + ->willReturn($previousData); + + $this->configWriter->save(); + } +}