PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 1.0.5
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v1.0.5
5.12.0 5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / Config / IniFileChain.php
matomo / app / core / Config Last commit date
Cache.php 6 years ago ConfigNotFoundException.php 6 years ago IniFileChain.php 6 years ago
IniFileChain.php
542 lines
1 <?php
2 /**
3 * Piwik - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 */
8 namespace Piwik\Config;
9
10 use Piwik\Common;
11 use Piwik\Ini\IniReader;
12 use Piwik\Ini\IniReadingException;
13 use Piwik\Ini\IniWriter;
14 use Piwik\Piwik;
15
16 /**
17 * Manages a list of INI files where the settings in each INI file merge with or override the
18 * settings in the previous INI file.
19 *
20 * The IniFileChain class manages two types of INI files: multiple default setting files and one
21 * user settings file.
22 *
23 * The default setting files (for example, global.ini.php & common.ini.php) hold the default setting values.
24 * The settings in these files are merged recursively, however, array settings in one file will still
25 * overwrite settings in the previous file.
26 *
27 * Default settings files cannot be modified through the IniFileChain class.
28 *
29 * The user settings file (for example, config.ini.php) holds the actual setting values. Settings in the
30 * user settings files overwrite other settings. So array settings will not merge w/ previous values.
31 *
32 * HTML characters and dollar signs are stored as encoded HTML entities in INI files. This prevents
33 * several `parse_ini_file` issues, including one where parse_ini_file tries to insert a variable
34 * into a setting value if a string like `"$varname" is present.
35 */
36 class IniFileChain
37 {
38 const CONFIG_CACHE_KEY = 'config.ini';
39 /**
40 * Maps INI file names with their parsed contents. The order of the files signifies the order
41 * in the chain. Files with lower index are overwritten/merged with files w/ a higher index.
42 *
43 * @var array
44 */
45 protected $settingsChain = array();
46
47 /**
48 * The merged INI settings.
49 *
50 * @var array
51 */
52 protected $mergedSettings = array();
53
54 /**
55 * Constructor.
56 *
57 * @param string[] $defaultSettingsFiles The list of paths to INI files w/ the default setting values.
58 * @param string|null $userSettingsFile The path to the user settings file.
59 */
60 public function __construct(array $defaultSettingsFiles = array(), $userSettingsFile = null)
61 {
62 $this->reload($defaultSettingsFiles, $userSettingsFile);
63 }
64
65 /**
66 * Return setting section by reference.
67 *
68 * @param string $name
69 * @return mixed
70 */
71 public function &get($name)
72 {
73 if (!isset($this->mergedSettings[$name])) {
74 $this->mergedSettings[$name] = array();
75 }
76
77 $result =& $this->mergedSettings[$name];
78 return $result;
79 }
80
81 /**
82 * Return setting section from a specific file, rather than the current merged settings.
83 *
84 * @param string $file The path of the file. Should be the path used in construction or reload().
85 * @param string $name The name of the section to access.
86 */
87 public function getFrom($file, $name)
88 {
89 return @$this->settingsChain[$file][$name];
90 }
91
92 /**
93 * Sets a setting value.
94 *
95 * @param string $name
96 * @param mixed $value
97 */
98 public function set($name, $value)
99 {
100 $this->mergedSettings[$name] = $value;
101 }
102
103 /**
104 * Returns all settings. Changes made to the array result will be reflected in the
105 * IniFileChain instance.
106 *
107 * @return array
108 */
109 public function &getAll()
110 {
111 return $this->mergedSettings;
112 }
113
114 /**
115 * Dumps the current in-memory setting values to a string in INI format and returns it.
116 *
117 * @param string $header The header of the output INI file.
118 * @return string The dumped INI contents.
119 */
120 public function dump($header = '')
121 {
122 return $this->dumpSettings($this->mergedSettings, $header);
123 }
124
125 /**
126 * Writes the difference of the in-memory setting values and the on-disk user settings file setting
127 * values to a string in INI format, and returns it.
128 *
129 * If a config section is identical to the default settings section (as computed by merging
130 * all default setting files), it is not written to the user settings file.
131 *
132 * @param string $header The header of the INI output.
133 * @return string The dumped INI contents.
134 */
135 public function dumpChanges($header = '')
136 {
137 $userSettingsFile = $this->getUserSettingsFile();
138
139 $defaultSettings = $this->getMergedDefaultSettings();
140 $existingMutableSettings = $this->settingsChain[$userSettingsFile];
141
142 $dirty = false;
143
144 $configToWrite = array();
145 foreach ($this->mergedSettings as $sectionName => $changedSection) {
146 if(isset($existingMutableSettings[$sectionName])){
147 $existingMutableSection = $existingMutableSettings[$sectionName];
148 } else{
149 $existingMutableSection = array();
150 }
151
152 // remove default values from both (they should not get written to local)
153 if (isset($defaultSettings[$sectionName])) {
154 $changedSection = $this->arrayUnmerge($defaultSettings[$sectionName], $changedSection);
155 $existingMutableSection = $this->arrayUnmerge($defaultSettings[$sectionName], $existingMutableSection);
156 }
157
158 // if either local/config have non-default values and the other doesn't,
159 // OR both have values, but different values, we must write to config.ini.php
160 if (empty($changedSection) xor empty($existingMutableSection)
161 || (!empty($changedSection)
162 && !empty($existingMutableSection)
163 && self::compareElements($changedSection, $existingMutableSection))
164 ) {
165 $dirty = true;
166 }
167
168 $configToWrite[$sectionName] = $changedSection;
169 }
170
171 if ($dirty) {
172 // sort config sections by how early they appear in the file chain
173 $self = $this;
174 uksort($configToWrite, function ($sectionNameLhs, $sectionNameRhs) use ($self) {
175 $lhsIndex = $self->findIndexOfFirstFileWithSection($sectionNameLhs);
176 $rhsIndex = $self->findIndexOfFirstFileWithSection($sectionNameRhs);
177
178 if ($lhsIndex == $rhsIndex) {
179 $lhsIndexInFile = $self->getIndexOfSectionInFile($lhsIndex, $sectionNameLhs);
180 $rhsIndexInFile = $self->getIndexOfSectionInFile($rhsIndex, $sectionNameRhs);
181
182 if ($lhsIndexInFile == $rhsIndexInFile) {
183 return 0;
184 } elseif ($lhsIndexInFile < $rhsIndexInFile) {
185 return -1;
186 } else {
187 return 1;
188 }
189 } elseif ($lhsIndex < $rhsIndex) {
190 return -1;
191 } else {
192 return 1;
193 }
194 });
195
196 return $this->dumpSettings($configToWrite, $header);
197 } else {
198 return null;
199 }
200 }
201
202 /**
203 * Reloads settings from disk.
204 */
205 public function reload($defaultSettingsFiles = array(), $userSettingsFile = null)
206 {
207 if (!empty($defaultSettingsFiles)
208 || !empty($userSettingsFile)
209 ) {
210 $this->resetSettingsChain($defaultSettingsFiles, $userSettingsFile);
211 }
212
213 $hasAbsoluteConfigFile = !empty($userSettingsFile) && strpos($userSettingsFile, DIRECTORY_SEPARATOR) === 0;
214 $useConfigCache = !empty($GLOBALS['ENABLE_CONFIG_PHP_CACHE']) && $hasAbsoluteConfigFile;
215
216 if ($useConfigCache) {
217 $cache = new Cache();
218 $values = $cache->doFetch(self::CONFIG_CACHE_KEY);
219
220 if (!empty($values)
221 && isset($values['mergedSettings'])
222 && isset($values['settingsChain'][$userSettingsFile])) {
223 $this->mergedSettings = $values['mergedSettings'];
224 $this->settingsChain = $values['settingsChain'];
225 return;
226 }
227 }
228
229 $reader = new IniReader();
230 foreach ($this->settingsChain as $file => $ignore) {
231 if (is_readable($file)) {
232 try {
233 $contents = $reader->readFile($file);
234 $this->settingsChain[$file] = $this->decodeValues($contents);
235 } catch (IniReadingException $ex) {
236 throw new IniReadingException('Unable to read INI file {' . $file . '}: ' . $ex->getMessage() . "\n Your host may have disabled parse_ini_file().");
237 }
238
239 $this->decodeValues($this->settingsChain[$file]);
240 }
241 }
242
243 $merged = $this->mergeFileSettings();
244 // remove reference to $this->settingsChain... otherwise dump() or compareElements() will never notice a difference
245 // on PHP 7+ as they would be always equal
246 $this->mergedSettings = $this->copy($merged);
247
248 if (!empty($GLOBALS['MATOMO_MODIFY_CONFIG_SETTINGS']) && !empty($this->mergedSettings)) {
249 $this->mergedSettings = call_user_func($GLOBALS['MATOMO_MODIFY_CONFIG_SETTINGS'], $this->mergedSettings);
250 }
251
252 if ($useConfigCache
253 && !empty($this->mergedSettings)
254 && !empty($this->settingsChain)) {
255
256 $ttlOneHour = 3600;
257 $cache = new Cache();
258 if ($cache->isValidHost($this->mergedSettings)) {
259 // we make sure to save the config only if the host is valid...
260 $data = array('mergedSettings' => $this->mergedSettings, 'settingsChain' => $this->settingsChain);
261 $cache->doSave(self::CONFIG_CACHE_KEY, $data, $ttlOneHour);
262 }
263 }
264 }
265
266 public function deleteConfigCache()
267 {
268 if (!empty($GLOBALS['ENABLE_CONFIG_PHP_CACHE'])) {
269 $cache = new Cache();
270 $cache->doDelete(IniFileChain::CONFIG_CACHE_KEY);
271 }
272 }
273
274 private function copy($merged)
275 {
276 $copy = array();
277 foreach ($merged as $index => $value) {
278 if (is_array($value)) {
279 $copy[$index] = $this->copy($value);
280 } else {
281 $copy[$index] = $value;
282 }
283 }
284 return $copy;
285 }
286
287 private function resetSettingsChain($defaultSettingsFiles, $userSettingsFile)
288 {
289 $this->settingsChain = array();
290
291 if (!empty($defaultSettingsFiles)) {
292 foreach ($defaultSettingsFiles as $file) {
293 $this->settingsChain[$file] = null;
294 }
295 }
296
297 if (!empty($userSettingsFile)) {
298 $this->settingsChain[$userSettingsFile] = null;
299 }
300 }
301
302 protected function mergeFileSettings()
303 {
304 $mergedSettings = $this->getMergedDefaultSettings();
305
306 $userSettings = end($this->settingsChain) ?: array();
307 foreach ($userSettings as $sectionName => $section) {
308 if (!isset($mergedSettings[$sectionName])) {
309 $mergedSettings[$sectionName] = $section;
310 } else {
311 // the last user settings file completely overwrites INI sections. the other files in the chain
312 // can add to array options
313 $mergedSettings[$sectionName] = array_merge($mergedSettings[$sectionName], $section);
314 }
315 }
316
317 return $mergedSettings;
318 }
319
320 protected function getMergedDefaultSettings()
321 {
322 $userSettingsFile = $this->getUserSettingsFile();
323
324 $mergedSettings = array();
325 foreach ($this->settingsChain as $file => $settings) {
326 if ($file == $userSettingsFile
327 || empty($settings)
328 ) {
329 continue;
330 }
331
332 foreach ($settings as $sectionName => $section) {
333 if (!isset($mergedSettings[$sectionName])) {
334 $mergedSettings[$sectionName] = $section;
335 } else {
336 $mergedSettings[$sectionName] = $this->array_merge_recursive_distinct($mergedSettings[$sectionName], $section);
337 }
338 }
339 }
340 return $mergedSettings;
341 }
342
343 protected function getUserSettingsFile()
344 {
345 // the user settings file is the last key in $settingsChain
346 end($this->settingsChain);
347 return key($this->settingsChain);
348 }
349
350 /**
351 * Comparison function
352 *
353 * @param mixed $elem1
354 * @param mixed $elem2
355 * @return int;
356 */
357 public static function compareElements($elem1, $elem2)
358 {
359 if (is_array($elem1)) {
360 if (is_array($elem2)) {
361 return strcmp(serialize($elem1), serialize($elem2));
362 }
363
364 return 1;
365 }
366
367 if (is_array($elem2)) {
368 return -1;
369 }
370
371 if ((string)$elem1 === (string)$elem2) {
372 return 0;
373 }
374
375 return ((string)$elem1 > (string)$elem2) ? 1 : -1;
376 }
377
378 /**
379 * Compare arrays and return difference, such that:
380 *
381 * $modified = array_merge($original, $difference);
382 *
383 * @param array $original original array
384 * @param array $modified modified array
385 * @return array differences between original and modified
386 */
387 public function arrayUnmerge($original, $modified)
388 {
389 // return key/value pairs for keys in $modified but not in $original
390 // return key/value pairs for keys in both $modified and $original, but values differ
391 // ignore keys that are in $original but not in $modified
392
393 if (empty($original) || !is_array($original)) {
394 $original = array();
395 }
396
397 if (empty($modified) || !is_array($modified)) {
398 $modified = array();
399 }
400
401 return array_udiff_assoc($modified, $original, array(__CLASS__, 'compareElements'));
402 }
403
404 /**
405 * array_merge_recursive does indeed merge arrays, but it converts values with duplicate
406 * keys to arrays rather than overwriting the value in the first array with the duplicate
407 * value in the second array, as array_merge does. I.e., with array_merge_recursive,
408 * this happens (documented behavior):
409 *
410 * array_merge_recursive(array('key' => 'org value'), array('key' => 'new value'));
411 * => array('key' => array('org value', 'new value'));
412 *
413 * array_merge_recursive_distinct does not change the datatypes of the values in the arrays.
414 * Matching keys' values in the second array overwrite those in the first array, as is the
415 * case with array_merge, i.e.:
416 *
417 * array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value'));
418 * => array('key' => array('new value'));
419 *
420 * Parameters are passed by reference, though only for performance reasons. They're not
421 * altered by this function.
422 *
423 * @param array $array1
424 * @param array $array2
425 * @return array
426 * @author Daniel <daniel (at) danielsmedegaardbuus (dot) dk>
427 * @author Gabriel Sobrinho <gabriel (dot) sobrinho (at) gmail (dot) com>
428 */
429 private function array_merge_recursive_distinct(array &$array1, array &$array2)
430 {
431 $merged = $array1;
432 foreach ($array2 as $key => &$value) {
433 if (is_array($value) && isset($merged [$key]) && is_array($merged [$key])) {
434 $merged [$key] = $this->array_merge_recursive_distinct($merged [$key], $value);
435 } else {
436 $merged [$key] = $value;
437 }
438 }
439 return $merged;
440 }
441
442 /**
443 * public for use in closure.
444 */
445 public function findIndexOfFirstFileWithSection($sectionName)
446 {
447 $count = 0;
448 foreach ($this->settingsChain as $file => $settings) {
449 if (isset($settings[$sectionName])) {
450 break;
451 }
452
453 ++$count;
454 }
455 return $count;
456 }
457
458 /**
459 * public for use in closure.
460 */
461 public function getIndexOfSectionInFile($fileIndex, $sectionName)
462 {
463 reset($this->settingsChain);
464 for ($i = 0; $i != $fileIndex; ++$i) {
465 next($this->settingsChain);
466 }
467
468 $settingsData = current($this->settingsChain);
469 if (empty($settingsData)) {
470 return -1;
471 }
472
473 $settingsDataSectionNames = array_keys($settingsData);
474
475 return array_search($sectionName, $settingsDataSectionNames);
476 }
477
478 /**
479 * Encode HTML entities
480 *
481 * @param mixed $values
482 * @return mixed
483 */
484 protected function encodeValues(&$values)
485 {
486 if (is_array($values)) {
487 foreach ($values as &$value) {
488 $value = $this->encodeValues($value);
489 }
490 } elseif (is_float($values)) {
491 $values = Common::forceDotAsSeparatorForDecimalPoint($values);
492 } elseif (is_string($values)) {
493 $values = htmlentities($values, ENT_COMPAT, 'UTF-8');
494 $values = str_replace('$', '&#36;', $values);
495 }
496 return $values;
497 }
498
499 /**
500 * Decode HTML entities
501 *
502 * @param mixed $values
503 * @return mixed
504 */
505 protected function decodeValues(&$values)
506 {
507 if (is_array($values)) {
508 foreach ($values as &$value) {
509 $value = $this->decodeValues($value);
510 }
511 return $values;
512 } elseif (is_string($values)) {
513 return html_entity_decode($values, ENT_COMPAT, 'UTF-8');
514 }
515 return $values;
516 }
517
518 private function dumpSettings($values, $header)
519 {
520 /**
521 * Triggered before a config is being written / saved on the local file system.
522 *
523 * A plugin can listen to it and modify which settings will be saved on the file system. This allows you
524 * to prevent saving config values that a plugin sets on demand. Say you configure the database password in the
525 * config on demand in your plugin, then you could prevent that the password is saved in the actual config file
526 * by listening to this event like this:
527 *
528 * **Example**
529 * function doNotSaveDbPassword (&$values) {
530 * unset($values['database']['password']);
531 * }
532 *
533 * @param array &$values Config values that will be saved
534 */
535 Piwik::postEvent('Config.beforeSave', array(&$values));
536 $values = $this->encodeValues($values);
537
538 $writer = new IniWriter();
539 return $writer->writeToString($values, $header);
540 }
541 }
542