]> git.mxchange.org Git - friendica.git/blob - src/Core/Config/Util/ConfigFileManager.php
Apply suggestions from code review
[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)) {
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                         $configStream = fopen($filename, 'r');
192                         try {
193                                 if (flock($configStream, LOCK_SH)) {
194                                         $content = fread($configStream, filesize($filename));
195                                         if (!$content) {
196                                                 throw new ConfigFileException(sprintf('Couldn\'t read file %s', $filename));
197                                         }
198                                 }
199                         } finally {
200                                 // unlock and close the stream for every circumstances
201                                 flock($configStream, LOCK_UN);
202                                 fclose($configStream);
203                         }
204
205                         /**
206                          * Evaluate the fetched content
207                          *
208                          * @note
209                          * Because `eval()` directly evaluates PHP content, we need to "close" the expected PHP content again with
210                          * the prefixed "?>". Now we're in plain HTML again and can evaluate any PHP file :-)
211                          */
212                         $dataArray = eval('?>' . $content);
213
214                         if (is_array($dataArray)) {
215                                 $configCache->load($dataArray, Cache::SOURCE_DATA);
216                         }
217                 }
218         }
219
220         /**
221          * Saves overridden config entries back into the data.config.php
222          *
223          * @param Cache $configCache The config cache
224          *
225          * @throws ConfigFileException In case the config file isn't writeable or the data is invalid
226          */
227         public function saveData(Cache $configCache)
228         {
229                 $filename = $this->configDir . '/' . self::CONFIG_DATA_FILE;
230
231                 if (file_exists($filename)) {
232                         $fileExists = true;
233                 } else {
234                         $fileExists = false;
235                 }
236
237                 /**
238                  * Creates a read-write stream
239                  *
240                  * @see https://www.php.net/manual/en/function.fopen.php
241                  * @note Open the file for reading and writing. If the file does not exist, it is created.
242                  * If it exists, it is neither truncated (as opposed to 'w'), nor the call to this function fails
243                  * (as is the case with 'x'). The file pointer is positioned on the beginning of the file.
244                  *
245                  */
246                 $configStream = fopen($filename, 'c+');
247
248                 try {
249                         // We do want an exclusive lock, so we wait until every LOCK_SH (config reading) is unlocked
250                         if (flock($configStream, LOCK_EX)) {
251
252                                 /**
253                                  * 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
254                                  * Since we're currently exclusive locked, no other process can now change the config again
255                                  */
256                                 if ($fileExists) {
257                                         // When reading the config file too fast, we get a wrong filesize, "clearstatcache" prevents that
258                                         clearstatcache(true, $filename);
259                                         $content = fread($configStream, filesize($filename));
260                                         if (!$content) {
261                                                 throw new ConfigFileException(sprintf('Cannot read file %s', $filename));
262                                         }
263
264                                         // Event truncating the whole content wouldn't automatically rewind the stream,
265                                         // so we need to do it manually
266                                         rewind($configStream);
267
268                                         $dataArray = eval('?>' . $content);
269
270                                         // Merge the new content into the existing file based config cache and use it
271                                         // as the new config cache
272                                         if (is_array($dataArray)) {
273                                                 $fileConfigCache = new Cache();
274                                                 $fileConfigCache->load($dataArray, Cache::SOURCE_DATA);
275                                                 $configCache = $fileConfigCache->merge($configCache);
276                                         }
277                                 }
278
279                                 // Only SOURCE_DATA is wanted, the rest isn't part of the node.config.php file
280                                 $data = $configCache->getDataBySource(Cache::SOURCE_DATA);
281
282                                 $encodedData = ConfigFileTransformer::encode($data);
283                                 if (!$encodedData) {
284                                         throw new ConfigFileException('config source cannot get encoded');
285                                 }
286
287                                 // Once again to avoid wrong, implicit "filesize" calls during the fwrite() or ftruncate() call
288                                 clearstatcache(true, $filename);
289                                 if (!ftruncate($configStream, 0) ||
290                                         !fwrite($configStream, $encodedData) ||
291                                         !fflush($configStream)) {
292                                         throw new ConfigFileException(sprintf('Cannot modify locked file %s', $filename));
293                                 }
294                         }
295                 } finally {
296                         // unlock and close the stream for every circumstances
297                         flock($configStream, LOCK_UN);
298                         fclose($configStream);
299                 }
300         }
301
302         /**
303          * Tries to load the specified addon-configuration and returns the config array.
304          *
305          * @param string $name The name of the configuration
306          *
307          * @return array The config array (empty if no config found)
308          *
309          * @throws ConfigFileException if the configuration file isn't readable
310          */
311         public function loadAddonConfig(string $name): array
312         {
313                 $filepath = $this->baseDir . DIRECTORY_SEPARATOR .   // /var/www/html/
314                                         Addon::DIRECTORY . DIRECTORY_SEPARATOR . // addon/
315                                         $name . DIRECTORY_SEPARATOR .            // openstreetmap/
316                                         'config' . DIRECTORY_SEPARATOR .         // config/
317                                         $name . ".config.php";                   // openstreetmap.config.php
318
319                 if (file_exists($filepath)) {
320                         return $this->loadConfigFile($filepath);
321                 } else {
322                         return [];
323                 }
324         }
325
326         /**
327          * Tries to load environment specific variables, based on the `env.config.php` mapping table
328          *
329          * @return array The config array (empty if no config was found)
330          *
331          * @throws ConfigFileException if the configuration file isn't readable
332          */
333         protected function loadEnvConfig(): array
334         {
335                 $filepath = $this->staticDir . DIRECTORY_SEPARATOR .   // /var/www/html/static/
336                                         "env.config.php";                          // env.config.php
337
338                 if (!file_exists($filepath)) {
339                         return [];
340                 }
341
342                 $envConfig = $this->loadConfigFile($filepath);
343
344                 $return = [];
345
346                 foreach ($envConfig as $envKey => $configStructure) {
347                         if (isset($this->server[$envKey])) {
348                                 $return[$configStructure[0]][$configStructure[1]] = $this->server[$envKey];
349                         }
350                 }
351
352                 return $return;
353         }
354
355         /**
356          * Get the config files of the config-directory
357          *
358          * @param bool $ini True, if scan for ini-files instead of config files
359          *
360          * @return array
361          */
362         private function getConfigFiles(bool $ini = false): array
363         {
364                 $files = scandir($this->configDir);
365                 $found = [];
366
367                 $filePattern = ($ini ? '*.ini.php' : '*.config.php');
368
369                 // Don't load sample files
370                 $sampleEnd = self::SAMPLE_END . ($ini ? '.ini.php' : '.config.php');
371
372                 foreach ($files as $filename) {
373                         if (fnmatch($filePattern, $filename) &&
374                                 substr_compare($filename, $sampleEnd, -strlen($sampleEnd)) &&
375                                 $filename !== self::CONFIG_DATA_FILE) {
376                                 $found[] = $this->configDir . '/' . $filename;
377                         }
378                 }
379
380                 return $found;
381         }
382
383         /**
384          * Tries to load the legacy config files (.htconfig.php, .htpreconfig.php) and returns the config array.
385          *
386          * @param string $name The name of the config file (default is empty, which means .htconfig.php)
387          *
388          * @return array The configuration array (empty if no config found)
389          *
390          * @deprecated since version 2018.09
391          */
392         private function loadLegacyConfig(string $name = ''): array
393         {
394                 $name     = !empty($name) ? $name : self::CONFIG_HTCONFIG;
395                 $fullName = $this->baseDir . DIRECTORY_SEPARATOR . '.' . $name . '.php';
396
397                 $config = [];
398                 if (file_exists($fullName)) {
399                         $a         = new \stdClass();
400                         $a->config = [];
401                         include $fullName;
402
403                         $htConfigCategories = array_keys($a->config);
404
405                         // map the legacy configuration structure to the current structure
406                         foreach ($htConfigCategories as $htConfigCategory) {
407                                 if (is_array($a->config[$htConfigCategory])) {
408                                         $keys = array_keys($a->config[$htConfigCategory]);
409
410                                         foreach ($keys as $key) {
411                                                 $config[$htConfigCategory][$key] = $a->config[$htConfigCategory][$key];
412                                         }
413                                 } else {
414                                         $config['config'][$htConfigCategory] = $a->config[$htConfigCategory];
415                                 }
416                         }
417
418                         unset($a);
419
420                         if (isset($db_host)) {
421                                 $config['database']['hostname'] = $db_host;
422                                 unset($db_host);
423                         }
424                         if (isset($db_user)) {
425                                 $config['database']['username'] = $db_user;
426                                 unset($db_user);
427                         }
428                         if (isset($db_pass)) {
429                                 $config['database']['password'] = $db_pass;
430                                 unset($db_pass);
431                         }
432                         if (isset($db_data)) {
433                                 $config['database']['database'] = $db_data;
434                                 unset($db_data);
435                         }
436                         if (isset($config['system']['db_charset'])) {
437                                 $config['database']['charset'] = $config['system']['db_charset'];
438                         }
439                         if (isset($pidfile)) {
440                                 $config['system']['pidfile'] = $pidfile;
441                                 unset($pidfile);
442                         }
443                         if (isset($default_timezone)) {
444                                 $config['system']['default_timezone'] = $default_timezone;
445                                 unset($default_timezone);
446                         }
447                         if (isset($lang)) {
448                                 $config['system']['language'] = $lang;
449                                 unset($lang);
450                         }
451                 }
452
453                 return $config;
454         }
455
456         /**
457          * Tries to load the specified legacy configuration file and returns the config array.
458          *
459          * @param string $filepath
460          *
461          * @return array The configuration array
462          * @throws ConfigFileException
463          * @deprecated since version 2018.12
464          */
465         private function loadINIConfigFile(string $filepath): array
466         {
467                 $contents = include($filepath);
468
469                 $config = parse_ini_string($contents, true, INI_SCANNER_TYPED);
470
471                 if ($config === false) {
472                         throw new ConfigFileException('Error parsing INI config file ' . $filepath);
473                 }
474
475                 return $config;
476         }
477
478         /**
479          * Tries to load the specified configuration file and returns the config array.
480          *
481          * The config format is PHP array and the template for configuration files is the following:
482          *
483          * <?php return [
484          *      'section' => [
485          *          'key' => 'value',
486          *      ],
487          * ];
488          *
489          * @param string $filepath The filepath of the
490          *
491          * @return array The config array0
492          *
493          * @throws ConfigFileException if the config cannot get loaded.
494          */
495         private function loadConfigFile(string $filepath): array
496         {
497                 if (file_exists($filepath)) {
498                         $config = include $filepath;
499
500                         if (!is_array($config)) {
501                                 throw new ConfigFileException('Error loading config file ' . $filepath);
502                         }
503
504                         return $config;
505                 } else {
506                         return [];
507                 }
508         }
509 }