Passed
Push — master ( e833d0...081de4 )
by
unknown
13:18
created

SiteConfiguration::write()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 26
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 18
nc 3
nop 2
dl 0
loc 26
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Core\Configuration;
19
20
use Symfony\Component\Finder\Finder;
21
use Symfony\Component\Yaml\Yaml;
22
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
23
use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
24
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
25
use TYPO3\CMS\Core\SingletonInterface;
26
use TYPO3\CMS\Core\Site\Entity\Site;
27
use TYPO3\CMS\Core\Utility\ArrayUtility;
28
use TYPO3\CMS\Core\Utility\GeneralUtility;
29
30
/**
31
 * Responsibility: Handles the format of the configuration (currently yaml), and the location of the file system folder
32
 *
33
 * Reads all available site configuration options, and puts them into Site objects.
34
 *
35
 * @internal
36
 */
37
class SiteConfiguration implements SingletonInterface
38
{
39
    protected PhpFrontend $cache;
40
41
    /**
42
     * @var string
43
     */
44
    protected $configPath;
45
46
    /**
47
     * Config yaml file name.
48
     *
49
     * @internal
50
     * @var string
51
     */
52
    protected $configFileName = 'config.yaml';
53
54
    /**
55
     * Identifier to store all configuration data in cache_core cache.
56
     *
57
     * @internal
58
     * @var string
59
     */
60
    protected $cacheIdentifier = 'sites-configuration';
61
62
    /**
63
     * Cache stores all configuration as Site objects, as long as they haven't been changed.
64
     * This drastically improves performance as SiteFinder utilizes SiteConfiguration heavily
65
     *
66
     * @var array|null
67
     */
68
    protected $firstLevelCache;
69
70
    /**
71
     * @param string $configPath
72
     * @param PhpFrontend $coreCache
73
     */
74
    public function __construct(string $configPath, PhpFrontend $coreCache = null)
75
    {
76
        $this->configPath = $configPath;
77
        // The following fallback to GeneralUtility;:getContainer() is only used in acceptance tests
78
        // @todo: Fix testing-framework/typo3/sysext/core/Classes/Configuration/SiteConfiguration.php
79
        // to inject the cache instance
80
        $this->cache = $coreCache ?? GeneralUtility::getContainer()->get('cache.core');
81
    }
82
83
    /**
84
     * Return all site objects which have been found in the filesystem.
85
     *
86
     * @param bool $useCache
87
     * @return Site[]
88
     */
89
    public function getAllExistingSites(bool $useCache = true): array
90
    {
91
        if ($useCache && $this->firstLevelCache !== null) {
92
            return $this->firstLevelCache;
93
        }
94
        return $this->resolveAllExistingSites($useCache);
95
    }
96
97
    /**
98
     * Creates a site configuration with one language "English" which is the de-facto default language for TYPO3 in general.
99
     *
100
     * @param string $identifier
101
     * @param int $rootPageId
102
     * @param string $base
103
     */
104
    public function createNewBasicSite(string $identifier, int $rootPageId, string $base): void
105
    {
106
        // Create a default site configuration called "main" as best practice
107
        $this->write($identifier, [
108
            'rootPageId' => $rootPageId,
109
            'base' => $base,
110
            'languages' => [
111
                0 => [
112
                    'title' => 'English',
113
                    'enabled' => true,
114
                    'languageId' => 0,
115
                    'base' => '/',
116
                    'typo3Language' => 'default',
117
                    'locale' => 'en_US.UTF-8',
118
                    'iso-639-1' => 'en',
119
                    'navigationTitle' => 'English',
120
                    'hreflang' => 'en-us',
121
                    'direction' => 'ltr',
122
                    'flag' => 'us',
123
                ],
124
            ],
125
            'errorHandling' => [],
126
            'routes' => [],
127
        ]);
128
    }
129
130
    /**
131
     * Resolve all site objects which have been found in the filesystem.
132
     *
133
     * @param bool $useCache
134
     * @return Site[]
135
     */
136
    public function resolveAllExistingSites(bool $useCache = true): array
137
    {
138
        $sites = [];
139
        $siteConfiguration = $this->getAllSiteConfigurationFromFiles($useCache);
140
        foreach ($siteConfiguration as $identifier => $configuration) {
141
            $rootPageId = (int)($configuration['rootPageId'] ?? 0);
142
            if ($rootPageId > 0) {
143
                $sites[$identifier] = GeneralUtility::makeInstance(Site::class, $identifier, $rootPageId, $configuration);
144
            }
145
        }
146
        $this->firstLevelCache = $sites;
147
        return $sites;
148
    }
149
150
    /**
151
     * Read the site configuration from config files.
152
     *
153
     * @param bool $useCache
154
     * @return array
155
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
156
     */
157
    protected function getAllSiteConfigurationFromFiles(bool $useCache = true): array
158
    {
159
        // Check if the data is already cached
160
        $siteConfiguration = $useCache ? $this->cache->require($this->cacheIdentifier) : false;
161
        if ($siteConfiguration !== false) {
162
            return $siteConfiguration;
163
        }
164
        $finder = new Finder();
165
        try {
166
            $finder->files()->depth(0)->name($this->configFileName)->in($this->configPath . '/*');
167
        } catch (\InvalidArgumentException $e) {
168
            // Directory $this->configPath does not exist yet
169
            $finder = [];
170
        }
171
        $loader = GeneralUtility::makeInstance(YamlFileLoader::class);
172
        $siteConfiguration = [];
173
        foreach ($finder as $fileInfo) {
174
            $configuration = $loader->load(GeneralUtility::fixWindowsFilePath((string)$fileInfo));
175
            $identifier = basename($fileInfo->getPath());
176
            $siteConfiguration[$identifier] = $configuration;
177
        }
178
        $this->cache->set($this->cacheIdentifier, 'return ' . var_export($siteConfiguration, true) . ';');
179
180
        return $siteConfiguration;
181
    }
182
183
    /**
184
     * Load plain configuration
185
     * This method should only be used in case the original configuration as it exists in the file should be loaded,
186
     * for example for writing / editing configuration.
187
     *
188
     * All read related actions should be performed on the site entity.
189
     *
190
     * @param string $siteIdentifier
191
     * @return array
192
     */
193
    public function load(string $siteIdentifier): array
194
    {
195
        $fileName = $this->configPath . '/' . $siteIdentifier . '/' . $this->configFileName;
196
        $loader = GeneralUtility::makeInstance(YamlFileLoader::class);
197
        return $loader->load(GeneralUtility::fixWindowsFilePath($fileName), YamlFileLoader::PROCESS_IMPORTS);
198
    }
199
200
    /**
201
     * Add or update a site configuration
202
     *
203
     * @param string $siteIdentifier
204
     * @param array $configuration
205
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
206
     */
207
    public function write(string $siteIdentifier, array $configuration): void
208
    {
209
        $folder = $this->configPath . '/' . $siteIdentifier;
210
        $fileName = $folder . '/' . $this->configFileName;
211
        $newConfiguration = $configuration;
212
        if (!file_exists($folder)) {
213
            GeneralUtility::mkdir_deep($folder);
214
        } elseif (file_exists($fileName)) {
215
            $loader = GeneralUtility::makeInstance(YamlFileLoader::class);
216
            // load without any processing to have the unprocessed base to modify
217
            $newConfiguration = $loader->load(GeneralUtility::fixWindowsFilePath($fileName), 0);
218
            // load the processed configuration to diff changed values
219
            $processed = $loader->load(GeneralUtility::fixWindowsFilePath($fileName));
220
            // find properties that were modified via GUI
221
            $newModified = array_replace_recursive(
222
                self::findRemoved($processed, $configuration),
223
                self::findModified($processed, $configuration)
224
            );
225
            // change _only_ the modified keys, leave the original non-changed areas alone
226
            ArrayUtility::mergeRecursiveWithOverrule($newConfiguration, $newModified);
227
        }
228
        $newConfiguration = $this->sortConfiguration($newConfiguration);
229
        $yamlFileContents = Yaml::dump($newConfiguration, 99, 2);
230
        GeneralUtility::writeFile($fileName, $yamlFileContents);
231
        $this->firstLevelCache = null;
232
        $this->cache->remove($this->cacheIdentifier);
233
    }
234
235
    /**
236
     * Renames a site identifier (and moves the folder)
237
     *
238
     * @param string $currentIdentifier
239
     * @param string $newIdentifier
240
     * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
241
     */
242
    public function rename(string $currentIdentifier, string $newIdentifier): void
243
    {
244
        $result = rename($this->configPath . '/' . $currentIdentifier, $this->configPath . '/' . $newIdentifier);
245
        if (!$result) {
246
            throw new \RuntimeException('Unable to rename folder sites/' . $currentIdentifier, 1522491300);
247
        }
248
        $this->cache->remove($this->cacheIdentifier);
249
        $this->firstLevelCache = null;
250
    }
251
252
    /**
253
     * Removes the config.yaml file of a site configuration.
254
     * Also clears the cache.
255
     *
256
     * @param string $siteIdentifier
257
     * @throws SiteNotFoundException
258
     */
259
    public function delete(string $siteIdentifier): void
260
    {
261
        $sites = $this->getAllExistingSites();
262
        if (!isset($sites[$siteIdentifier])) {
263
            throw new SiteNotFoundException('Site configuration named ' . $siteIdentifier . ' not found.', 1522866183);
264
        }
265
        $fileName = $this->configPath . '/' . $siteIdentifier . '/' . $this->configFileName;
266
        if (!file_exists($fileName)) {
267
            throw new SiteNotFoundException('Site configuration file ' . $this->configFileName . ' within the site ' . $siteIdentifier . ' not found.', 1522866184);
268
        }
269
        @unlink($fileName);
270
        $this->cache->remove($this->cacheIdentifier);
271
        $this->firstLevelCache = null;
272
    }
273
274
    /**
275
     * @param array $newConfiguration
276
     * @return array
277
     */
278
    protected function sortConfiguration(array $newConfiguration): array
279
    {
280
        ksort($newConfiguration);
281
        if (isset($newConfiguration['imports'])) {
282
            $imports = $newConfiguration['imports'];
283
            unset($newConfiguration['imports']);
284
            $newConfiguration['imports'] = $imports;
285
        }
286
        return $newConfiguration;
287
    }
288
289
    protected static function findModified(array $currentConfiguration, array $newConfiguration): array
290
    {
291
        $differences = [];
292
        foreach ($newConfiguration as $key => $value) {
293
            if (!isset($currentConfiguration[$key]) || $currentConfiguration[$key] !== $newConfiguration[$key]) {
294
                if (!isset($newConfiguration[$key]) && isset($currentConfiguration[$key])) {
295
                    $differences[$key] = '__UNSET';
296
                } elseif (isset($currentConfiguration[$key])
297
                    && is_array($newConfiguration[$key])
298
                    && is_array($currentConfiguration[$key])
299
                ) {
300
                    $differences[$key] = self::findModified($currentConfiguration[$key], $newConfiguration[$key]);
301
                } else {
302
                    $differences[$key] = $value;
303
                }
304
            }
305
        }
306
        return $differences;
307
    }
308
309
    protected static function findRemoved(array $currentConfiguration, array $newConfiguration): array
310
    {
311
        $removed = [];
312
        foreach ($currentConfiguration as $key => $value) {
313
            if (!isset($newConfiguration[$key])) {
314
                $removed[$key] = '__UNSET';
315
            } elseif (isset($currentConfiguration[$key]) && is_array($currentConfiguration[$key]) && is_array($newConfiguration[$key])) {
316
                $removedInRecursion = self::findRemoved($currentConfiguration[$key], $newConfiguration[$key]);
317
                if (!empty($removedInRecursion)) {
318
                    $removed[$key] = $removedInRecursion;
319
                }
320
            }
321
        }
322
323
        return $removed;
324
    }
325
}
326