| <?php |
|
|
| |
| |
| |
| |
| |
| |
|
|
| namespace Piwik\Config; |
|
|
| use Piwik\Common; |
| use Matomo\Ini\IniReader; |
| use Matomo\Ini\IniReadingException; |
| use Matomo\Ini\IniWriter; |
| use Piwik\Piwik; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| class IniFileChain |
| { |
| public const CONFIG_CACHE_KEY = 'config.ini'; |
| |
| |
| |
| |
| |
| |
| protected $settingsChain = []; |
|
|
| |
| |
| |
| |
| |
| protected $mergedSettings = []; |
|
|
| |
| |
| |
| |
| public function __construct(array $defaultSettingsFiles = [], $userSettingsFile = null) |
| { |
| $this->reload($defaultSettingsFiles, $userSettingsFile); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function &get($name) |
| { |
| if (!isset($this->mergedSettings[$name])) { |
| $this->mergedSettings[$name] = []; |
| } |
|
|
| $result = & $this->mergedSettings[$name]; |
| return $result; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function getFrom($file, $name) |
| { |
| return @$this->settingsChain[$file][$name]; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function set($name, $value) |
| { |
| $name = $this->replaceSectionInvalidChars($name); |
| if ($value !== null) { |
| $value = $this->replaceInvalidChars($value); |
| } |
|
|
| $this->mergedSettings[$name] = $value; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function &getAll() |
| { |
| return $this->mergedSettings; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function dump($header = '') |
| { |
| return $this->dumpSettings($this->mergedSettings, $header); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function dumpChanges($header = '') |
| { |
| $userSettingsFile = $this->getUserSettingsFile(); |
|
|
| $defaultSettings = $this->getMergedDefaultSettings(); |
| $existingMutableSettings = $this->settingsChain[$userSettingsFile]; |
|
|
| $dirty = false; |
|
|
| $configToWrite = []; |
| foreach ($this->mergedSettings as $sectionName => $changedSection) { |
| if (isset($existingMutableSettings[$sectionName])) { |
| $existingMutableSection = $existingMutableSettings[$sectionName]; |
| } else { |
| $existingMutableSection = []; |
| } |
|
|
| |
| if (isset($defaultSettings[$sectionName])) { |
| $changedSection = $this->arrayUnmerge($defaultSettings[$sectionName], $changedSection); |
| $existingMutableSection = $this->arrayUnmerge($defaultSettings[$sectionName], $existingMutableSection); |
| } |
|
|
| |
| |
| if ( |
| (empty($changedSection) xor empty($existingMutableSection)) |
| || self::compareElements($changedSection, $existingMutableSection) |
| ) { |
| $dirty = true; |
| } |
|
|
| $configToWrite[$sectionName] = $changedSection; |
| } |
|
|
| if ($dirty) { |
| |
| $self = $this; |
| uksort($configToWrite, function ($sectionNameLhs, $sectionNameRhs) use ($self) { |
| $lhsIndex = $self->findIndexOfFirstFileWithSection($sectionNameLhs); |
| $rhsIndex = $self->findIndexOfFirstFileWithSection($sectionNameRhs); |
|
|
| if ($lhsIndex == $rhsIndex) { |
| $lhsIndexInFile = $self->getIndexOfSectionInFile($lhsIndex, $sectionNameLhs); |
| $rhsIndexInFile = $self->getIndexOfSectionInFile($rhsIndex, $sectionNameRhs); |
|
|
| if ($lhsIndexInFile == $rhsIndexInFile) { |
| return 0; |
| } elseif ($lhsIndexInFile < $rhsIndexInFile) { |
| return -1; |
| } else { |
| return 1; |
| } |
| } elseif ($lhsIndex < $rhsIndex) { |
| return -1; |
| } else { |
| return 1; |
| } |
| }); |
|
|
| return $this->dumpSettings($configToWrite, $header); |
| } else { |
| return null; |
| } |
| } |
|
|
| |
| |
| |
| public function reload($defaultSettingsFiles = [], $userSettingsFile = null) |
| { |
| if ( |
| !empty($defaultSettingsFiles) |
| || !empty($userSettingsFile) |
| ) { |
| $this->resetSettingsChain($defaultSettingsFiles, $userSettingsFile); |
| } |
|
|
| $hasAbsoluteConfigFile = !empty($userSettingsFile) && strpos($userSettingsFile, DIRECTORY_SEPARATOR) === 0; |
| $useConfigCache = !empty($GLOBALS['ENABLE_CONFIG_PHP_CACHE']) && $hasAbsoluteConfigFile; |
|
|
| if ($useConfigCache && is_file($userSettingsFile)) { |
| $cache = new Cache(); |
| $values = $cache->doFetch(self::CONFIG_CACHE_KEY); |
|
|
| if ( |
| !empty($values) |
| && isset($values['mergedSettings']) |
| && isset($values['settingsChain'][$userSettingsFile]) |
| ) { |
| $this->mergedSettings = $values['mergedSettings']; |
| $this->settingsChain = $values['settingsChain']; |
| return; |
| } |
| } |
|
|
| $reader = new IniReader(); |
| foreach ($this->settingsChain as $file => $ignore) { |
| if (is_readable($file)) { |
| try { |
| $contents = $reader->readFile($file); |
| $this->settingsChain[$file] = $this->decodeValues($contents); |
| } catch (IniReadingException $ex) { |
| throw new IniReadingException('Unable to read INI file {' . $file . '}: ' . $ex->getMessage() . "\n Your host may have disabled parse_ini_file()."); |
| } |
|
|
| $this->decodeValues($this->settingsChain[$file]); |
| } |
| } |
|
|
| $merged = $this->mergeFileSettings(); |
| |
| |
| $this->mergedSettings = $this->copy($merged); |
|
|
| if (!empty($GLOBALS['MATOMO_MODIFY_CONFIG_SETTINGS']) && !empty($this->mergedSettings)) { |
| $this->mergedSettings = call_user_func($GLOBALS['MATOMO_MODIFY_CONFIG_SETTINGS'], $this->mergedSettings); |
| } |
|
|
| if ( |
| $useConfigCache |
| && !empty($this->mergedSettings) |
| && !empty($this->settingsChain) |
| && Cache::hasHostConfig($this->mergedSettings) |
| ) { |
| $ttlOneHour = 3600; |
| $cache = new Cache(); |
| if ($cache->isValidHost($this->mergedSettings)) { |
| |
| $data = ['mergedSettings' => $this->mergedSettings, 'settingsChain' => $this->settingsChain]; |
| $cache->doSave(self::CONFIG_CACHE_KEY, $data, $ttlOneHour); |
| } |
| } |
| } |
|
|
| public function deleteConfigCache() |
| { |
| if (!empty($GLOBALS['ENABLE_CONFIG_PHP_CACHE'])) { |
| $cache = new Cache(); |
| $cache->doDelete(IniFileChain::CONFIG_CACHE_KEY); |
| } |
| } |
|
|
| private function copy($merged) |
| { |
| $copy = []; |
| foreach ($merged as $index => $value) { |
| if (is_array($value)) { |
| $copy[$index] = $this->copy($value); |
| } else { |
| $copy[$index] = $value; |
| } |
| } |
| return $copy; |
| } |
|
|
| private function resetSettingsChain($defaultSettingsFiles, $userSettingsFile) |
| { |
| $this->settingsChain = []; |
|
|
| if (!empty($defaultSettingsFiles)) { |
| foreach ($defaultSettingsFiles as $file) { |
| $this->settingsChain[$file] = null; |
| } |
| } |
|
|
| if (!empty($userSettingsFile)) { |
| $this->settingsChain[$userSettingsFile] = null; |
| } |
| } |
|
|
| protected function mergeFileSettings() |
| { |
| $mergedSettings = $this->getMergedDefaultSettings(); |
|
|
| $userSettings = end($this->settingsChain) ?: []; |
| foreach ($userSettings as $sectionName => $section) { |
| if (!isset($mergedSettings[$sectionName])) { |
| $mergedSettings[$sectionName] = $section; |
| } else { |
| |
| |
| $mergedSettings[$sectionName] = array_merge($mergedSettings[$sectionName], $section); |
| } |
| } |
|
|
| return $mergedSettings; |
| } |
|
|
| protected function getMergedDefaultSettings() |
| { |
| $userSettingsFile = $this->getUserSettingsFile(); |
|
|
| $mergedSettings = []; |
| foreach ($this->settingsChain as $file => $settings) { |
| if ( |
| $file == $userSettingsFile |
| || empty($settings) |
| ) { |
| continue; |
| } |
|
|
| foreach ($settings as $sectionName => $section) { |
| if (!isset($mergedSettings[$sectionName])) { |
| $mergedSettings[$sectionName] = $section; |
| } else { |
| $mergedSettings[$sectionName] = $this->array_merge_recursive_distinct($mergedSettings[$sectionName], $section); |
| } |
| } |
| } |
| return $mergedSettings; |
| } |
|
|
| protected function getUserSettingsFile() |
| { |
| |
| end($this->settingsChain); |
| return key($this->settingsChain); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| public static function compareElements($elem1, $elem2) |
| { |
| if (is_array($elem1)) { |
| if (is_array($elem2)) { |
| return strcmp(serialize($elem1), serialize($elem2)); |
| } |
|
|
| return 1; |
| } |
|
|
| if (is_array($elem2)) { |
| return -1; |
| } |
|
|
| if ((string)$elem1 === (string)$elem2) { |
| return 0; |
| } |
|
|
| return ((string)$elem1 > (string)$elem2) ? 1 : -1; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function arrayUnmerge($original, $modified) |
| { |
| |
| |
| |
|
|
| if (empty($original) || !is_array($original)) { |
| $original = []; |
| } |
|
|
| if (empty($modified) || !is_array($modified)) { |
| $modified = []; |
| } |
|
|
| return array_udiff_assoc($modified, $original, [__CLASS__, 'compareElements']); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| private function array_merge_recursive_distinct(array &$array1, array &$array2) // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps |
| { |
| $merged = $array1; |
| foreach ($array2 as $key => &$value) { |
| if (is_array($value) && isset($merged [$key]) && is_array($merged [$key])) { |
| $merged [$key] = $this->array_merge_recursive_distinct($merged [$key], $value); |
| } else { |
| $merged [$key] = $value; |
| } |
| } |
| return $merged; |
| } |
|
|
| |
| |
| |
| public function findIndexOfFirstFileWithSection($sectionName) |
| { |
| $count = 0; |
| foreach ($this->settingsChain as $file => $settings) { |
| if (isset($settings[$sectionName])) { |
| break; |
| } |
|
|
| ++$count; |
| } |
| return $count; |
| } |
|
|
| |
| |
| |
| public function getIndexOfSectionInFile($fileIndex, $sectionName) |
| { |
| reset($this->settingsChain); |
| for ($i = 0; $i != $fileIndex; ++$i) { |
| next($this->settingsChain); |
| } |
|
|
| $settingsData = current($this->settingsChain); |
| if (empty($settingsData)) { |
| return -1; |
| } |
|
|
| $settingsDataSectionNames = array_keys($settingsData); |
|
|
| return array_search($sectionName, $settingsDataSectionNames); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| protected function encodeValues(&$values) |
| { |
| if (is_array($values)) { |
| foreach ($values as &$value) { |
| $value = $this->encodeValues($value); |
| } |
| } elseif (is_float($values)) { |
| $values = Common::forceDotAsSeparatorForDecimalPoint($values); |
| } elseif (is_string($values)) { |
| $values = htmlentities($values, ENT_COMPAT, 'UTF-8'); |
| $values = str_replace('$', '$', $values); |
| } |
| return $values; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| protected function decodeValues(&$values) |
| { |
| if (is_array($values)) { |
| foreach ($values as &$value) { |
| $value = $this->decodeValues($value); |
| } |
| return $values; |
| } elseif (is_string($values)) { |
| return html_entity_decode($values, ENT_COMPAT, 'UTF-8'); |
| } |
| return $values; |
| } |
|
|
| private function dumpSettings($values, $header) |
| { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| Piwik::postEvent('Config.beforeSave', [&$values]); |
| $values = $this->encodeValues($values); |
|
|
| $writer = new IniWriter(); |
| return $writer->writeToString($values, $header); |
| } |
|
|
| private function replaceInvalidChars($value) |
| { |
| if (is_array($value)) { |
| $result = []; |
| foreach ($value as $key => $arrayValue) { |
| $key = $this->replaceInvalidChars($key); |
| if (is_array($arrayValue)) { |
| $arrayValue = $this->replaceInvalidChars($arrayValue); |
| } |
|
|
| $result[$key] = $arrayValue; |
| } |
| return $result; |
| } else { |
| return preg_replace('/[^a-zA-Z0-9_\[\]-]/', '', $value); |
| } |
| } |
|
|
| private function replaceSectionInvalidChars($value) |
| { |
| return preg_replace('/[^a-zA-Z0-9_-]/', '', $value); |
| } |
| } |
|
|