32 private $configArray = [];
33 private $comments = [];
36 private $lookupTable = [];
39 private $isModified =
false;
40 private $addedFiles = [];
41 private $containedFiles = [];
43 private $configPath =
null;
44 private $configExtension =
'ini';
45 private $cachePath =
null;
47 private $fileUtil =
null;
49 private static $logger =
null;
57 $this->configPath = $configPath;
58 $this->cachePath = $cachePath;
60 if (self::$logger ==
null) {
66 $configArray = $this->configArray;
69 foreach($configArray as $section => $config) {
70 $result .= PHP_EOL.
'['.$section.
']'.PHP_EOL;
72 foreach ($config as $key => $value) {
73 $result .= $key.
' = '.json_encode($value).PHP_EOL;
84 return $this->configPath;
95 return $this->fileUtil->getFiles($this->configPath,
'/\.'.$this->configExtension.
'$/',
true);
104 if (self::$logger->isDebugEnabled()) {
105 self::$logger->debug(
"Add configuration: ".$name);
107 $filename = $this->configPath.$name;
111 $numParsedFiles =
sizeof($this->addedFiles);
112 $lastFile = $numParsedFiles > 0 ? $this->addedFiles[$numParsedFiles-1] :
'';
113 if (self::$logger->isDebugEnabled()) {
114 self::$logger->debug(
"Parsed files: ".$numParsedFiles.
", last file: ".$lastFile);
115 foreach($this->addedFiles as $addedFile) {
116 self::$logger->debug(
"File date ".$addedFile.
": ".@filemtime($addedFile));
119 self::$logger->debug(
"Cache file date ".$cachedFile.
": ".@filemtime($cachedFile));
121 if ($numParsedFiles > 0 && $lastFile == $filename &&
123 if (self::$logger->isDebugEnabled()) {
124 self::$logger->debug(
"Skipping");
129 if (!file_exists($filename)) {
133 if (self::$logger->isDebugEnabled()) {
134 self::$logger->debug(
"Adding...");
137 $this->addedFiles[] = $filename;
139 if (self::$logger->isDebugEnabled()) {
140 self::$logger->debug(
"Parse first time");
142 $result = $this->
processFile($filename, $this->configArray, $this->containedFiles);
143 $this->configArray = $result[
'config'];
144 $this->containedFiles = array_unique($result[
'files']);
146 if ($processValues) {
160 if (self::$logger->isDebugEnabled()) {
161 self::$logger->debug(
"Reuse from cache");
174 protected function processFile($filename, $configArray=[], $parsedFiles=[]) {
176 if (!in_array($filename, $parsedFiles)) {
177 $parsedFiles[] = $filename;
185 foreach ($includes as $include) {
186 $result = $this->
processFile($this->configPath.$include, $configArray, $parsedFiles);
187 $configArray = $this->
configMerge($configArray, $result[
'config'],
true);
188 $parsedFiles = $result[
'files'];
193 $configArray = $this->
configMerge($configArray, $content,
true);
195 return [
'config' => $configArray,
'files' => $parsedFiles];
202 return array_keys($this->configArray);
209 return ($this->
lookup($section) !=
null);
216 $lookupEntry = $this->
lookup($section);
217 if ($lookupEntry ==
null) {
222 return $this->configArray[$lookupEntry[0]];
225 return array_filter($this->configArray[$lookupEntry[0]], function($k) {
226 return !preg_match('/^__/
', $k);
227 }, \ARRAY_FILTER_USE_KEY);
235 public function hasValue($key, $section) {
236 return ($this->lookup($section, $key) != null);
242 public function getValue($key, $section) {
243 $lookupEntry = $this->lookup($section, $key);
244 if ($lookupEntry == null || sizeof($lookupEntry) == 1) {
245 throw new ConfigurationException('Key \
''.$key.
'\' not found in section \
''.$section.
'\'!
');
248 return $this->configArray[$lookupEntry[0]][$lookupEntry[1]];
255 public function getBooleanValue($key, $section) {
256 $value = $this->getValue($key, $section);
257 return StringUtil::getBoolean($value);
263 public function getDirectoryValue($key, $section) {
264 $value = $this->getValue($key, $section);
265 $isArray = is_array($value);
266 $values = !$isArray ? [$value] : $value;
269 foreach ($values as $path) {
270 $absPath = WCMF_BASE.$path;
271 $result[] = $this->fileUtil->realpath($absPath).'/
';
274 return $isArray ? $result : (sizeof($result) > 0 ? $result[0] : null);
280 public function getFileValue($key, $section) {
281 $value = $this->getValue($key, $section);
282 $isArray = is_array($value);
283 $values = !$isArray ? [$value] : $value;
286 foreach ($values as $path) {
287 $absPath = WCMF_BASE.$path;
288 $result[] = $this->fileUtil->realpath(dirname($absPath)).'/
'.$this->fileUtil->basename($absPath);
291 return $isArray ? $result : (sizeof($result) > 0 ? $result[0] : null);
297 public function getKey($value, $section) {
298 $map = array_flip($this->getSection($section));
299 if (!isset($map[$value])) {
300 throw new ConfigurationException('Value \
''.$value.
'\' not found in section \
''.$section.
'\'!
');
312 public function isEditable($section) {
313 if ($this->hasValue('readonlySections
', 'config
')) {
314 $readonlySections = $this->getValue('readonlySections
', 'config
');
315 $sectionLower = strtolower($section);
316 if (is_array($readonlySections)) {
317 foreach($readonlySections as $readonlySection) {
318 if ($sectionLower == strtolower($readonlySection)) {
330 public function isModified() {
331 return $this->isModified;
337 public function createSection($section) {
338 $section = trim($section);
339 if (strlen($section) == 0) {
340 throw new IllegalArgumentException('Empty section names are not allowed!
');
342 if ($this->hasSection($section)) {
343 throw new IllegalArgumentException('Section \
''.$section.
'\' already exists!
');
345 $this->configArray[$section] = '';
346 $this->buildLookupTable();
347 $this->isModified = true;
354 public function removeSection($section) {
355 if (!$this->isEditable($section)) {
356 throw new IllegalArgumentException('Section \
''.$section.
'\' is not editable!
');
358 $lookupEntry = $this->lookup($section);
359 if ($lookupEntry != null) {
360 unset($this->configArray[$lookupEntry[0]]);
361 $this->buildLookupTable();
362 $this->isModified = true;
369 public function renameSection($oldname, $newname) {
370 $newname = trim($newname);
371 if (strlen($newname) == 0) {
372 throw new IllegalArgumentException('Empty section names are not allowed!
');
374 $lookupEntryOld = $this->lookup($oldname);
375 if ($lookupEntryOld == null) {
376 throw new IllegalArgumentException('Section \
''.$oldname.
'\' does not exist!
');
378 if (!$this->isEditable($oldname)) {
379 throw new IllegalArgumentException('Section \
''.$oldname.
'\' is not editable!
');
381 $lookupEntryNew = $this->lookup($newname);
382 if ($lookupEntryNew != null) {
383 throw new IllegalArgumentException('Section \
''.$newname.
'\' already exists!
');
386 $value = $this->configArray[$lookupEntryOld[0]];
387 $this->configArray[$newname] = $value;
388 unset($this->configArray[$lookupEntryOld[0]]);
389 $this->buildLookupTable();
390 $this->isModified = true;
396 public function setValue($key, $value, $section, $createSection=true) {
398 if (strlen($key) == 0) {
399 throw new IllegalArgumentException('Empty key names are not allowed!
');
401 $lookupEntrySection = $this->lookup($section);
402 if ($lookupEntrySection == null && !$createSection) {
403 throw new IllegalArgumentException('Section \
''.$section.
'\' does not exist!
');
405 if ($lookupEntrySection != null && !$this->isEditable($section)) {
406 throw new IllegalArgumentException('Section \
''.$section.
'\' is not editable!
');
409 // create section if requested and determine section name
410 if ($lookupEntrySection == null && $createSection) {
411 $section = trim($section);
412 $this->configArray[$section] = [];
413 $finalSectionName = $section;
416 $finalSectionName = $lookupEntrySection[0];
418 // determine key name
419 if ($lookupEntrySection != null) {
420 $lookupEntryKey = $this->lookup($section, $key);
421 if ($lookupEntryKey == null) {
422 // key does not exist yet
423 $finalKeyName = $key;
426 $finalKeyName = $lookupEntryKey[1];
430 $finalKeyName = $key;
432 $this->configArray[$finalSectionName][$finalKeyName] = $value;
433 $this->buildLookupTable();
434 $this->isModified = true;
440 public function removeKey($key, $section) {
441 if (!$this->isEditable($section)) {
442 throw new IllegalArgumentException('Section \
''.$section.
'\' is not editable!
');
444 $lookupEntry = $this->lookup($section, $key);
445 if ($lookupEntry != null) {
446 unset($this->configArray[$lookupEntry[0]][$lookupEntry[1]]);
447 $this->buildLookupTable();
448 $this->isModified = true;
455 public function renameKey($oldname, $newname, $section) {
456 $newname = trim($newname);
457 if (strlen($newname) == 0) {
458 throw new IllegalArgumentException('Empty key names are not allowed!
');
460 if (!$this->hasSection($section)) {
461 throw new IllegalArgumentException('Section \
''.$section.
'\' does not exist!
');
463 if (!$this->isEditable($section)) {
464 throw new IllegalArgumentException('Section \
''.$section.
'\' is not editable!
');
466 $lookupEntryOld = $this->lookup($section, $oldname);
467 if ($lookupEntryOld == null) {
468 throw new IllegalArgumentException('Key \
''.$oldname.
'\' does not exist in section \
''.$section.
'\'!
');
470 $lookupEntryNew = $this->lookup($section, $newname);
471 if ($lookupEntryNew != null) {
472 throw new IllegalArgumentException('Key \
''.$newname.
'\' already exists in section \
''.$section.
'\'!
');
475 $value = $this->configArray[$lookupEntryOld[0]][$lookupEntryOld[1]];
476 $this->configArray[$lookupEntryOld[0]][$newname] = $value;
477 unset($this->configArray[$lookupEntryOld[0]][$lookupEntryOld[1]]);
478 $this->buildLookupTable();
479 $this->isModified = true;
485 public function writeConfiguration($name) {
488 foreach($this->configArray as $section => $values) {
489 $sectionString = "[".$section."]";
490 $content .= $this->comments[$sectionString];
491 $content .= $sectionString."\n";
492 if (is_array($values)) {
493 foreach($values as $key => $value) {
494 if (is_array($value)) {
495 $value = "{".join(", ", $value)."}";
497 // unescape double quotes
498 $value = str_replace("\\\"", "\"", $value);
499 $content .= $this->comments[$section][$key];
500 $content .= $key." = ".$value."\n";
504 $content .= $this->comments[';
'];
506 if (!$fh = fopen($filename, 'w
')) {
507 throw new IOException('Can\
't open ini file \''.$filename.
'\'!
');
510 if (!fwrite($fh, $content)) {
511 throw new IOException('Can\
't write ini file \''.$filename.
'\'!
');
515 // notify configuration change listeners
516 $this->configChanged();
517 $this->isModified = false;
535 protected function parseIniFile($filename) {
536 if (!file_exists($filename)) {
537 throw new ConfigurationException('The config file
'.$filename.' does not exist.
');
541 $lines = file($filename);
542 $commentsPending = '';
543 foreach($lines as $line) {
545 // comments/blank lines
546 if($line == '' || $line[0] == ';
') {
547 $commentsPending .= $line."\n";
551 if($line[0] == '[
' && $line[strlen($line)-1] == ']
') {
552 $sectionName = substr($line, 1, strlen($line)-2);
553 $configArray[$sectionName] = [];
555 // store comments/blank lines for section
556 $this->comments[$line] = $commentsPending;
557 $commentsPending = '';
560 $parts = explode('=
', $line, 2);
561 $key = trim($parts[0]);
562 $value = trim($parts[1]);
563 $configArray[$sectionName][$key] = $value;
565 // store comments/blank lines for key
566 $this->comments[$sectionName][$key] = $commentsPending;
567 $commentsPending = "";
570 // store comments/blank lines from the end of the file
571 $this->comments[';
'] = substr($commentsPending, 0, -1);
581 protected function processValues() {
582 array_walk_recursive($this->configArray, [$this, '
processValue']);
591 protected function processValue(&$value) {
592 if (!is_array($value)) {
593 // decode encoded (%##) values
594 if (preg_match ("/%/", $value)) {
595 $value = urldecode($value);
598 if(preg_match("/^{.*}$/", $value)) {
599 $arrayValues = StringUtil::quotesplit(substr($value, 1, -1));
601 foreach ($arrayValues as $arrayValue) {
602 $value[] = trim($arrayValue);
617 protected function configMerge($array1, $array2, $override) {
619 foreach(array_keys($array2) as $key) {
620 if (!array_key_exists($key, $result)) {
621 // copy complete section, if new
622 $result[$key] = $array2[$key];
625 // process existing section
626 // remove old keys, if inheritence is disabled
627 $inherit = !isset($array2[$key]['__inherit
']) || $array2[$key]['__inherit
'] == false;
629 foreach(array_keys($result[$key]) as $subkey) {
630 unset($result[$key][$subkey]);
634 foreach(array_keys($array2[$key]) as $subkey) {
635 if ((array_key_exists($subkey, $result[$key]) && $override) || !isset($result[$key][$subkey])) {
636 $result[$key][$subkey] = $array2[$key][$subkey];
649 protected function getConfigIncludes($array) {
650 $sectionMatches = null;
651 if (preg_match('/(?:^|,)(config)(?:,|$)/i
', join(',
', array_keys($array)), $sectionMatches)) {
652 $sectionKey = sizeof($sectionMatches) > 0 ? $sectionMatches[1] : null;
655 if (preg_match('/(?:^|,)(include)(?:,|$)/i
', join(',
', array_keys($array[$sectionKey])), $keyMatches)) {
656 return sizeof($keyMatches) > 0 ? $array[$sectionKey][$keyMatches[1]] : null;
666 protected function serialize() {
667 if (!$this->isModified() && ($cacheFile = $this->getSerializeFilename($this->addedFiles))) {
668 if (self::$logger->isDebugEnabled()) {
669 self::$logger->debug("Serialize configuration: ".join(',
', $this->addedFiles)." to file: ".$cacheFile);
671 $this->fileUtil->mkdirRec(dirname($cacheFile));
672 if ($fh = @fopen($cacheFile, "w")) {
673 if (@fwrite($fh, serialize(array_filter(get_object_vars($this), function($value, $name) {
674 return $name != 'comments
'; // don't store comments
675 }, ARRAY_FILTER_USE_BOTH)))) {
691 if (!$this->isModified() && ($cacheFile = $this->getSerializeFilename($parsedFiles)) && file_exists($cacheFile)) {
692 $parsedFiles[] = __FILE__;
693 if (!$this->checkFileDate($parsedFiles, $cacheFile)) {
694 $vars = unserialize(file_get_contents($cacheFile));
697 $includes = $vars[
'containedFiles'];
698 if (is_array($includes)) {
699 if ($this->checkFileDate($includes, $cacheFile)) {
705 foreach($vars as $key => $val) {
721 if (!$this->cachePath) {
724 $path = $this->fileUtil->realpath($this->cachePath).
'/';
725 $filename = $path.
'wcmf_config_'.hash(
'sha256', $this->fileUtil->realpath($this->configPath).
'/'.join(
'_', $parsedFiles));
736 foreach ($fileList as $file) {
737 if (@filemtime($file) > @filemtime($referenceFile)) {
748 if (self::$logger->isDebugEnabled()) {
749 self::$logger->debug(
"Configuration is changed");
751 if (ObjectFactory::isConfigured()) {
752 if (self::$logger->isDebugEnabled()) {
753 self::$logger->debug(
"Emitting change event");
755 ObjectFactory::getInstance(
'eventManager')->dispatch(ConfigChangeEvent::NAME,
764 $this->lookupTable = [];
765 foreach ($this->configArray as $section => $entry) {
767 $lookupSectionKey = strtolower($section.
':');
768 $this->lookupTable[$lookupSectionKey] = [$section];
770 foreach ($entry as $key => $value) {
771 $lookupKey = strtolower($lookupSectionKey.$key);
772 $this->lookupTable[$lookupKey] = [$section, $key];
783 protected function lookup($section, $key=
null) {
784 $lookupKey = strtolower($section).
':'.strtolower($key);
785 if (isset($this->lookupTable[$lookupKey])) {
786 return $this->lookupTable[$lookupKey];