<?php 
 
/** 
 * Pimcore 
 * 
 * This source file is available under two different licenses: 
 * - GNU General Public License version 3 (GPLv3) 
 * - Pimcore Commercial License (PCL) 
 * Full copyright and license information is available in 
 * LICENSE.md which is distributed with this source code. 
 * 
 *  @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org) 
 *  @license    http://www.pimcore.org/license     GPLv3 and PCL 
 */ 
 
namespace Pimcore\Config; 
 
use Pimcore\Config; 
use Pimcore\Db\PhpArrayFileTable; 
use Pimcore\File; 
use Pimcore\Helper\StopMessengerWorkersTrait; 
use Pimcore\Model\Tool\SettingsStore; 
use Symfony\Component\Yaml\Yaml; 
 
class LocationAwareConfigRepository 
{ 
    use StopMessengerWorkersTrait; 
 
    /** 
     * @deprecated Will be removed in Pimcore 11 
     */ 
    public const LOCATION_LEGACY = 'legacy'; 
 
    public const LOCATION_SYMFONY_CONFIG = 'symfony-config'; 
 
    public const LOCATION_SETTINGS_STORE = 'settings-store'; 
 
    public const LOCATION_DISABLED = 'disabled'; 
 
    /** 
     * @var array 
     */ 
    protected array $containerConfig = []; 
 
    /** 
     * @var string|null 
     */ 
    protected ?string $settingsStoreScope = null; 
 
    /** 
     * @var string|null 
     * 
     * @deprecated Will be removed in Pimcore 11 
     */ 
    protected ?string $storageDirectory = null; 
 
    /** 
     * @var string|null 
     * 
     * @deprecated Will be removed in Pimcore 11 
     */ 
    protected ?string $writeTargetEnvVariableName = null; 
 
    /** 
     * @var string|null 
     * 
     * @deprecated Will be removed in Pimcore 11 
     */ 
    protected ?string $defaultWriteLocation = self::LOCATION_SYMFONY_CONFIG; 
 
    /** 
     * @deprecated Will be removed in Pimcore 11 
     */ 
    protected mixed $loadLegacyConfigCallback; 
 
    /** 
     * @deprecated Will be removed in Pimcore 11 
     * 
     * @var string|null 
     */ 
    protected ?string $legacyConfigFile = null; 
 
    /** 
     * @deprecated Will be removed in Pimcore 11 
     */ 
    private ?PhpArrayFileTable $legacyStore = null; 
 
    protected ?array $storageConfig = null; 
 
    /** 
     * @param array $containerConfig 
     * @param string|null $settingsStoreScope 
     * @param string|array|null $storageDirectory 
     * @param string|null $writeTargetEnvVariableName 
     * @param string|null $defaultWriteLocation 
     * @param string|null $legacyConfigFile 
     * @param mixed $loadLegacyConfigCallback 
     */ 
    public function __construct( 
        array $containerConfig, 
        ?string $settingsStoreScope, 
        string|array|null $storageDirectory, // will be renamed to `array $storageConfig` in Pimcore 11 
        ?string $writeTargetEnvVariableName, // @TODO to be removed in Pimcore 11 
        ?string $defaultWriteLocation = null, // @TODO to be removed in Pimcore 11 
        ?string $legacyConfigFile = null, // @TODO to be removed in Pimcore 11 
        mixed $loadLegacyConfigCallback = null // @TODO to be removed in Pimcore 11 
    ) { 
        $this->containerConfig = $containerConfig; 
        $this->settingsStoreScope = $settingsStoreScope; 
        $this->writeTargetEnvVariableName = $writeTargetEnvVariableName; 
        $this->defaultWriteLocation = $defaultWriteLocation ?: self::LOCATION_SYMFONY_CONFIG; 
        $this->legacyConfigFile = $legacyConfigFile; 
        $this->loadLegacyConfigCallback = $loadLegacyConfigCallback; 
 
        if (is_string($storageDirectory)) { 
            $this->storageDirectory = rtrim($storageDirectory, '/\\'); 
        } elseif (is_array($storageDirectory)) { 
            $this->storageConfig = $storageDirectory; 
        } 
    } 
 
    /** 
     * @param string $key 
     * 
     * @return array 
     */ 
    public function loadConfigByKey(string $key) 
    { 
        $dataSource = null; 
 
        // try to load from container config 
        $data = $this->getDataFromContainerConfig($key, $dataSource); 
 
        // try to load from SettingsStore 
        if (!$data) { 
            $data = $this->getDataFromSettingsStore($key, $dataSource); 
        } 
 
        // try to load from legacy config 
        if (!$data) { 
            $data = $this->getDataFromLegacyConfig($key, $dataSource); 
        } 
 
        return [ 
            $data, 
            $dataSource, 
        ]; 
    } 
 
    /** 
     * @param string $key 
     * @param string|null $dataSource 
     * 
     * @return mixed 
     */ 
    private function getDataFromContainerConfig(string $key, ?string &$dataSource) 
    { 
        if (isset($this->containerConfig[$key])) { 
            $dataSource = self::LOCATION_SYMFONY_CONFIG; 
        } 
 
        return $this->containerConfig[$key] ?? null; 
    } 
 
    /** 
     * @param string $key 
     * @param string|null $dataSource 
     * 
     * @return mixed 
     */ 
    private function getDataFromSettingsStore(string $key, ?string &$dataSource) 
    { 
        $settingsStoreEntryData = null; 
        $settingsStoreEntry = SettingsStore::get($key, $this->settingsStoreScope); 
        if ($settingsStoreEntry) { 
            $settingsStoreEntryData = json_decode($settingsStoreEntry->getData(), true); 
            $dataSource = self::LOCATION_SETTINGS_STORE; 
        } 
 
        return $settingsStoreEntryData; 
    } 
 
    /** 
     * @deprecated Will be removed in Pimcore 11 
     * 
     * @param string $key 
     * 
     * @return mixed 
     */ 
    private function getDataFromLegacyConfig(string $key, ?string &$dataSource) 
    { 
        $callback = $this->loadLegacyConfigCallback; 
        if (is_callable($callback)) { 
            return $callback($this, $dataSource); 
        } 
 
        if (!$this->legacyConfigFile) { 
            return null; 
        } 
 
        $data = $this->getLegacyStore()->fetchAll(); 
 
        if (isset($data[$key])) { 
            $dataSource = self::LOCATION_LEGACY; 
        } 
 
        return $data[$key] ?? null; 
    } 
 
    /** 
     * @param string|null $key 
     * @param string|null $dataSource 
     * 
     * @return bool 
     * 
     * @throws \Exception 
     */ 
    public function isWriteable(?string $key = null, ?string $dataSource = null): bool 
    { 
        $key = $key ?: uniqid('pimcore_random_key_', true); 
        $writeTarget = $this->getWriteTarget(); 
 
        if ($writeTarget === self::LOCATION_SYMFONY_CONFIG && !\Pimcore::getKernel()->isDebug()) { 
            return false; 
        } elseif ($writeTarget === self::LOCATION_DISABLED) { 
            return false; 
        } elseif ($dataSource === self::LOCATION_SYMFONY_CONFIG && !file_exists($this->getVarConfigFile($key))) { 
            return false; 
        } elseif ($dataSource && $dataSource !== self::LOCATION_LEGACY && $dataSource !== $writeTarget) { 
            return false; 
        } 
 
        return true; 
    } 
 
    /** 
     * @return string Can be either yaml (var/config/...) or "settings-store". defaults to "yaml" 
     * 
     * @throws \Exception 
     */ 
    public function getWriteTarget(): string 
    { 
        //TODO remove in Pimcore 11 
        $writeLocation = $this->writeTargetEnvVariableName ? $_SERVER[$this->writeTargetEnvVariableName] ?? null : null; 
 
        if ($writeLocation === null) { 
            $writeLocation = $this->storageConfig['write_target']['type'] ?? $this->defaultWriteLocation; 
        } 
 
        if (!in_array($writeLocation, [self::LOCATION_SETTINGS_STORE, self::LOCATION_SYMFONY_CONFIG, self::LOCATION_DISABLED])) { 
            throw new \Exception(sprintf('Invalid write location: %s', $writeLocation)); 
        } 
 
        return $writeLocation; 
    } 
 
    /** 
     * @param string $key 
     * @param mixed $data 
     * @param null|callable $yamlStructureCallback 
     * 
     * @throws \Exception 
     */ 
    public function saveConfig(string $key, $data, $yamlStructureCallback = null) 
    { 
        $writeLocation = $this->getWriteTarget(); 
 
        if ($writeLocation === self::LOCATION_SYMFONY_CONFIG) { 
            if (is_callable($yamlStructureCallback)) { 
                $data = $yamlStructureCallback($key, $data); 
            } 
 
            $this->writeYaml($key, $data); 
        } elseif ($writeLocation === self::LOCATION_SETTINGS_STORE) { 
            $settingsStoreData = json_encode($data); 
            SettingsStore::set($key, $settingsStoreData, 'string', $this->settingsStoreScope); 
        } 
 
        $this->stopMessengerWorkers(); 
    } 
 
    /** 
     * @param string $key 
     * @param array $data 
     * 
     * @throws \Exception 
     */ 
    private function writeYaml(string $key, $data): void 
    { 
        $yamlFilename = $this->getVarConfigFile($key); 
 
        if (!file_exists($yamlFilename)) { 
            list($existingData, $dataSource) = $this->loadConfigByKey($key); 
            if ($dataSource && $dataSource !== self::LOCATION_LEGACY) { 
                // this configuration already exists so check if it is writeable 
                // this is only the case if it comes from var/config or from the legacy file, or the settings-store 
                // however, we never want to write it back to the legacy file 
 
                throw new \Exception(sprintf('Configuration can only be written to %s, however the config comes from a different source', $yamlFilename)); 
            } 
        } 
 
        $this->searchAndReplaceMissingParameters($data); 
 
        File::put($yamlFilename, Yaml::dump($data, 50)); 
 
        $this->invalidateConfigCache(); 
    } 
 
    private function searchAndReplaceMissingParameters(array &$data): void 
    { 
        $container = \Pimcore::getContainer(); 
 
        foreach ($data as $key => &$value) { 
            if (is_array($value)) { 
                $this->searchAndReplaceMissingParameters($value); 
 
                continue; 
            } 
 
            if (!is_scalar($value)) { 
                continue; 
            } 
 
            if (preg_match('/%([^%\s]+)%/', $value, $match)) { 
                $key = $match[1]; 
 
                if (str_starts_with($key, 'env(') && str_ends_with($key, ')')  && 'env()' !== $key) { 
                    continue; 
                } 
 
                if (!$container->hasParameter($key)) { 
                    $value = preg_replace('/%([^%\s]+)%/', '%%$1%%', $value); 
                } 
            } 
        } 
    } 
 
    /** 
     * @param string $key 
     * 
     * @return string 
     */ 
    private function getVarConfigFile(string $key): string 
    { 
        $directory = rtrim($this->storageDirectory ?? $this->storageConfig['write_target']['options']['directory'], '/\\'); 
 
        return $directory . '/' . $key . '.yaml'; 
    } 
 
    /** 
     * @deprecated Will be removed in Pimcore 11 
     * 
     * @return PhpArrayFileTable 
     */ 
    private function getLegacyStore(): PhpArrayFileTable 
    { 
        if ($this->legacyStore === null) { 
            $file = Config::locateConfigFile($this->legacyConfigFile); 
            $this->legacyStore = PhpArrayFileTable::get($file); 
        } 
 
        return $this->legacyStore; 
    } 
 
    /** 
     * @param string $key 
     * @param string|null $dataSource 
     * 
     * @throws \Exception 
     */ 
    public function deleteData(string $key, ?string $dataSource): void 
    { 
        if (!$this->isWriteable($key)) { 
            throw new \Exception('You are trying to delete a non-writable configuration.'); 
        } 
 
        if ($dataSource === self::LOCATION_SYMFONY_CONFIG) { 
            unlink($this->getVarConfigFile($key)); 
            $this->invalidateConfigCache(); 
        } elseif ($dataSource === self::LOCATION_SETTINGS_STORE) { 
            SettingsStore::delete($key, $this->settingsStoreScope); 
        } elseif ($dataSource === self::LOCATION_LEGACY) { 
            $this->getLegacyStore()->delete($key); 
        } 
 
        $this->stopMessengerWorkers(); 
    } 
 
    /** 
     * @return array 
     */ 
    public function fetchAllKeys(): array 
    { 
        return array_unique(array_merge( 
            SettingsStore::getIdsByScope($this->settingsStoreScope), 
            array_keys($this->containerConfig), 
            $this->legacyConfigFile ? array_keys($this->getLegacyStore()->fetchAll()) : [], 
        )); 
    } 
 
    private function invalidateConfigCache(): void 
    { 
        // invalidate container config cache if debug flag on kernel is set 
        $systemConfigFile = Config::locateConfigFile('system.yml'); 
        if ($systemConfigFile) { 
            touch($systemConfigFile); 
        } 
    } 
 
    /** 
     * @TODO to be removed in Pimcore 11 
     * 
     * @internal 
     * 
     * @param array $containerConfig 
     * @param string $configId 
     * 
     * @return array 
     */ 
    public static function getStorageConfigurationCompatibilityLayer( 
        array $containerConfig, 
        string $configId, 
        string $storagePathEnvVarName, 
        string $writeTargetEnvVarName, 
    ): array { 
        $storageConfig = $containerConfig['config_location'][$configId]; 
 
        if (isset($_SERVER[$writeTargetEnvVarName])) { 
            trigger_deprecation('pimcore/pimcore', '10.6', 
                sprintf('Setting write targets (%s) using environment variables is deprecated, instead use the symfony config. It will be removed in Pimcore 11.', $writeTargetEnvVarName)); 
 
            $storageConfig['write_target']['type'] = $_SERVER[$writeTargetEnvVarName]; 
        } 
 
        if (isset($_SERVER[$storagePathEnvVarName])) { 
            trigger_deprecation('pimcore/pimcore', '10.6', 
                sprintf('Setting storage directory (%s) in the environment variables file is deprecated, instead use the symfony config. It will be removed in Pimcore 11.', $storagePathEnvVarName)); 
 
            $storageConfig['write_target']['options']['directory'] = $_SERVER[$storagePathEnvVarName]; 
        } 
 
        return $storageConfig; 
    } 
}