diff --git a/application/Espo/Core/Utils/File/Manager.php b/application/Espo/Core/Utils/File/Manager.php index fd4e653b50..9877b92f90 100644 --- a/application/Espo/Core/Utils/File/Manager.php +++ b/application/Espo/Core/Utils/File/Manager.php @@ -376,64 +376,27 @@ class Manager } /** - * Merge file content and save it to a file. - * - * @param string | array $path - * @param string $content JSON string - * @param bool $isReturnJson - * @param string | array $removeOptions List of unset keys from content. - * @param bool $isPhp Is merge php files. - * - * @return bool | array + * Merge JSON file contents with existing and override the file. */ - public function mergeContents($path, $content, $isReturnJson = false, $removeOptions = null, $isPhp = false) + public function mergeJsonContents(string $path, array $data): bool { - if ($isPhp) { - $fileContent = $this->getPhpContents($path); - } - else { - $fileContent = $this->getContents($path); + $currentContents = $this->getContents($path); + + if ($this->isFile($path) && $currentContents === false) { + throw new Error("FileManager: Failed to read file '{$path}'."); } - $fullPath = $this->concatPaths($path); + $currentData = $this->isFile($path) ? + Json::decode($currentContents, true): + []; - if (file_exists($fullPath) && ($fileContent === false || empty($fileContent))) { - throw new Error('FileManager: Failed to read file [' . $fullPath .'].'); - } + $mergedData = Util::merge($currentData, $data); - $savedDataArray = Json::decode($fileContent, true); + $jsonOptions = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES; - $newDataArray = Json::decode($content, true); + $stringData = Json::encode($mergedData, $jsonOptions); - if (isset($removeOptions)) { - $savedDataArray = Util::unsetInArray($savedDataArray, $removeOptions); - $newDataArray = Util::unsetInArray($newDataArray, $removeOptions); - } - - $data = Util::merge($savedDataArray, $newDataArray); - - if ($isReturnJson) { - $data = Json::encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - } - - if ($isPhp) { - return $this->putPhpContents($path, $data); - } - - return $this->putContents($path, $data); - } - - /** - * Merge PHP content and save it to a file. - * - * @param string | array $path - * @param string $content JSON string - * @param string | array $removeOptions - List of unset keys from content - * @return bool - */ - public function mergePhpContents($path, $content, $removeOptions = null) - { - return $this->mergeContents($path, $content, false, $removeOptions, true); + return (bool) $this->putContents($path, $stringData); } /** @@ -450,39 +413,33 @@ class Manager } /** - * Unset some element of content data. - * - * @param string | array $path - * @param array | string $unsets - * @return bool + * Unset specific items in a JSON file and override the file. + * Items are specified as an array of JSON paths. */ - public function unsetContents($path, $unsets, $isJSON = true) + public function unsetJsonContents(string $path, array $unsets): bool { - $currentData = $this->getContents($path); - - if (!isset($currentData) || !$currentData) { + if (!file_exists($path)) { return true; } - $currentDataArray = Json::decode($currentData, true); + $currentContents = $this->getContents($path); - $unsettedData = Util::unsetInArray($currentDataArray, $unsets, true); - - if (is_null($unsettedData) || (is_array($unsettedData) && empty($unsettedData))) { - $fullPath = $this->concatPaths($path); - - if (!file_exists($fullPath)) { - return true; - } - - return $this->unlink($fullPath); + if (!isset($currentContents) || !$currentContents) { + return true; } - if ($isJSON) { - return $this->putContentsJson($path, $unsettedData); + $currentData = Json::decode($currentContents, true); + + $unsettedData = Util::unsetInArray($currentData, $unsets, true); + + if ( + is_null($unsettedData) || + (is_array($unsettedData) && empty($unsettedData)) + ) { + return $this->unlink($path); } - return $this->putContents($path, $unsettedData); + return (bool) $this->putContentsJson($path, $unsettedData); } diff --git a/application/Espo/Core/Utils/File/Permission.php b/application/Espo/Core/Utils/File/Permission.php index 806a3a6076..5f3bc582e2 100644 --- a/application/Espo/Core/Utils/File/Permission.php +++ b/application/Espo/Core/Utils/File/Permission.php @@ -29,9 +29,12 @@ namespace Espo\Core\Utils\File; -use Espo\Core\Utils; +use Espo\Core\Utils\Util; + use Espo\Core\Exceptions\Error; +use Throwable; + class Permission { private $fileManager; @@ -89,12 +92,6 @@ class Permission } } } - - protected function getFileManager() - { - return $this->fileManager; - } - /** * Get default settings. * @@ -179,7 +176,7 @@ class Permission /** * Change permissions. * - * @param string $filename + * @param string $path * @param int|array $octal ex. 0755, array(0644, 0755), array('file'=>0644, 'dir'=>0755). * @param bool $recurse * @@ -245,7 +242,7 @@ class Permission /** * Change permissions recursive. * - * @param string $filename + * @param string $path * @param int $fileOctal - ex. 0644 * @param int $dirOctal - ex. 0755 * @@ -263,9 +260,10 @@ class Permission $result = $this->chmodReal($path, $dirOctal); - $allFiles = $this->getFileManager()->getFileList($path); + $allFiles = $this->fileManager->getFileList($path); + foreach ($allFiles as $item) { - $result &= $this->chmodRecurse($path . Utils\Util::getSeparator() . $item, $fileOctal, $dirOctal); + $result &= $this->chmodRecurse($path . Util::getSeparator() . $item, $fileOctal, $dirOctal); } return (bool) $result; @@ -319,9 +317,9 @@ class Permission $result = $this->chownReal($path, $user); - $allFiles = $this->getFileManager()->getFileList($path); + $allFiles = $this->fileManager->getFileList($path); foreach ($allFiles as $item) { - $result &= $this->chownRecurse($path . Utils\Util::getSeparator() . $item, $user); + $result &= $this->chownRecurse($path . Util::getSeparator() . $item, $user); } return (bool) $result; @@ -376,9 +374,9 @@ class Permission $result = $this->chgrpReal($path, $group); - $allFiles = $this->getFileManager()->getFileList($path); + $allFiles = $this->fileManager->getFileList($path); foreach ($allFiles as $item) { - $result &= $this->chgrpRecurse($path . Utils\Util::getSeparator() . $item, $group); + $result &= $this->chgrpRecurse($path . Util::getSeparator() . $item, $group); } return (bool) $result; @@ -500,7 +498,7 @@ class Permission try { $this->chmod($path, $this->writablePermissions, $options['recursive']); } - catch (\Throwable $e) {} + catch (Throwable $e) {} /** check is writable */ $res = is_writable($path); @@ -508,10 +506,12 @@ class Permission if (is_dir($path)) { try { $name = uniqid(); - $res &= $this->getFileManager()->putContents([$path, $name], 'test'); - $res &= $this->getFileManager()->removeFile($name, $path); + + $res &= $this->fileManager->putContents($path . '/' . $name, 'test'); + + $res &= $this->fileManager->removeFile($name, $path); } - catch (\Throwable $e) { + catch (Throwable $e) { $res = false; } } diff --git a/application/Espo/Core/Utils/Language.php b/application/Espo/Core/Utils/Language.php index 9f35978aa1..b6f46123f0 100644 --- a/application/Espo/Core/Utils/Language.php +++ b/application/Espo/Core/Utils/Language.php @@ -72,7 +72,7 @@ class Language ]; public function __construct( - ?string $language = null, + ?string $language, FileManager $fileManager, Metadata $metadata, DataCache $dataCache = null, @@ -81,7 +81,8 @@ class Language ) { if ($language) { $this->currentLanguage = $language; - } else { + } + else { $this->currentLanguage = $this->defaultLanguage; } @@ -99,16 +100,6 @@ class Language $this->unifier = new FileUnifier($this->fileManager, $this->metadata); } - protected function getFileManager() - { - return $this->fileManager; - } - - protected function getMetadata() - { - return $this->metadata; - } - protected function getUnifier() { return $this->unifier; @@ -247,17 +238,22 @@ class Language if (!empty($this->changedData)) { foreach ($this->changedData as $scope => $data) { - if (!empty($data)) { - $result &= $this->getFileManager()->mergeContents([$path, $scope.'.json'], $data, true); + if (empty($data)) { + continue; } + + $result &= $this->fileManager->mergeJsonContents($path . "/{$scope}.json", $data); + } } if (!empty($this->deletedData)) { foreach ($this->deletedData as $scope => $unsetData) { - if (!empty($unsetData)) { - $result &= $this->getFileManager()->unsetContents([$path, $scope.'.json'], $unsetData, true); + if (empty($unsetData)) { + continue; } + + $result &= $this->fileManager->unsetJsonContents($path . "/{$scope}.json", $unsetData); } } @@ -322,11 +318,11 @@ class Language /** * Remove a label. * - * @param string $name - * @param string $category - * @param string $scope + * @param string $scope + * @param string $category + * @param string|array $name */ - public function delete($scope, $category, $name) + public function delete(string $scope, string $category, $name): void { if (is_array($name)) { foreach ($name as $rowLabel) { diff --git a/application/Espo/Core/Utils/Metadata.php b/application/Espo/Core/Utils/Metadata.php index f270e8e18e..ee62d99193 100644 --- a/application/Espo/Core/Utils/Metadata.php +++ b/application/Espo/Core/Utils/Metadata.php @@ -38,6 +38,8 @@ use Espo\Core\{ Utils\DataCache, }; +use StdClass; + class Metadata { protected $data = null; @@ -46,8 +48,6 @@ class Metadata protected $useCache; - private $unifier; - private $objUnifier; private $module; @@ -75,6 +75,7 @@ class Metadata private $changedData = []; private $fileManager; + private $dataCache; public function __construct(FileManager $fileManager, DataCache $dataCache, bool $useCache = false) @@ -352,14 +353,15 @@ class Metadata /** * Get metadata definition in custom directory. * - * @param string|array $key - * @param mixed $default + * @param string|array $key + * @param mixed $default * - * @return object|mixed + * @return object */ public function getCustom($key1, $key2, $default = null) { - $filePath = array($this->paths['customPath'], $key1, $key2.'.json'); + $filePath = $this->paths['customPath'] . "/{$key1}/{$key2}.json"; + $fileContent = $this->fileManager->getContents($filePath); if ($fileContent) { @@ -373,43 +375,33 @@ class Metadata * Set and save metadata in custom directory. * The data is not merging with existing data. Use getCustom() to get existing data. * - * @param string $key1 - * @param string $key2 - * @param array $data - * - * @return boolean + * @param string $key1 + * @param string $key2 + * @param array|object $data */ - public function saveCustom($key1, $key2, $data) + public function saveCustom(string $key1, string $key2, $data): void { if (is_object($data)) { foreach ($data as $key => $item) { - if ($item == new \stdClass()) { + if ($item == new StdClass()) { unset($data->$key); } } } - $filePath = array($this->paths['customPath'], $key1, $key2.'.json'); + $filePath = $this->paths['customPath'] . "/{$key1}/{$key2}.json"; + $changedData = Json::encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - $result = $this->fileManager->putContents($filePath, $changedData); + $this->fileManager->putContents($filePath, $changedData); $this->init(true); - - return true; } /** - * Set Metadata data. - * Ex. $key1 = menu, $key2 = Account then will be created a file metadataFolder/menu/Account.json - * - * @param string $key1 - * @param string $key2 - * @param JSON string $data - * - * @return bool - */ - public function set($key1, $key2, $data) + * Set Metadata data. + */ + public function set(string $key1, string $key2, $data): void { if (is_array($data)) { foreach ($data as $key => $item) { @@ -419,11 +411,11 @@ class Metadata } } - $newData = array( - $key1 => array( + $newData = [ + $key1 => [ $key2 => $data, - ), - ); + ], + ]; $this->changedData = Util::merge($this->changedData, $newData); $this->data = Util::merge($this->getData(), $newData); @@ -434,13 +426,11 @@ class Metadata /** * Unset some fields and other stuff in metadata. * - * @param string $key1 - * @param string $key2 - * @param array | string $unsets Ex. 'fields.name' + * @param array|string $unsets Ex. `fields.name`. * * @return bool */ - public function delete($key1, $key2, $unsets = null) + public function delete(string $key1, string $key2, $unsets = null) { if (!is_array($unsets)) { $unsets = (array) $unsets; @@ -459,7 +449,9 @@ class Metadata $fieldPath = [$key1, $key2, 'fields', $fieldName]; $additionalFields = $this->getMetadataHelper()->getAdditionalFieldList( - $fieldName, $this->get($fieldPath, []), $fieldDefinitionList + $fieldName, + $this->get($fieldPath, []), + $fieldDefinitionList ); if (is_array($additionalFields)) { @@ -524,7 +516,7 @@ class Metadata * * @return bool */ - public function save() + public function save(): bool { $path = $this->paths['customPath']; @@ -533,9 +525,13 @@ class Metadata if (!empty($this->changedData)) { foreach ($this->changedData as $key1 => $keyData) { foreach ($keyData as $key2 => $data) { - if (!empty($data)) { - $result &= $this->fileManager->mergeContents([$path, $key1, $key2.'.json'], $data, true); + if (empty($data)) { + continue; } + + $filePath = $path . "/{$key1}/{$key2}.json"; + + $result &= $this->fileManager->mergeJsonContents($filePath, $data); } } } @@ -543,25 +539,27 @@ class Metadata if (!empty($this->deletedData)) { foreach ($this->deletedData as $key1 => $keyData) { foreach ($keyData as $key2 => $unsetData) { - if (!empty($unsetData)) { - $rowResult = $this->fileManager->unsetContents( - [$path, $key1, $key2.'.json'], $unsetData, true - ); - - if ($rowResult == false) { - $GLOBALS['log']->warning( - 'Metadata items ['.$key1.'.'.$key2.'] can be deleted for custom code only.' - ); - } - - $result &= $rowResult; + if (empty($unsetData)) { + continue; } + + $filePath = $path . "/{$key1}/{$key2}.json"; + + $rowResult = $this->fileManager->unsetJsonContents($filePath, $unsetData); + + if (!$rowResult) { + $GLOBALS['log']->warning( + 'Metadata items ['.$key1.'.'.$key2.'] can be deleted for custom code only.' + ); + } + + $result &= $rowResult; } } } - if ($result == false) { - throw new Error("Error saving metadata. See log file for details."); + if (!$result) { + throw new Error("Error while saving metadata. See log file for details."); } $this->clearChanges(); diff --git a/application/Espo/Tools/EntityManager/EntityManager.php b/application/Espo/Tools/EntityManager/EntityManager.php index 4414a01eee..9d19b11686 100644 --- a/application/Espo/Tools/EntityManager/EntityManager.php +++ b/application/Espo/Tools/EntityManager/EntityManager.php @@ -263,12 +263,12 @@ class EntityManager } if ($this->checkControllerExists($name)) { - throw new Conflict('Entity name \''.$name.'\' is not allowed.'); + throw new Conflict('Entity name \''.$name.'\' is not allowed. Controller already exists.'); } $serviceFactory = $this->getServiceFactory(); - if ($serviceFactory && $serviceFactory->checKExists($name)) { + if ($serviceFactory && $serviceFactory->checkExists($name)) { throw new Conflict('Entity name \''.$name.'\' is not allowed.'); } @@ -486,6 +486,7 @@ class EntityManager $this->getBaseLanguage()->save(); $layoutsPath = $templatePath . "/Layouts/{$type}"; + if ($this->getFileManager()->isDir($layoutsPath)) { $this->getFileManager()->copy($layoutsPath, 'custom/Espo/Custom/Resources/layouts/' . $name); } diff --git a/application/Espo/Tools/FieldManager/FieldManager.php b/application/Espo/Tools/FieldManager/FieldManager.php index 0c8965cff1..dbadbeb307 100644 --- a/application/Espo/Tools/FieldManager/FieldManager.php +++ b/application/Espo/Tools/FieldManager/FieldManager.php @@ -550,7 +550,9 @@ class FieldManager } } - return $this->getMetadata()->saveCustom('entityDefs', $scope, $customDefs); + $this->getMetadata()->saveCustom('entityDefs', $scope, $customDefs); + + return true; } protected function getLinkDefs($scope, $name) diff --git a/application/Espo/Tools/LayoutManager/LayoutManager.php b/application/Espo/Tools/LayoutManager/LayoutManager.php index 37795d59cd..cd4b8f0250 100644 --- a/application/Espo/Tools/LayoutManager/LayoutManager.php +++ b/application/Espo/Tools/LayoutManager/LayoutManager.php @@ -119,7 +119,9 @@ class LayoutManager $data = Json::encode($layoutData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); - $result &= $this->fileManager->putContents([$layoutPath, $layoutName.'.json'], $data); + $path = $layoutPath . '/' . $layoutName . '.json'; + + $result &= $this->fileManager->putContents($path, $data); } } diff --git a/tests/unit/Espo/Core/Utils/File/ManagerTest.php b/tests/unit/Espo/Core/Utils/File/ManagerTest.php index 5baa68d792..ce4e42eb66 100644 --- a/tests/unit/Espo/Core/Utils/File/ManagerTest.php +++ b/tests/unit/Espo/Core/Utils/File/ManagerTest.php @@ -186,10 +186,11 @@ class ManagerTest extends \PHPUnit\Framework\TestCase $initData = '{"fields":{"someName":{"type":"varchar","maxLength":40},"someName2":{"type":"varchar","maxLength":36}}}'; $this->object->putContents($testPath, $initData); - $unsets = 'fields.someName2'; - $this->assertTrue($this->object->unsetContents($testPath, $unsets)); + $unsets = ['fields.someName2']; + $this->assertTrue($this->object->unsetJsonContents($testPath, $unsets)); $result = '{"fields":{"someName":{"type":"varchar","maxLength":40}}}'; + $this->assertJsonStringEqualsJsonFile($testPath, $result); }