]> git.mxchange.org Git - friendica.git/blob - src/Core/Config/Util/ConfigFileManager.php
Add support for toString/Serializable
[friendica.git] / src / Core / Config / Util / ConfigFileManager.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2023, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Core\Config\Util;
23
24 use Friendica\Core\Addon;
25 use Friendica\Core\Config\Exception\ConfigFileException;
26 use Friendica\Core\Config\ValueObject\Cache;
27
28 /**
29  * The ConfigFileLoader loads and saves config-files and stores them in a ConfigCache ( @see Cache )
30  *
31  * It is capable of loading the following config files:
32  * - *.config.php   (current)
33  * - *.ini.php      (deprecated)
34  * - *.htconfig.php (deprecated)
35  */
36 class ConfigFileManager
37 {
38         /**
39          * The default name of the user defined legacy config file
40          *
41          * @var string
42          */
43         const CONFIG_HTCONFIG = 'htconfig';
44
45         /**
46          * The config file, where overrides per admin page/console are saved at
47          *
48          * @var string
49          */
50         const CONFIG_DATA_FILE = 'node.config.php';
51
52         /**
53          * The sample string inside the configs, which shouldn't get loaded
54          *
55          * @var string
56          */
57         const SAMPLE_END = '-sample';
58
59         /**
60          * @var string
61          */
62         private $baseDir;
63         /**
64          * @var string
65          */
66         private $configDir;
67         /**
68          * @var string
69          */
70         private $staticDir;
71
72         /**
73          * @var array
74          */
75         private $server;
76
77         /**
78          * @param string $baseDir   The base
79          * @param string $configDir
80          * @param string $staticDir
81          */
82         public function __construct(string $baseDir, string $configDir, string $staticDir, array $server = [])
83         {
84                 $this->baseDir   = $baseDir;
85                 $this->configDir = $configDir;
86                 $this->staticDir = $staticDir;
87                 $this->server    = $server;
88         }
89
90         /**
91          * Load the configuration files into an configuration cache
92          *
93          * First loads the default value for all the configuration keys, then the legacy configuration files, then the
94          * expected local.config.php
95          *
96          * @param Cache $configCache The config cache to load to
97          * @param bool  $raw         Set up the raw config format
98          *
99          * @throws ConfigFileException
100          */
101         public function setupCache(Cache $configCache, bool $raw = false)
102         {
103                 // Load static config files first, the order is important
104                 $configCache->load($this->loadStaticConfig('defaults'), Cache::SOURCE_STATIC);
105                 $configCache->load($this->loadStaticConfig('settings'), Cache::SOURCE_STATIC);
106
107                 // try to load the legacy config first
108                 $configCache->load($this->loadLegacyConfig('htpreconfig'), Cache::SOURCE_FILE);
109                 $configCache->load($this->loadLegacyConfig('htconfig'), Cache::SOURCE_FILE);
110
111                 // Now load every other config you find inside the 'config/' directory
112                 $this->loadCoreConfig($configCache);
113
114                 // Now load the node.config.php file with the node specific config values (based on admin gui/console actions)
115                 $this->loadDataConfig($configCache);
116
117                 $configCache->load($this->loadEnvConfig(), Cache::SOURCE_ENV);
118
119                 // In case of install mode, add the found basepath (because there isn't a basepath set yet
120                 if (!$raw && empty($configCache->get('system', 'basepath'))) {
121                         // Setting at least the basepath we know
122                         $configCache->set('system', 'basepath', $this->baseDir, Cache::SOURCE_FILE);
123                 }
124         }
125
126         /**
127          * Tries to load the static core-configuration and returns the config array.
128          *
129          * @param string $name The name of the configuration
130          *
131          * @return array The config array (empty if no config found)
132          *
133          * @throws ConfigFileException if the configuration file isn't readable
134          */
135         private function loadStaticConfig(string $name): array
136         {
137                 $configName = $this->staticDir . DIRECTORY_SEPARATOR . $name . '.config.php';
138                 $iniName    = $this->staticDir . DIRECTORY_SEPARATOR . $name . '.ini.php';
139
140                 if (file_exists($configName)) {
141                         return $this->loadConfigFile($configName);
142                 } else if (file_exists($iniName)) {
143                         return $this->loadINIConfigFile($iniName);
144                 } else {
145                         return [];
146                 }
147         }
148
149         /**
150          * Tries to load the specified core-configuration into the config cache.
151          *
152          * @param Cache $configCache The Config cache
153          *
154          * @throws ConfigFileException if the configuration file isn't readable
155          */
156         private function loadCoreConfig(Cache $configCache)
157         {
158                 // try to load legacy ini-files first
159                 foreach ($this->getConfigFiles(true) as $configFile) {
160                         $configCache->load($this->loadINIConfigFile($configFile), Cache::SOURCE_FILE);
161                 }
162
163                 // try to load supported config at last to overwrite it
164                 foreach ($this->getConfigFiles() as $configFile) {
165                         $configCache->load($this->loadConfigFile($configFile), Cache::SOURCE_FILE);
166                 }
167         }
168
169         /**
170          * Tries to load the data config file with the overridden data
171          *
172          * @param Cache $configCache The Config cache
173          *
174          * @throws ConfigFileException In case the config file isn't loadable
175          */
176         private function loadDataConfig(Cache $configCache)
177         {
178                 $filename = $this->configDir . '/' . self::CONFIG_DATA_FILE;
179
180                 if (file_exists($filename) && (filesize($filename) > 0)) {
181
182                         // The fallback empty return content
183                         $content = '<?php return [];';
184
185                         /**
186                          * This code-block creates a readonly node.config.php content stream (fopen() with "r")
187                          * The stream is locked shared (LOCK_SH), so not exclusively, but the OS knows that there's a lock
188                          *
189                          * Any exclusive locking (LOCK_EX) would need to wait until all LOCK_SHs are unlocked
190                          */
191                         if (($configStream = @fopen($filename, 'r')) === false) {
192                                 throw new ConfigFileException(sprintf('Cannot open file "%s" in mode r', $filename));
193                         }
194
195                         try {
196                                 if (flock($configStream, LOCK_SH)) {
197                                         clearstatcache(true, $filename);
198
199                                         if (($filesize = filesize($filename)) === 0) {
200                                                 return;
201                                         }
202
203                                         $content = fread($configStream, $filesize);
204                                         if (!$content) {
205                                                 throw new ConfigFileException(sprintf('Couldn\'t read file %s', $filename));
206                                         }
207                                 }
208                         } finally {
209                                 // unlock and close the stream for every circumstances
210                                 flock($configStream, LOCK_UN);
211                                 fclose($configStream);
212                         }
213
214                         /**
215                          * Evaluate the content string as PHP code
216                          *
217                          * @see https://www.php.net/manual/en/function.eval.php
218                          *
219                          * @note
220                          * To leave the PHP mode, we have to use the appropriate PHP tags '?>' as prefix.
221                          */
222                         $dataArray = eval('?>' . $content);
223
224                         if (is_array($dataArray)) {
225                                 $configCache->load($dataArray, Cache::SOURCE_DATA);
226                         }
227                 }
228         }
229
230         /**
231          * Checks, if the node.config.php is writable
232          *
233          * @return bool
234          */
235         public function dataIsWritable(): bool
236         {
237                 $filename = $this->configDir . '/' . self::CONFIG_DATA_FILE;
238
239                 if (file_exists($filename)) {
240                         return is_writable($filename);
241                 } else {
242                         return is_writable($this->configDir);
243                 }
244         }
245
246         /**
247          * Saves overridden config entries back into the data.config.php
248          *
249          * @param Cache $configCache The config cache
250          *
251          * @throws ConfigFileException In case the config file isn't writeable or the data is invalid
252          */
253         public function saveData(Cache $configCache)
254         {
255                 $filename = $this->configDir . '/' . self::CONFIG_DATA_FILE;
256
257                 if (file_exists($filename)) {
258                         $fileExists = true;
259                 } else {
260                         $fileExists = false;
261                 }
262
263                 /**
264                  * Creates a read-write stream
265                  *
266                  * @see  https://www.php.net/manual/en/function.fopen.php
267                  * @note Open the file for reading and writing. If the file does not exist, it is created.
268                  * If it exists, it is neither truncated (as opposed to 'w'), nor the call to this function fails
269                  * (as is the case with 'x'). The file pointer is positioned on the beginning of the file.
270                  *
271                  */
272                 if (($configStream = @fopen($filename, 'c+')) === false) {
273                         throw new ConfigFileException(sprintf('Cannot open file "%s" in mode c+', $filename));
274                 }
275
276                 try {
277                         // We do want an exclusive lock, so we wait until every LOCK_SH (config reading) is unlocked
278                         if (flock($configStream, LOCK_EX)) {
279
280                                 /**
281                                  * If the file exists, we read the whole file again to avoid a race condition with concurrent threads that could have modified the file between the first config read of this thread and now
282                                  * Since we're currently exclusive locked, no other process can now change the config again
283                                  */
284                                 if ($fileExists) {
285                                         // When reading the config file too fast, we get a wrong filesize, "clearstatcache" prevents that
286                                         clearstatcache(true, $filename);
287                                         $content = fread($configStream, filesize($filename));
288                                         if (!$content) {
289                                                 throw new ConfigFileException(sprintf('Cannot read file %s', $filename));
290                                         }
291
292                                         // Event truncating the whole content wouldn't automatically rewind the stream,
293                                         // so we need to do it manually
294                                         rewind($configStream);
295
296                                         $dataArray = eval('?>' . $content);
297
298                                         // Merge the new content into the existing file based config cache and use it
299                                         // as the new config cache
300                                         if (is_array($dataArray)) {
301                                                 $fileConfigCache = new Cache();
302                                                 $fileConfigCache->load($dataArray, Cache::SOURCE_DATA);
303                                                 $configCache = $fileConfigCache->merge($configCache);
304                                         }
305                                 }
306
307                                 // Only SOURCE_DATA is wanted, the rest isn't part of the node.config.php file
308                                 $data = $configCache->getDataBySource(Cache::SOURCE_DATA);
309
310                                 $encodedData = ConfigFileTransformer::encode($data);
311                                 if (!$encodedData) {
312                                         throw new ConfigFileException('config source cannot get encoded');
313                                 }
314
315                                 // Once again to avoid wrong, implicit "filesize" calls during the fwrite() or ftruncate() call
316                                 clearstatcache(true, $filename);
317                                 if (!ftruncate($configStream, 0) ||
318                                         !fwrite($configStream, $encodedData) ||
319                                         !fflush($configStream)) {
320                                         throw new ConfigFileException(sprintf('Cannot modify locked file %s', $filename));
321                                 }
322                         }
323                 } finally {
324                         // unlock and close the stream for every circumstances
325                         flock($configStream, LOCK_UN);
326                         fclose($configStream);
327                 }
328         }
329
330         /**
331          * Tries to load the specified addon-configuration and returns the config array.
332          *
333          * @param string $name The name of the configuration
334          *
335          * @return array The config array (empty if no config found)
336          *
337          * @throws ConfigFileException if the configuration file isn't readable
338          */
339         public function loadAddonConfig(string $name): array
340         {
341                 $filepath = $this->baseDir . DIRECTORY_SEPARATOR .   // /var/www/html/
342                                         Addon::DIRECTORY . DIRECTORY_SEPARATOR . // addon/
343                                         $name . DIRECTORY_SEPARATOR .            // openstreetmap/
344                                         'config' . DIRECTORY_SEPARATOR .         // config/
345                                         $name . ".config.php";                   // openstreetmap.config.php
346
347                 if (file_exists($filepath)) {
348                         return $this->loadConfigFile($filepath);
349                 } else {
350                         return [];
351                 }
352         }
353
354         /**
355          * Tries to load environment specific variables, based on the `env.config.php` mapping table
356          *
357          * @return array The config array (empty if no config was found)
358          *
359          * @throws ConfigFileException if the configuration file isn't readable
360          */
361         protected function loadEnvConfig(): array
362         {
363                 $filepath = $this->staticDir . DIRECTORY_SEPARATOR .   // /var/www/html/static/
364                                         "env.config.php";                          // env.config.php
365
366                 if (!file_exists($filepath)) {
367                         return [];
368                 }
369
370                 $envConfig = $this->loadConfigFile($filepath);
371
372                 $return = [];
373
374                 foreach ($envConfig as $envKey => $configStructure) {
375                         if (isset($this->server[$envKey])) {
376                                 $return[$configStructure[0]][$configStructure[1]] = $this->server[$envKey];
377                         }
378                 }
379
380                 return $return;
381         }
382
383         /**
384          * Get the config files of the config-directory
385          *
386          * @param bool $ini True, if scan for ini-files instead of config files
387          *
388          * @return array
389          */
390         private function getConfigFiles(bool $ini = false): array
391         {
392                 $files = scandir($this->configDir);
393                 $found = [];
394
395                 $filePattern = ($ini ? '*.ini.php' : '*.config.php');
396
397                 // Don't load sample files
398                 $sampleEnd = self::SAMPLE_END . ($ini ? '.ini.php' : '.config.php');
399
400                 foreach ($files as $filename) {
401                         if (fnmatch($filePattern, $filename) &&
402                                 substr_compare($filename, $sampleEnd, -strlen($sampleEnd)) &&
403                                 $filename !== self::CONFIG_DATA_FILE) {
404                                 $found[] = $this->configDir . '/' . $filename;
405                         }
406                 }
407
408                 return $found;
409         }
410
411         /**
412          * Tries to load the legacy config files (.htconfig.php, .htpreconfig.php) and returns the config array.
413          *
414          * @param string $name The name of the config file (default is empty, which means .htconfig.php)
415          *
416          * @return array The configuration array (empty if no config found)
417          *
418          * @deprecated since version 2018.09
419          */
420         private function loadLegacyConfig(string $name = ''): array
421         {
422                 $name     = !empty($name) ? $name : self::CONFIG_HTCONFIG;
423                 $fullName = $this->baseDir . DIRECTORY_SEPARATOR . '.' . $name . '.php';
424
425                 $config = [];
426                 if (file_exists($fullName)) {
427                         $a         = new \stdClass();
428                         $a->config = [];
429                         include $fullName;
430
431                         $htConfigCategories = array_keys($a->config);
432
433                         // map the legacy configuration structure to the current structure
434                         foreach ($htConfigCategories as $htConfigCategory) {
435                                 if (is_array($a->config[$htConfigCategory])) {
436                                         $keys = array_keys($a->config[$htConfigCategory]);
437
438                                         foreach ($keys as $key) {
439                                                 $config[$htConfigCategory][$key] = $a->config[$htConfigCategory][$key];
440                                         }
441                                 } else {
442                                         $config['config'][$htConfigCategory] = $a->config[$htConfigCategory];
443                                 }
444                         }
445
446                         unset($a);
447
448                         if (isset($db_host)) {
449                                 $config['database']['hostname'] = $db_host;
450                                 unset($db_host);
451                         }
452                         if (isset($db_user)) {
453                                 $config['database']['username'] = $db_user;
454                                 unset($db_user);
455                         }
456                         if (isset($db_pass)) {
457                                 $config['database']['password'] = $db_pass;
458                                 unset($db_pass);
459                         }
460                         if (isset($db_data)) {
461                                 $config['database']['database'] = $db_data;
462                                 unset($db_data);
463                         }
464                         if (isset($config['system']['db_charset'])) {
465                                 $config['database']['charset'] = $config['system']['db_charset'];
466                         }
467                         if (isset($pidfile)) {
468                                 $config['system']['pidfile'] = $pidfile;
469                                 unset($pidfile);
470                         }
471                         if (isset($default_timezone)) {
472                                 $config['system']['default_timezone'] = $default_timezone;
473                                 unset($default_timezone);
474                         }
475                         if (isset($lang)) {
476                                 $config['system']['language'] = $lang;
477                                 unset($lang);
478                         }
479                 }
480
481                 return $config;
482         }
483
484         /**
485          * Tries to load the specified legacy configuration file and returns the config array.
486          *
487          * @param string $filepath
488          *
489          * @return array The configuration array
490          * @throws ConfigFileException
491          * @deprecated since version 2018.12
492          */
493         private function loadINIConfigFile(string $filepath): array
494         {
495                 $contents = include($filepath);
496
497                 $config = parse_ini_string($contents, true, INI_SCANNER_TYPED);
498
499                 if ($config === false) {
500                         throw new ConfigFileException('Error parsing INI config file ' . $filepath);
501                 }
502
503                 return $config;
504         }
505
506         /**
507          * Tries to load the specified configuration file and returns the config array.
508          *
509          * The config format is PHP array and the template for configuration files is the following:
510          *
511          * <?php return [
512          *      'section' => [
513          *          'key' => 'value',
514          *      ],
515          * ];
516          *
517          * @param string $filepath The filepath of the
518          *
519          * @return array The config array0
520          *
521          * @throws ConfigFileException if the config cannot get loaded.
522          */
523         private function loadConfigFile(string $filepath): array
524         {
525                 if (file_exists($filepath)) {
526                         $config = include $filepath;
527
528                         if (!is_array($config)) {
529                                 throw new ConfigFileException('Error loading config file ' . $filepath);
530                         }
531
532                         return $config;
533                 } else {
534                         return [];
535                 }
536         }
537 }