reload($defaultSettingsFiles, $userSettingsFile); } /** * Return setting section by reference. * * @param string $name * @return mixed */ public function &get($name) { if (!isset($this->mergedSettings[$name])) { $this->mergedSettings[$name] = array(); } $result =& $this->mergedSettings[$name]; return $result; } /** * Return setting section from a specific file, rather than the current merged settings. * * @param string $file The path of the file. Should be the path used in construction or reload(). * @param string $name The name of the section to access. */ public function getFrom($file, $name) { return @$this->settingsChain[$file][$name]; } /** * Sets a setting value. * * @param string $name * @param mixed $value */ public function set($name, $value) { $this->mergedSettings[$name] = $value; } /** * Returns all settings. Changes made to the array result will be reflected in the * IniFileChain instance. * * @return array */ public function &getAll() { return $this->mergedSettings; } /** * Dumps the current in-memory setting values to a string in INI format and returns it. * * @param string $header The header of the output INI file. * @return string The dumped INI contents. */ public function dump($header = '') { return $this->dumpSettings($this->mergedSettings, $header); } /** * Writes the difference of the in-memory setting values and the on-disk user settings file setting * values to a string in INI format, and returns it. * * If a config section is identical to the default settings section (as computed by merging * all default setting files), it is not written to the user settings file. * * @param string $header The header of the INI output. * @return string The dumped INI contents. */ public function dumpChanges($header = '') { $userSettingsFile = $this->getUserSettingsFile(); $defaultSettings = $this->getMergedDefaultSettings(); $existingMutableSettings = $this->settingsChain[$userSettingsFile]; $dirty = false; $configToWrite = array(); foreach ($this->mergedSettings as $sectionName => $changedSection) { $existingMutableSection = @$existingMutableSettings[$sectionName] ?: array(); // remove default values from both (they should not get written to local) if (isset($defaultSettings[$sectionName])) { $changedSection = $this->arrayUnmerge($defaultSettings[$sectionName], $changedSection); $existingMutableSection = $this->arrayUnmerge($defaultSettings[$sectionName], $existingMutableSection); } // if either local/config have non-default values and the other doesn't, // OR both have values, but different values, we must write to config.ini.php if (empty($changedSection) xor empty($existingMutableSection) || (!empty($changedSection) && !empty($existingMutableSection) && self::compareElements($changedSection, $existingMutableSection)) ) { $dirty = true; } $configToWrite[$sectionName] = $changedSection; } if ($dirty) { // sort config sections by how early they appear in the file chain $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; } } /** * Reloads settings from disk. */ public function reload($defaultSettingsFiles = array(), $userSettingsFile = null) { if (!empty($defaultSettingsFiles) || !empty($userSettingsFile) ) { $this->resetSettingsChain($defaultSettingsFiles, $userSettingsFile); } $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]); } } $this->mergedSettings = $this->mergeFileSettings(); } private function resetSettingsChain($defaultSettingsFiles, $userSettingsFile) { $this->settingsChain = array(); 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) ?: array(); foreach ($userSettings as $sectionName => $section) { if (!isset($mergedSettings[$sectionName])) { $mergedSettings[$sectionName] = $section; } else { // the last user settings file completely overwrites INI sections. the other files in the chain // can add to array options $mergedSettings[$sectionName] = array_merge($mergedSettings[$sectionName], $section); } } return $mergedSettings; } protected function getMergedDefaultSettings() { $userSettingsFile = $this->getUserSettingsFile(); $mergedSettings = array(); 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() { // the user settings file is the last key in $settingsChain end($this->settingsChain); return key($this->settingsChain); } /** * Comparison function * * @param mixed $elem1 * @param mixed $elem2 * @return int; */ 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; } /** * Compare arrays and return difference, such that: * * $modified = array_merge($original, $difference); * * @param array $original original array * @param array $modified modified array * @return array differences between original and modified */ public function arrayUnmerge($original, $modified) { // return key/value pairs for keys in $modified but not in $original // return key/value pairs for keys in both $modified and $original, but values differ // ignore keys that are in $original but not in $modified return array_udiff_assoc($modified, $original, array(__CLASS__, 'compareElements')); } /** * array_merge_recursive does indeed merge arrays, but it converts values with duplicate * keys to arrays rather than overwriting the value in the first array with the duplicate * value in the second array, as array_merge does. I.e., with array_merge_recursive, * this happens (documented behavior): * * array_merge_recursive(array('key' => 'org value'), array('key' => 'new value')); * => array('key' => array('org value', 'new value')); * * array_merge_recursive_distinct does not change the datatypes of the values in the arrays. * Matching keys' values in the second array overwrite those in the first array, as is the * case with array_merge, i.e.: * * array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value')); * => array('key' => array('new value')); * * Parameters are passed by reference, though only for performance reasons. They're not * altered by this function. * * @param array $array1 * @param array $array2 * @return array * @author Daniel * @author Gabriel Sobrinho */ private function array_merge_recursive_distinct(array &$array1, array &$array2) { $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 for use in closure. */ public function findIndexOfFirstFileWithSection($sectionName) { $count = 0; foreach ($this->settingsChain as $file => $settings) { if (isset($settings[$sectionName])) { break; } ++$count; } return $count; } /** * public for use in closure. */ 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); } /** * Encode HTML entities * * @param mixed $values * @return mixed */ 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; } /** * Decode HTML entities * * @param mixed $values * @return mixed */ 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) { $values = $this->encodeValues($values); $writer = new IniWriter(); return $writer->writeToString($values, $header); } }