diff --git a/core/Config.php b/core/Config.php index f0b5c3ed264d09f97313bab6b586f827776df57e..91853de73380cdbac343ef02b40659071a765ab7 100644 --- a/core/Config.php +++ b/core/Config.php @@ -10,10 +10,9 @@ namespace Piwik; use Exception; +use Piwik\Config\IniFileChain; use Piwik\Config\ConfigNotFoundException; -use Piwik\Ini\IniReader; use Piwik\Ini\IniReadingException; -use Piwik\Ini\IniWriter; /** * Singleton that provides read & write access to Piwik's INI configuration. @@ -51,11 +50,6 @@ class Config extends Singleton /** * @var boolean */ - protected $initialized = false; - protected $configGlobal = array(); - protected $configLocal = array(); - protected $configCommon = array(); - protected $configCache = array(); protected $pathGlobal = null; protected $pathCommon = null; protected $pathLocal = null; @@ -66,22 +60,19 @@ class Config extends Singleton protected $doNotWriteConfigInTests = false; /** - * @var IniReader + * @var IniFileChain */ - private $iniReader; + protected $settings; - /** - * @var IniWriter - */ - private $iniWriter; + private $initialized = false; public function __construct($pathGlobal = null, $pathLocal = null, $pathCommon = null) { $this->pathGlobal = $pathGlobal ?: self::getGlobalConfigPath(); $this->pathCommon = $pathCommon ?: self::getCommonConfigPath(); $this->pathLocal = $pathLocal ?: self::getLocalConfigPath(); - $this->iniReader = new IniReader(); - $this->iniWriter = new IniWriter(); + + $this->settings = new IniFileChain(); } /** @@ -131,30 +122,28 @@ class Config extends Singleton $this->pathGlobal = $pathGlobal ?: Config::getGlobalConfigPath(); $this->pathCommon = $pathCommon ?: Config::getCommonConfigPath(); - $this->init(); + $this->reload(); - if (isset($this->configGlobal['database_tests']) - || isset($this->configLocal['database_tests']) - ) { - $this->__get('database_tests'); - $this->configCache['database'] = $this->configCache['database_tests']; + $databaseTestsSettings = $this->database_tests; + if (!empty($databaseTestsSettings)) { + $this->database = $databaseTestsSettings; } // Ensure local mods do not affect tests if (empty($pathGlobal)) { - $this->configCache['Debug'] = $this->configGlobal['Debug']; - $this->configCache['mail'] = $this->configGlobal['mail']; - $this->configCache['General'] = $this->configGlobal['General']; - $this->configCache['Segments'] = $this->configGlobal['Segments']; - $this->configCache['Tracker'] = $this->configGlobal['Tracker']; - $this->configCache['Deletelogs'] = $this->configGlobal['Deletelogs']; - $this->configCache['Deletereports'] = $this->configGlobal['Deletereports']; - $this->configCache['Development'] = $this->configGlobal['Development']; + $this->Debug = $this->settings->getFrom($this->pathGlobal, 'Debug'); + $this->mail = $this->settings->getFrom($this->pathGlobal, 'mail'); + $this->General = $this->settings->getFrom($this->pathGlobal, 'General'); + $this->Segments = $this->settings->getFrom($this->pathGlobal, 'Segments'); + $this->Tracker = $this->settings->getFrom($this->pathGlobal, 'Tracker'); + $this->Deletelogs = $this->settings->getFrom($this->pathGlobal, 'Deletelogs'); + $this->Deletereports = $this->settings->getFrom($this->pathGlobal, 'Deletereports'); + $this->Development = $this->settings->getFrom($this->pathGlobal, 'Development'); } // for unit tests, we set that no plugin is installed. This will force // the test initialization to create the plugins tables, execute ALTER queries, etc. - $this->configCache['PluginsInstalled'] = array('PluginsInstalled' => array()); + $this->PluginsInstalled = array('PluginsInstalled' => array()); } /** @@ -301,59 +290,56 @@ class Config extends Singleton /** * Clear in-memory configuration so it can be reloaded + * @deprecated since v2.12.0 */ public function clear() { - $this->configGlobal = array(); - $this->configLocal = array(); - $this->configCommon = array(); - $this->configCache = array(); $this->initialized = false; + $this->reload(); } /** * Read configuration from files into memory * * @throws Exception if local config file is not readable; exits for other errors + * @deprecated since v2.12.0 */ public function init() { - $this->clear(); + $this->reload(); + } + + /** + * Reloads config data from disk. + * + * @throws \Exception if the global config file is not found and this is a tracker request, or + * if the local config file is not found and this is NOT a tracker request. + */ + public function reload() + { $this->initialized = true; - $reportError = SettingsServer::isTrackerApiRequest(); + + $inTrackerRequest = SettingsServer::isTrackerApiRequest(); // read defaults from global.ini.php - if (!is_readable($this->pathGlobal) && $reportError) { + if (!is_readable($this->pathGlobal) && $inTrackerRequest) { throw new Exception(Piwik::translate('General_ExceptionConfigurationFileNotFound', array($this->pathGlobal))); } try { - $this->configGlobal = $this->iniReader->readFile($this->pathGlobal); + $this->settings->reload(array($this->pathGlobal, $this->pathCommon), $this->pathLocal); } catch (IniReadingException $e) { - if ($reportError) { - throw new Exception(Piwik::translate('General_ExceptionUnreadableFileDisabledMethod', array($this->pathGlobal, "parse_ini_file()"))); + if ($inTrackerRequest) { + throw $e; } } - try { - if (file_exists($this->pathCommon)) { - $this->configCommon = $this->iniReader->readFile($this->pathCommon); - } else { - $this->configCommon = false; - } - } catch (IniReadingException $e) { - $this->configCommon = false; - } + // decode section datas + $this->decodeValues($this->settings->getAll()); // Check config.ini.php last - $this->checkLocalConfigFound(); - - try { - $this->configLocal = $this->iniReader->readFile($this->pathLocal); - } catch (IniReadingException $e) { - if ($reportError) { - throw new Exception(Piwik::translate('General_ExceptionUnreadableFileDisabledMethod', array($this->pathLocal, "parse_ini_file()"))); - } + if (!$inTrackerRequest) { + $this->checkLocalConfigFound(); } } @@ -381,7 +367,7 @@ class Config extends Singleton * @param mixed $values * @return mixed */ - protected function decodeValues($values) + protected function decodeValues(&$values) { if (is_array($values)) { foreach ($values as &$value) { @@ -400,7 +386,7 @@ class Config extends Singleton * @param mixed $values * @return mixed */ - protected function encodeValues($values) + protected function encodeValues(&$values) { if (is_array($values)) { foreach ($values as &$value) { @@ -427,60 +413,26 @@ class Config extends Singleton public function &__get($name) { if (!$this->initialized) { - $this->init(); + $this->reload(array($this->pathGlobal, $this->pathCommon), $this->pathLocal); // must be called here, not in init(), since setTestEnvironment() calls init(). (this avoids // infinite recursion) - Piwik::postTestEvent('Config.createConfigSingleton', - array($this, &$this->configCache, &$this->configLocal)); - } - - // check cache for merged section - if (isset($this->configCache[$name])) { - $tmp =& $this->configCache[$name]; - return $tmp; - } - - $section = $this->getFromGlobalConfig($name); - $sectionCommon = $this->getFromCommonConfig($name); - if (empty($section) && !empty($sectionCommon)) { - $section = $sectionCommon; - } elseif (!empty($section) && !empty($sectionCommon)) { - $section = $this->array_merge_recursive_distinct($section, $sectionCommon); + $allSettings =& $this->settings->getAll(); + Piwik::postTestEvent('Config.createConfigSingleton', array($this, &$allSettings)); } - if (isset($this->configLocal[$name])) { - // local settings override the global defaults - $section = $section - ? array_merge($section, $this->configLocal[$name]) - : $this->configLocal[$name]; - } - - if ($section === null) { - $section = array(); - } - - // cache merged section for later - $this->configCache[$name] = $this->decodeValues($section); - $tmp =& $this->configCache[$name]; - - return $tmp; + $section =& $this->settings->get($name); + return $section; } public function getFromGlobalConfig($name) { - if (isset($this->configGlobal[$name])) { - return $this->configGlobal[$name]; - } - return null; + return $this->settings->getFrom($this->pathGlobal, $name); } public function getFromCommonConfig($name) { - if (isset($this->configCommon[$name])) { - return $this->configCommon[$name]; - } - return null; + return $this->settings->getFrom($this->pathCommon, $name); } /** @@ -492,125 +444,32 @@ class Config extends Singleton */ public function __set($name, $value) { - $this->configCache[$name] = $value; - } - - /** - * 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 array_unmerge($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')); + $this->settings->set($name, $value); } /** * Dump config * - * @param array $configLocal - * @param array $configGlobal - * @param array $configCommon - * @param array $configCache - * @return string + * @return string|null + * @throws \Exception */ - public function dumpConfig($configLocal, $configGlobal, $configCommon, $configCache) + public function dumpConfig() { - $dirty = false; - - if (!$configCache) { - return false; - } - - $configToWrite = array(); - - // If there is a common.config.ini.php, this will ensure config.ini.php does not duplicate its values - if (!empty($configCommon)) { - $configGlobal = $this->array_merge_recursive_distinct($configGlobal, $configCommon); - } - - if ($configLocal) { - foreach ($configLocal as $name => $section) { - if (!isset($configCache[$name])) { - $configCache[$name] = $this->decodeValues($section); - } - } - } - - $sectionNames = array_unique(array_merge(array_keys($configGlobal), array_keys($configCache))); - - foreach ($sectionNames as $section) { - if (!isset($configCache[$section])) { - continue; - } + $this->encodeValues($this->settings->getAll()); - // Only merge if the section exists in global.ini.php (in case a section only lives in config.ini.php) - - // get local and cached config - $local = isset($configLocal[$section]) ? $configLocal[$section] : array(); - $config = $configCache[$section]; - - // remove default values from both (they should not get written to local) - if (isset($configGlobal[$section])) { - $config = $this->array_unmerge($configGlobal[$section], $configCache[$section]); - $local = $this->array_unmerge($configGlobal[$section], $local); - } + try { + $header = "; <?php exit; ?> DO NOT REMOVE THIS LINE\n"; + $header .= "; file automatically generated or modified by Piwik; you can manually override the default values in global.ini.php by redefining them in this file.\n"; + $dumpedString = $this->settings->dumpChanges($header); - // 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($local) xor empty($config) - || (!empty($local) - && !empty($config) - && self::compareElements($config, $configLocal[$section])) - ) { - $dirty = true; - } + $this->decodeValues($this->settings->getAll()); + } catch (Exception $ex) { + $this->decodeValues($this->settings->getAll()); - $configToWrite[$section] = array_map(array($this, 'encodeValues'), $config); + throw $ex; } - if ($dirty) { - $header = "; <?php exit; ?> DO NOT REMOVE THIS LINE\n"; - $header .= "; file automatically generated or modified by Piwik; you can manually override the default values in global.ini.php by redefining them in this file.\n"; - return $this->iniWriter->writeToString($configToWrite, $header); - } - return false; + return $dumpedString; } /** @@ -625,22 +484,24 @@ class Config extends Singleton * * @throws \Exception if config file not writable */ - protected function writeConfig($configLocal, $configGlobal, $configCommon, $configCache, $pathLocal, $clear = true) + protected function writeConfig($clear = true) { if ($this->doNotWriteConfigInTests) { return; } - $output = $this->dumpConfig($configLocal, $configGlobal, $configCommon, $configCache); - if ($output !== false) { - $success = @file_put_contents($pathLocal, $output); - if (!$success) { + $output = $this->dumpConfig(); + if ($output !== null + && $output !== false + ) { + $success = @file_put_contents($this->pathLocal, $output); + if ($success === false) { throw $this->getConfigNotWritableException(); } } if ($clear) { - $this->clear(); + $this->reload(); } } @@ -652,7 +513,7 @@ class Config extends Singleton */ public function forceSave() { - $this->writeConfig($this->configLocal, $this->configGlobal, $this->configCommon, $this->configCache, $this->pathLocal); + $this->writeConfig(); } /** @@ -663,42 +524,4 @@ class Config extends Singleton $path = "config/" . basename($this->pathLocal); return new Exception(Piwik::translate('General_ConfigFileIsNotWritable', array("(" . $path . ")", ""))); } - - /** - * 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 <daniel (at) danielsmedegaardbuus (dot) dk> - * @author Gabriel Sobrinho <gabriel (dot) sobrinho (at) gmail (dot) com> - */ - 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; - } } \ No newline at end of file diff --git a/core/Config/IniFileChain.php b/core/Config/IniFileChain.php new file mode 100644 index 0000000000000000000000000000000000000000..247f97c30969dbe077835a43bcf24629ca37fdb2 --- /dev/null +++ b/core/Config/IniFileChain.php @@ -0,0 +1,370 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ +namespace Piwik\Config; + +use Piwik\Ini\IniReader; +use Piwik\Ini\IniReadingException; +use Piwik\Ini\IniWriter; +use Piwik\Piwik; + +/** + * Manages a list of INI files where the settings in each INI file merge with or override the + * settings in the previous INI file. + * + * The IniFileChain class manages two types of INI files: multiple default setting files and one + * user settings file. + * + * The default setting files (for example, global.ini.php & common.ini.php) hold the default setting values. + * The settings in these files are merged recursively, however, array settings in one file will still + * overwrite settings in the previous file. + * + * Default settings files cannot be modified through the IniFileChain class. + * + * The user settings file (for example, config.ini.php) holds the actual setting values. Settings in the + * user settings files overwrite other settings. So array settings will not merge w/ previous values. + */ +class IniFileChain +{ + /** + * Maps INI file names with their parsed contents. The order of the files signifies the order + * in the chain. Files with lower index are overwritten/merged with files w/ a higher index. + * + * @var array + */ + protected $settingsChain = array(); + + /** + * The merged INI settings. + * + * @var array + */ + protected $mergedSettings = array(); + + /** + * Constructor. + * + * @param string[] $defaultSettingsFiles The list of paths to INI files w/ the default setting values. + * @param string|null $userSettingsFile The path to the user settings file. + */ + public function __construct(array $defaultSettingsFiles = array(), $userSettingsFile = null) + { + $this->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 = '') + { + $writer = new IniWriter(); + return $writer->writeToString($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) { + return 0; + } else if ($lhsIndex < $rhsIndex) { + return -1; + } else { + return 1; + } + }); + + $writer = new IniWriter(); + return $writer->writeToString($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 { + $this->settingsChain[$file] = $reader->readFile($file); + } catch (IniReadingException $ex) { + $message = Piwik::translate('General_ExceptionUnreadableFileDisabledMethod', array($file, "parse_ini_file()")); + throw new IniReadingException($message, $code = 0, $ex); + } + } + } + + $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 <daniel (at) danielsmedegaardbuus (dot) dk> + * @author Gabriel Sobrinho <gabriel (dot) sobrinho (at) gmail (dot) com> + */ + 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 function findIndexOfFirstFileWithSection($sectionName) + { + $count = 0; + foreach ($this->settingsChain as $file => $settings) { + if (isset($settings[$sectionName])) { + break; + } + + ++$count; + } + return $count; + } +} \ No newline at end of file diff --git a/tests/PHPUnit/Integration/TrackerTest.php b/tests/PHPUnit/Integration/TrackerTest.php index 9f261074eaaca41708590707701b94019dc22347..260e8d721b7e4698074c586914350c7e8ec6320d 100644 --- a/tests/PHPUnit/Integration/TrackerTest.php +++ b/tests/PHPUnit/Integration/TrackerTest.php @@ -148,7 +148,11 @@ class TrackerTest extends IntegrationTestCase $this->assertTrue(Config::getInstance()->existsLocalConfig()); $this->removeConfigFile(); - Config::getInstance()->clear(); + try { + Config::getInstance()->reload(); + } catch (\Exception $ex) { + // ignore config file not found exception + } $this->assertFalse(Config::getInstance()->existsLocalConfig()); diff --git a/tests/PHPUnit/TestingEnvironment.php b/tests/PHPUnit/TestingEnvironment.php index 3acc202a911b9af8ea672c7569b97df618f387d1..388efa1c1f737869ef1d45660a469f256d9fbd9d 100644 --- a/tests/PHPUnit/TestingEnvironment.php +++ b/tests/PHPUnit/TestingEnvironment.php @@ -146,9 +146,11 @@ class Piwik_TestingEnvironment \Piwik\Profiler::setupProfilerXHProf($mainRun = false, $setupDuringTracking = true); } - Config::setSingletonInstance(new Config( - $testingEnvironment->configFileGlobal, $testingEnvironment->configFileLocal, $testingEnvironment->configFileCommon - )); + if ($testingEnvironment->dontUseTestConfig) { + Config::setSingletonInstance(new Config( + $testingEnvironment->configFileGlobal, $testingEnvironment->configFileLocal, $testingEnvironment->configFileCommon + )); + } // Apply DI config from the fixture if ($testingEnvironment->fixtureClass) { @@ -172,11 +174,11 @@ class Piwik_TestingEnvironment } }); if (!$testingEnvironment->dontUseTestConfig) { - Piwik::addAction('Config.createConfigSingleton', function(Config $config, &$cache, &$local) use ($testingEnvironment) { + Piwik::addAction('Config.createConfigSingleton', function(Config $config, &$cache) use ($testingEnvironment) { $config->setTestEnvironment($testingEnvironment->configFileLocal, $testingEnvironment->configFileGlobal, $testingEnvironment->configFileCommon); if ($testingEnvironment->configFileLocal) { - $local['General']['session_save_handler'] = 'dbtable'; + $config->General['session_save_handler'] = 'dbtable'; } $manager = \Piwik\Plugin\Manager::getInstance(); @@ -187,19 +189,19 @@ class Piwik_TestingEnvironment sort($pluginsToLoad); - $local['Plugins'] = array('Plugins' => $pluginsToLoad); + $config->Plugins = array('Plugins' => $pluginsToLoad); - $local['log']['log_writers'] = array('file'); + $config->log['log_writers'] = array('file'); $manager->unloadPlugins(); // TODO: replace this and below w/ configOverride use if ($testingEnvironment->tablesPrefix) { - $cache['database']['tables_prefix'] = $testingEnvironment->tablesPrefix; + $config->database['tables_prefix'] = $testingEnvironment->tablesPrefix; } if ($testingEnvironment->dbName) { - $cache['database']['dbname'] = $testingEnvironment->dbName; + $config->database['dbname'] = $testingEnvironment->dbName; } if ($testingEnvironment->configOverride) { diff --git a/tests/PHPUnit/Unit/Config/IniFileChainTest.php b/tests/PHPUnit/Unit/Config/IniFileChainTest.php new file mode 100644 index 0000000000000000000000000000000000000000..b806febef68e7898b2b5bf1afb109fb0e918748f --- /dev/null +++ b/tests/PHPUnit/Unit/Config/IniFileChainTest.php @@ -0,0 +1,285 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ +namespace Piwik\Tests\Unit\Config; + +use PHPUnit_Framework_TestCase; +use Piwik\Config\IniFileChain; + +/** + * @group Core + */ +class IniFileChainTest extends PHPUnit_Framework_TestCase +{ + /** + * Data provider for testCompareElements + */ + public function getCompareElementsData() + { + return array( + array('string = string', array( + 'a', 'a', 0, + )), + array('string > string', array( + 'b', 'a', 1, + )), + array('string < string', array( + 'a', 'b', -1, + )), + array('string vs array', array( + 'a', array('a'), -1, + )), + array('array vs string', array( + array('a'), 'a', 1, + )), + array('array = array', array( + array('a'), array('a'), 0, + )), + array('array > array', array( + array('b'), array('a'), 1, + )), + array('array < array', array( + array('a'), array('b'), -1, + )), + ); + } + + /** + * @dataProvider getCompareElementsData + */ + public function test_compareElements_CorrectlyComparesElements($description, $test) + { + list($a, $b, $expected) = $test; + + $result = IniFileChain::compareElements($a, $b); + $this->assertEquals($expected, $result, $description); + } + + /** + * Dataprovider for testArrayUnmerge + * @return array + */ + public function getArrayUnmergeData() + { + return array( + array('description of test', array( + array(), + array(), + )), + array('override with empty', array( + array('login' => 'root', 'password' => 'b33r'), + array('password' => ''), + )), + array('override with non-empty', array( + array('login' => 'root', 'password' => ''), + array('password' => 'b33r'), + )), + array('add element', array( + array('login' => 'root', 'password' => ''), + array('auth' => 'Login'), + )), + array('override with empty array', array( + array('headers' => ''), + array('headers' => array()), + )), + array('override with array', array( + array('headers' => ''), + array('headers' => array('Content-Length', 'Content-Type')), + )), + array('override an array', array( + array('headers' => array()), + array('headers' => array('Content-Length', 'Content-Type')), + )), + array('override similar arrays', array( + array('headers' => array('Content-Length', 'Set-Cookie')), + array('headers' => array('Content-Length', 'Content-Type')), + )), + array('override dyslexic arrays', array( + array('headers' => array('Content-Type', 'Content-Length')), + array('headers' => array('Content-Length', 'Content-Type')), + )), + ); + } + + /** + * @dataProvider getArrayUnmergeData + */ + public function test_ArrayUnmerge_ReturnsCorrectDiff($description, $test) + { + $configWriter = new IniFileChain(array(), null); + + list($a, $b) = $test; + + $combined = array_merge($a, $b); + + $diff = $configWriter->arrayUnmerge($a, $combined); + + // expect $b == $diff + $this->assertEquals(serialize($b), serialize($diff), $description); + } + + public function getMergingTestData() + { + return array( + array('test default settings are merged recursively', + array( // default settings + __DIR__ . '/test_files/default_settings_1.ini.php', + __DIR__ . '/test_files/empty.ini.php', + __DIR__ . '/test_files/default_settings_2.ini.php' + ), + __DIR__ . '/tests_files/empty.ini.php', // user settings + array( // expected + 'Section1' => array( + 'var1' => 'overriddenValue1', + 'var3' => array( + 'overriddenValue2', + 'overriddenValue3' + ) + ), + 'Section2' => array( + 'var4' => 'value5' + ) + ) + ), + + array('test user settings completely overwrite default', + array( // default settings + __DIR__ . '/test_files/default_settings_1.ini.php' + ), + __DIR__ . '/test_files/default_settings_2.ini.php', // user settings + array( // expected + 'Section1' => array( + 'var1' => 'overriddenValue1', + 'var3' => array( + 'overriddenValue2', + 'overriddenValue3' + ) + ), + 'Section2' => array( + 'var4' => 'value5' + ) + ) + ) + ); + } + + /** + * @dataProvider getMergingTestData + */ + public function test_construct_MergesFileData_Correctly($testDescription, $defaultSettingFiles, $userSettingsFile, $expected) + { + $fileChain = new IniFileChain($defaultSettingFiles, $userSettingsFile); + $this->assertEquals($expected, $fileChain->getAll(), "'$testDescription' failed"); + } + + /** + * @dataProvider getMergingTestData + */ + public function test_reload_MergesFileData_Correctly($testDescription, $defaultSettingsFiles, $userSettingsFile, $expected) + { + $fileChain = new IniFileChain(); + $fileChain->reload($defaultSettingsFiles, $userSettingsFile); + $this->assertEquals($expected, $fileChain->getAll(), "'$testDescription' failed"); + } + + public function test_get_ReturnsReferenceToSettingsSection() + { + $fileChain = new IniFileChain( + array(__DIR__ . '/test_files/default_settings_1.ini.php') + ); + + $data =& $fileChain->get('Section1'); + + $this->assertEquals(array('var1' => 'value2', 'var3' => array('value3', 'value4')), $data); + + $data['var1'] = 'changed'; + $data['var3'][] = 'newValue'; + + $this->assertEquals(array('var1' => 'changed', 'var3' => array('value3', 'value4', 'newValue')), $fileChain->get('Section1')); + } + + public function test_get_ReturnsReferenceToSettingsSection_EvenIfSettingsIsEmpty() + { + $fileChain = new IniFileChain(array(__DIR__ . '/test_files/empty.ini.php')); + + $data =& $fileChain->get('Section'); + $this->assertEquals(array(), $data); + + $data['var1'] = 'changed'; + $this->assertEquals(array('var1' => 'changed'), $fileChain->get('Section')); + } + + public function test_getAll_ReturnsReferenceToAllSettings() + { + $fileChain = new IniFileChain(); + + $data =& $fileChain->getAll(); + $data['var'] = 'value'; + + $this->assertEquals(array('var' => 'value'), $fileChain->getAll()); + } + + public function test_set_CorrectlySetsSettingValue() + { + $fileChain = new IniFileChain(); + + $fileChain->set('var', 'value'); + + $this->assertEquals(array('var' => 'value'), $fileChain->getAll()); + } + + public function test_getFrom_CorrectlyGetsSettingsFromFile_AndNotCurrentModifiedSettings() + { + $defaultSettingsPath = __DIR__ . '/test_files/default_settings_1.ini.php'; + + $fileChain = new IniFileChain( + array($defaultSettingsPath), + __DIR__ . '/test_files/default_settings_2.ini.php' + ); + + $this->assertEquals(array('var1' => 'value2', 'var3' => array('value3', 'value4')), $fileChain->getFrom($defaultSettingsPath, 'Section1')); + } + + public function getTestDataForDumpTest() + { + return array( + array( + array( // default settings + __DIR__ . '/test_files/default_settings_1.ini.php' + ), + __DIR__ . '/test_files/default_settings_2.ini.php', // user settings + "; some header\n", + "; some header\n[Section1]\nvar1 = \"overriddenValue1\"\nvar3[] = \"overriddenValue2\"\nvar3[] = \"overriddenValue3\"\n\n[Section2]\nvar4 = \"value5\"\n\n", + "; some header\n[Section1]\nvar1 = \"overriddenValue1\"\nvar3[] = \"overriddenValue2\"\nvar3[] = \"overriddenValue3\"\n\n" + ) + ); + } + + /** + * @dataProvider getTestDataForDumpTest + */ + public function test_dump_CorrectlyGeneratesIniString_ForAllCurrentSettings( + $defaultSettingsFiles, $userSettingsFile, $header, $expectedDump) + { + $fileChain = new IniFileChain($defaultSettingsFiles, $userSettingsFile); + + $actualOutput = $fileChain->dump($header); + $this->assertEquals($expectedDump, $actualOutput); + } + + /** + * @dataProvider getTestDataForDumpTest + */ + public function test_dumpChanges_CorrectlyGeneratesMinimalUserSettingsIniString( + $defaultSettingsFiles, $userSettingsFile, $header, $expectedDump, $expectedDumpChanges) + { + $fileChain = new IniFileChain($defaultSettingsFiles, $userSettingsFile); + + $actualOutput = $fileChain->dumpChanges($header); + $this->assertEquals($expectedDumpChanges, $actualOutput); + } +} \ No newline at end of file diff --git a/tests/PHPUnit/Unit/Config/test_files/default_settings_1.ini.php b/tests/PHPUnit/Unit/Config/test_files/default_settings_1.ini.php new file mode 100644 index 0000000000000000000000000000000000000000..2eb4fdd926f77beb99d44faef8aa0203ff03f799 --- /dev/null +++ b/tests/PHPUnit/Unit/Config/test_files/default_settings_1.ini.php @@ -0,0 +1,9 @@ +[Section1] + +var1 = "value2" +var3[] = "value3" +var3[] = "value4" + +[Section2] + +var4 = "value5" \ No newline at end of file diff --git a/tests/PHPUnit/Unit/Config/test_files/default_settings_2.ini.php b/tests/PHPUnit/Unit/Config/test_files/default_settings_2.ini.php new file mode 100644 index 0000000000000000000000000000000000000000..48e9e0f644652f83158127646e741265954cf1bf --- /dev/null +++ b/tests/PHPUnit/Unit/Config/test_files/default_settings_2.ini.php @@ -0,0 +1,6 @@ +[Section1] + +var3[] = "overriddenValue2" +var3[] = "overriddenValue3" + +var1 = "overriddenValue1" diff --git a/tests/PHPUnit/Unit/Config/test_files/empty.ini.php b/tests/PHPUnit/Unit/Config/test_files/empty.ini.php new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/PHPUnit/Unit/ConfigTest.php b/tests/PHPUnit/Unit/ConfigTest.php index 2fe32fec68cbb64a3354de73978b6aa78ad91575..d161743e34ce99e12a12d22962c0acd5309b5f5f 100644 --- a/tests/PHPUnit/Unit/ConfigTest.php +++ b/tests/PHPUnit/Unit/ConfigTest.php @@ -11,6 +11,34 @@ namespace Piwik\Tests\Unit; use PHPUnit_Framework_TestCase; use Piwik\Config; +class DumpConfigTestMockIniFileChain extends Config\IniFileChain +{ + public function __construct($settingsChain, $mergedSettings) + { + parent::__construct(); + + $this->settingsChain = $settingsChain; + $this->mergedSettings = $mergedSettings; + } +} + +class DumpConfigTestMockConfig extends Config +{ + public function __construct($configLocal, $configGlobal, $configCommon, $configCache) + { + parent::__construct(); + + $this->settings = new DumpConfigTestMockIniFileChain( + array( + $this->pathGlobal => $configGlobal, + $this->pathCommon => $configCommon, + $this->pathLocal => $configLocal, + ), + $configCache + ); + } +} + /** * @group Core */ @@ -115,113 +143,6 @@ class ConfigTest extends PHPUnit_Framework_TestCase Config::getInstance()->clear(); } - /** - * Dateprovider for testCompareElements - */ - public function getCompareElementsData() - { - return array( - array('string = string', array( - 'a', 'a', 0, - )), - array('string > string', array( - 'b', 'a', 1, - )), - array('string < string', array( - 'a', 'b', -1, - )), - array('string vs array', array( - 'a', array('a'), -1, - )), - array('array vs string', array( - array('a'), 'a', 1, - )), - array('array = array', array( - array('a'), array('a'), 0, - )), - array('array > array', array( - array('b'), array('a'), 1, - )), - array('array < array', array( - array('a'), array('b'), -1, - )), - ); - } - - /** - * @dataProvider getCompareElementsData - */ - public function testCompareElements($description, $test) - { - list($a, $b, $expected) = $test; - - $result = Config::compareElements($a, $b); - $this->assertEquals($expected, $result, $description); - } - - /** - * Dataprovider for testArrayUnmerge - * @return array - */ - public function getArrayUnmergeData() - { - return array( - array('description of test', array( - array(), - array(), - )), - array('override with empty', array( - array('login' => 'root', 'password' => 'b33r'), - array('password' => ''), - )), - array('override with non-empty', array( - array('login' => 'root', 'password' => ''), - array('password' => 'b33r'), - )), - array('add element', array( - array('login' => 'root', 'password' => ''), - array('auth' => 'Login'), - )), - array('override with empty array', array( - array('headers' => ''), - array('headers' => array()), - )), - array('override with array', array( - array('headers' => ''), - array('headers' => array('Content-Length', 'Content-Type')), - )), - array('override an array', array( - array('headers' => array()), - array('headers' => array('Content-Length', 'Content-Type')), - )), - array('override similar arrays', array( - array('headers' => array('Content-Length', 'Set-Cookie')), - array('headers' => array('Content-Length', 'Content-Type')), - )), - array('override dyslexic arrays', array( - array('headers' => array('Content-Type', 'Content-Length')), - array('headers' => array('Content-Length', 'Content-Type')), - )), - ); - } - - /** - * @dataProvider getArrayUnmergeData - */ - public function testArrayUnmerge($description, $test) - { - $configWriter = Config::getInstance(); - - list($a, $b) = $test; - - $combined = array_merge($a, $b); - - $diff = $configWriter->array_unmerge($a, $combined); - - // expect $b == $diff - $this->assertEquals(serialize($b), serialize($diff), $description); - } - /** * Dataprovider for testDumpConfig */ @@ -337,7 +258,7 @@ class ConfigTest extends PHPUnit_Framework_TestCase array('Tracker' => array('anonymize' => 1)), // local array('General' => array('debug' => 1)), // global array(), // common - array('General' => array('debug' => 2)), + array('General' => array('debug' => 2), 'Tracker' => array('anonymize' => 1)), $header . "[General]\ndebug = 2\n\n[Tracker]\nanonymize = 1\n\n", )), @@ -347,7 +268,8 @@ class ConfigTest extends PHPUnit_Framework_TestCase array('General' => array('debug' => 0), // global 'Tracker' => array('anonymize' => 0)), array(), // common - array('Tracker' => array('anonymize' => 2)), + array('Tracker' => array('anonymize' => 2), + 'General' => array('debug' => 1)), $header . "[General]\ndebug = 1\n\n[Tracker]\nanonymize = 2\n\n", )), @@ -357,7 +279,9 @@ class ConfigTest extends PHPUnit_Framework_TestCase array('General' => array('debug' => 0), // global 'Tracker' => array('anonymize' => 0)), array(), // common - array('Segment' => array('dimension' => 'foo')), + array('Segment' => array('dimension' => 'foo'), + 'Tracker' => array('anonymize' => 1), // local + 'General' => array('debug' => 1)), $header . "[General]\ndebug = 1\n\n[Tracker]\nanonymize = 1\n\n[Segment]\ndimension = \"foo\"\n\n", )), @@ -406,7 +330,8 @@ class ConfigTest extends PHPUnit_Framework_TestCase array('CommonCategory' => array('settingCommon' => 'common', // common 'settingCommon2' => 'common2')), array('CommonCategory' => array('settingCommon2' => 'common2', - 'newSetting' => 'newValue')), + 'newSetting' => 'newValue'), + 'General' => array('key' => 'value')), $header . "[General]\nkey = \"value\"\n\n[CommonCategory]\nnewSetting = \"newValue\"\n\n", )), @@ -417,7 +342,8 @@ class ConfigTest extends PHPUnit_Framework_TestCase array('CommonCategory' => array('settingCommon' => 'common', // common 'settingCommon2' => 'common2')), array('CommonCategory' => array('settingCommon2' => 'common2', - 'newSetting' => 'newValue')), + 'newSetting' => 'newValue'), + 'General' => array('key' => '$value', 'key2' => '${value}')), $header . "[General]\nkey = \"$value\"\nkey2 = \"${value}\"\n\n[CommonCategory]\nnewSetting = \"newValue\"\n\n", )), ); @@ -429,11 +355,11 @@ class ConfigTest extends PHPUnit_Framework_TestCase */ public function testDumpConfig($description, $test) { - $config = Config::getInstance(); - list($configLocal, $configGlobal, $configCommon, $configCache, $expected) = $test; - $output = $config->dumpConfig($configLocal, $configGlobal, $configCommon, $configCache); + $config = new DumpConfigTestMockConfig($configLocal, $configGlobal, $configCommon, $configCache); + + $output = $config->dumpConfig(); $this->assertEquals($expected, $output, $description); } @@ -443,11 +369,26 @@ class ConfigTest extends PHPUnit_Framework_TestCase $globalFile = PIWIK_INCLUDE_PATH . '/tests/resources/Config/global.ini.php'; $commonFile = PIWIK_INCLUDE_PATH . '/tests/resources/Config/common.config.ini.php'; - $config = Config::getInstance(); - $config->setTestEnvironment($userFile, $globalFile, $commonFile); - $config->init(); + $config = new Config($globalFile, $userFile, $commonFile); $this->assertEquals('${@piwik(crash))}', $config->Category['key3']); } + + public function test_forceSave_writesNothingIfThereAreNoChanges() + { + $sourceConfigFile = PIWIK_INCLUDE_PATH . '/tests/resources/Config/config.ini.php'; + $configFile = PIWIK_INCLUDE_PATH . '/tmp/tmp.config.ini.php'; + + @unlink($configFile); + copy($sourceConfigFile, $configFile); + + $config = new Config($sourceConfigFile, $configFile); + $config->reload(); + $config->forceSave(); + + $this->assertEquals(file_get_contents($sourceConfigFile), file_get_contents($configFile)); + + @unlink($configFile); + } }