. * * 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. ************************************************************************/ namespace Espo\Core\Utils\File; use Espo\Core\Utils\DataUtil; use Espo\Core\Utils\File\Manager as FileManager; use Espo\Core\Utils\Json; use Espo\Core\Utils\Module; use Espo\Core\Utils\Resource\PathProvider; use Espo\Core\Utils\Util; use JsonException; use LogicException; use stdClass; class Unifier { protected bool $useObjects = false; private string $unsetFileName = 'unset.json'; private const APPEND_VALUE = '__APPEND__'; private const ANY_KEY = '__ANY__'; public function __construct( private FileManager $fileManager, private Module $module, private PathProvider $pathProvider ) {} /** * Merge data of resource files. * * @param array $forceAppendPathList * @return array|stdClass */ public function unify(string $path, bool $noCustom = false, array $forceAppendPathList = []) { if ($this->useObjects) { return $this->unifyObject($path, $noCustom, $forceAppendPathList); } return $this->unifyArray($path, $noCustom); } /** * @return array */ private function unifyArray(string $path, bool $noCustom = false) { /** @var array $data */ $data = $this->unifySingle($this->pathProvider->getCore() . $path, true); foreach ($this->getModuleList() as $moduleName) { $filePath = $this->pathProvider->getModule($moduleName) . $path; /** @var array $newData */ $newData = $this->unifySingle($filePath, true); /** @var array $data */ $data = Util::merge($data, $newData); } if ($noCustom) { return $data; } $customFilePath = $this->pathProvider->getCustom() . $path; /** @var array $newData */ $newData = $this->unifySingle($customFilePath, true); /** @var array */ return Util::merge($data, $newData); } /** * @param array $forceAppendPathList * @return stdClass */ private function unifyObject(string $path, bool $noCustom = false, array $forceAppendPathList = []) { /** @var stdClass $data */ $data = $this->unifySingle($this->pathProvider->getCore() . $path, true); foreach ($this->getModuleList() as $moduleName) { $filePath = $this->pathProvider->getModule($moduleName) . $path; /** @var stdClass $itemData */ $itemData = $this->unifySingle($filePath, true); $this->prepareItemDataObject($itemData, $forceAppendPathList); /** @var stdClass $data */ $data = DataUtil::merge($data, $itemData); } if ($noCustom) { return $data; } $customFilePath = $this->pathProvider->getCustom() . $path; /** @var stdClass $itemData */ $itemData = $this->unifySingle($customFilePath, true); $this->prepareItemDataObject($itemData, $forceAppendPathList); /** @var stdClass */ return DataUtil::merge($data, $itemData); } /** * @return array|stdClass */ private function unifySingle(string $dirPath, bool $recursively) { $data = []; $unsets = []; if ($this->useObjects) { $data = (object) []; } if (empty($dirPath) || !$this->fileManager->exists($dirPath)) { return $data; } $fileList = $this->fileManager->getFileList($dirPath, $recursively, '\.json$'); //$dirName = $this->fileManager->getDirName($dirPath, false); foreach ($fileList as $dirName => $item) { if (is_array($item)) { /** @var string $dirName */ // Only a first level of a subdirectory. $itemValue = $this->unifySingle( Util::concatPath($dirPath, $dirName), false ); if ($this->useObjects) { /** @var stdClass $data */ $data->$dirName = $itemValue; continue; } /** @var array $data */ $data[$dirName] = $itemValue; continue; } /** @var string $item */ $fileName = $item; if ($fileName === $this->unsetFileName) { $fileContent = $this->fileManager->getContents($dirPath . '/' . $fileName); $unsets = Json::decode($fileContent, true); continue; } $itemValue = $this->getContents($dirPath . '/' . $fileName); if (empty($itemValue)) { continue; } $name = $this->fileManager->getFileName($fileName, '.json'); if ($this->useObjects) { /** @var stdClass $data */ $data->$name = $itemValue; continue; } /** @var array $data */ $data[$name] = $itemValue; } if ($this->useObjects) { /** @var stdClass $data */ /** @var stdClass */ return DataUtil::unsetByKey($data, $unsets); } /** @var array $data */ /** @var array */ return Util::unsetInArray($data, $unsets); } /** * @return stdClass|array * @throws JsonException */ private function getContents(string $path) { $fileContent = $this->fileManager->getContents($path); try { return Json::decode($fileContent, !$this->useObjects); } catch (JsonException) { throw new JsonException("JSON syntax error in '$path'."); } } /** * @return string[] */ private function getModuleList(): array { return $this->module->getOrderedList(); } /** * @param array $forceAppendPathList */ private function prepareItemDataObject(stdClass $data, array $forceAppendPathList): void { foreach ($forceAppendPathList as $path) { $this->addAppendToData($data, $path); } } /** * @param string[] $path */ private function addAppendToData(stdClass $data, array $path): void { if (count($path) === 0) { return; } $nextPath = array_slice($path, 1); $key = $path[0]; if ($key === self::ANY_KEY) { foreach (array_keys(get_object_vars($data)) as $itemKey) { $this->addAppendToDataItem($data, $itemKey, $nextPath); } return; } $this->addAppendToDataItem($data, $key, $nextPath); } /** * @param string[] $path */ private function addAppendToDataItem(stdClass $data, string $key, array $path): void { $item = $data->$key ?? null; if (count($path) === 0) { if ($item === null) { $item = []; } if (!is_array($item)) { throw new LogicException("Expected array in metadata, but non-array is set."); } if (($item[0] ?? null) === self::APPEND_VALUE) { return; } $data->$key = array_merge([self::APPEND_VALUE], $item); return; } if (!$item instanceof stdClass) { return; } $this->addAppendToData($item, $path); } }