‪TYPO3CMS  ‪main
SiteConfiguration.php
Go to the documentation of this file.
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 
19 
20 use Psr\EventDispatcher\EventDispatcherInterface;
21 use Symfony\Component\Finder\Finder;
22 use Symfony\Component\Yaml\Yaml;
38 
47 {
53  protected string ‪$configFileName = 'config.yaml';
54 
60  protected string ‪$settingsFileName = 'settings.yaml';
61 
67  protected string ‪$contentSecurityFileName = 'csp.yaml';
68 
74  protected string ‪$cacheIdentifier = 'sites-configuration';
75 
82  protected ‪$firstLevelCache;
83 
84  public function ‪__construct(
85  protected string $configPath,
86  protected EventDispatcherInterface $eventDispatcher,
87  protected ‪PhpFrontend $cache
88  ) {}
89 
95  public function ‪getAllExistingSites(bool $useCache = true): array
96  {
97  if ($useCache && $this->firstLevelCache !== null) {
99  }
100  return $this->‪resolveAllExistingSites($useCache);
101  }
102 
108  public function ‪createNewBasicSite(string ‪$identifier, int $rootPageId, string $base): void
109  {
110  // Create a default site configuration called "main" as best practice
111  $this->‪write($identifier, [
112  'rootPageId' => $rootPageId,
113  'base' => $base,
114  'languages' => [
115  0 => [
116  'title' => 'English',
117  'enabled' => true,
118  'languageId' => 0,
119  'base' => '/',
120  'locale' => 'en_US.UTF-8',
121  'navigationTitle' => 'English',
122  'flag' => 'us',
123  ],
124  ],
125  'errorHandling' => [],
126  'routes' => [],
127  ]);
128  }
129 
135  public function ‪resolveAllExistingSites(bool $useCache = true): array
136  {
137  $sites = [];
138  $siteConfiguration = $this->‪getAllSiteConfigurationFromFiles($useCache);
139  foreach ($siteConfiguration as ‪$identifier => $configuration) {
140  // cast $identifier to string, as the identifier can potentially only consist of (int) digit numbers
141  ‪$identifier = (string)‪$identifier;
142  $siteSettings = $this->‪getSiteSettings(‪$identifier, $configuration);
143  $configuration['contentSecurityPolicies'] = $this->‪getContentSecurityPolicies(‪$identifier);
144 
145  $rootPageId = (int)($configuration['rootPageId'] ?? 0);
146  if ($rootPageId > 0) {
147  $sites[‪$identifier] = new Site(‪$identifier, $rootPageId, $configuration, $siteSettings);
148  }
149  }
150  $this->firstLevelCache = $sites;
151  return $sites;
152  }
153 
161  public function ‪resolveAllExistingSitesRaw(): array
162  {
163  $sites = [];
164  $siteConfiguration = $this->‪getAllSiteConfigurationFromFiles(false);
165  foreach ($siteConfiguration as ‪$identifier => $configuration) {
166  // cast $identifier to string, as the identifier can potentially only consist of (int) digit numbers
167  ‪$identifier = (string)‪$identifier;
168  $siteSettings = new SiteSettings($configuration['settings'] ?? []);
169 
170  $rootPageId = (int)($configuration['rootPageId'] ?? 0);
171  if ($rootPageId > 0) {
172  $sites[‪$identifier] = new Site(‪$identifier, $rootPageId, $configuration, $siteSettings);
173  }
174  }
175  return $sites;
176  }
177 
183  public function ‪getAllSiteConfigurationPaths(): array
184  {
185  ‪$finder = new Finder();
186  $paths = [];
187  try {
188  ‪$finder->files()->depth(0)->name($this->configFileName)->in($this->configPath . '/*');
189  } catch (\InvalidArgumentException $e) {
190  ‪$finder = [];
191  }
192 
193  foreach (‪$finder as $fileInfo) {
194  $path = $fileInfo->getPath();
195  $paths[basename($path)] = $path;
196  }
197  return $paths;
198  }
199 
205  protected function ‪getAllSiteConfigurationFromFiles(bool $useCache = true): array
206  {
207  // Check if the data is already cached
208  $siteConfiguration = $useCache ? $this->cache->require($this->cacheIdentifier) : false;
209  if ($siteConfiguration !== false) {
210  return $siteConfiguration;
211  }
212  ‪$finder = new Finder();
213  try {
214  ‪$finder->files()->depth(0)->name($this->configFileName)->in($this->configPath . '/*');
215  } catch (\InvalidArgumentException $e) {
216  // Directory $this->configPath does not exist yet
217  ‪$finder = [];
218  }
219  $loader = GeneralUtility::makeInstance(YamlFileLoader::class);
220  $siteConfiguration = [];
221  foreach (‪$finder as $fileInfo) {
222  $configuration = $loader->load(GeneralUtility::fixWindowsFilePath((string)$fileInfo));
223  ‪$identifier = basename($fileInfo->getPath());
224  $event = $this->eventDispatcher->dispatch(new SiteConfigurationLoadedEvent(‪$identifier, $configuration));
225  $siteConfiguration[‪$identifier] = $event->getConfiguration();
226  }
227  $this->cache->set($this->cacheIdentifier, 'return ' . var_export($siteConfiguration, true) . ';');
228 
229  return $siteConfiguration;
230  }
231 
242  public function ‪load(string ‪$siteIdentifier): array
243  {
244  $fileName = $this->configPath . '/' . ‪$siteIdentifier . '/' . ‪$this->configFileName;
245  $loader = GeneralUtility::makeInstance(YamlFileLoader::class);
246  return $loader->load(GeneralUtility::fixWindowsFilePath($fileName), ‪YamlFileLoader::PROCESS_IMPORTS);
247  }
248 
258  protected function ‪getSiteSettings(string ‪$siteIdentifier, array $siteConfiguration): SiteSettings
259  {
260  $fileName = $this->configPath . '/' . ‪$siteIdentifier . '/' . ‪$this->settingsFileName;
261  if (file_exists($fileName)) {
262  $loader = GeneralUtility::makeInstance(YamlFileLoader::class);
263  $settings = $loader->load(GeneralUtility::fixWindowsFilePath($fileName));
264  } else {
265  $settings = $siteConfiguration['settings'] ?? [];
266  }
267  return new SiteSettings($settings);
268  }
269 
270  protected function ‪getContentSecurityPolicies(string ‪$siteIdentifier): array
271  {
272  $fileName = $this->configPath . '/' . ‪$siteIdentifier . '/' . ‪$this->contentSecurityFileName;
273  if (file_exists($fileName)) {
274  $loader = GeneralUtility::makeInstance(YamlFileLoader::class);
275  return $loader->load(GeneralUtility::fixWindowsFilePath($fileName), ‪YamlFileLoader::PROCESS_IMPORTS);
276  }
277  return [];
278  }
279 
280  public function ‪writeSettings(string ‪$siteIdentifier, array $settings): void
281  {
282  $fileName = $this->configPath . '/' . ‪$siteIdentifier . '/' . ‪$this->settingsFileName;
283  $yamlFileContents = Yaml::dump($settings, 99, 2);
284  if (!‪GeneralUtility::writeFile($fileName, $yamlFileContents)) {
285  throw new ‪SiteConfigurationWriteException('Unable to write site settings in sites/' . ‪$siteIdentifier . '/' . $this->configFileName, 1590487411);
286  }
287  }
288 
296  public function ‪write(string ‪$siteIdentifier, array $configuration, bool $protectPlaceholders = false): void
297  {
298  $folder = $this->configPath . '/' . ‪$siteIdentifier;
299  $fileName = $folder . '/' . ‪$this->configFileName;
300  $newConfiguration = $configuration;
301  if (!file_exists($folder)) {
303  if ($protectPlaceholders && $newConfiguration !== []) {
304  $newConfiguration = $this->protectPlaceholders([], $newConfiguration);
305  }
306  } elseif (file_exists($fileName)) {
307  $loader = GeneralUtility::makeInstance(YamlFileLoader::class);
308  // load without any processing to have the unprocessed base to modify
309  $newConfiguration = $loader->load(GeneralUtility::fixWindowsFilePath($fileName), 0);
310  // load the processed configuration to diff changed values
311  $processed = $loader->load(GeneralUtility::fixWindowsFilePath($fileName));
312  // find properties that were modified via GUI
313  $newModified = array_replace_recursive(
314  self::findRemoved($processed, $configuration),
315  self::findModified($processed, $configuration)
316  );
317  if ($protectPlaceholders && $newModified !== []) {
318  $newModified = $this->protectPlaceholders($newConfiguration, $newModified);
319  }
320  // change _only_ the modified keys, leave the original non-changed areas alone
321  ArrayUtility::mergeRecursiveWithOverrule($newConfiguration, $newModified);
322  }
323  $event = $this->eventDispatcher->dispatch(new SiteConfigurationBeforeWriteEvent(‪$siteIdentifier, $newConfiguration));
324  $newConfiguration = $this->‪sortConfiguration($event->getConfiguration());
325  $yamlFileContents = Yaml::dump($newConfiguration, 99, 2);
326  if (!‪GeneralUtility::writeFile($fileName, $yamlFileContents)) {
327  throw new SiteConfigurationWriteException('Unable to write site configuration in sites/' . ‪$siteIdentifier . '/' . $this->configFileName, 1590487011);
328  }
329  $this->firstLevelCache = null;
330  $this->cache->remove($this->cacheIdentifier);
331  }
332 
338  public function ‪rename(string $currentIdentifier, string $newIdentifier): void
339  {
340  if (!‪rename($this->configPath . '/' . $currentIdentifier, $this->configPath . '/' . $newIdentifier)) {
341  throw new SiteConfigurationWriteException('Unable to rename folder sites/' . $currentIdentifier, 1522491300);
342  }
343  $this->cache->remove($this->cacheIdentifier);
344  $this->firstLevelCache = null;
345  }
346 
353  public function delete(string ‪$siteIdentifier): void
354  {
355  $sites = $this->‪getAllExistingSites();
356  if (!isset($sites[‪$siteIdentifier])) {
357  throw new SiteNotFoundException('Site configuration named ' . ‪$siteIdentifier . ' not found.', 1522866183);
358  }
359  $fileName = $this->configPath . '/' . ‪$siteIdentifier . '/' . ‪$this->configFileName;
360  if (!file_exists($fileName)) {
361  throw new SiteNotFoundException('Site configuration file ' . $this->configFileName . ' within the site ' . ‪$siteIdentifier . ' not found.', 1522866184);
362  }
363  if (!unlink($fileName)) {
364  throw new SiteConfigurationWriteException('Unable to delete folder sites/' . ‪$siteIdentifier, 1596462020);
365  }
366  $this->cache->remove($this->cacheIdentifier);
367  $this->firstLevelCache = null;
368  }
369 
379  protected function protectPlaceholders(array $existingConfiguration, array $modifiedConfiguration): array
380  {
381  try {
382  return GeneralUtility::makeInstance(YamlPlaceholderGuard::class, $existingConfiguration)
383  ->process($modifiedConfiguration);
384  } catch (‪YamlPlaceholderException $exception) {
385  throw new ‪SiteConfigurationWriteException($exception->getMessage(), 1670361271, $exception);
386  }
387  }
388 
389  protected function ‪sortConfiguration(array $newConfiguration): array
390  {
391  ksort($newConfiguration);
392  if (isset($newConfiguration['imports'])) {
393  $imports = $newConfiguration['imports'];
394  unset($newConfiguration['imports']);
395  $newConfiguration['imports'] = $imports;
396  }
397  return $newConfiguration;
398  }
399 
400  protected static function ‪findModified(array $currentConfiguration, array $newConfiguration): array
401  {
402  $differences = [];
403  foreach ($newConfiguration as $key => $value) {
404  if (!isset($currentConfiguration[$key]) || $currentConfiguration[$key] !== $newConfiguration[$key]) {
405  if (!isset($newConfiguration[$key]) && isset($currentConfiguration[$key])) {
406  $differences[$key] = '__UNSET';
407  } elseif (isset($currentConfiguration[$key])
408  && is_array($newConfiguration[$key])
409  && is_array($currentConfiguration[$key])
410  ) {
411  $differences[$key] = ‪self::findModified($currentConfiguration[$key], $newConfiguration[$key]);
412  } else {
413  $differences[$key] = $value;
414  }
415  }
416  }
417  return $differences;
418  }
419 
420  protected static function ‪findRemoved(array $currentConfiguration, array $newConfiguration): array
421  {
422  $removed = [];
423  foreach ($currentConfiguration as $key => $value) {
424  if (!isset($newConfiguration[$key])) {
425  $removed[$key] = '__UNSET';
426  } elseif (isset($currentConfiguration[$key]) && is_array($currentConfiguration[$key]) && is_array($newConfiguration[$key])) {
427  $removedInRecursion = ‪self::findRemoved($currentConfiguration[$key], $newConfiguration[$key]);
428  if (!empty($removedInRecursion)) {
429  $removed[$key] = $removedInRecursion;
430  }
431  }
432  }
433 
434  return $removed;
435  }
436 
437  public function ‪warmupCaches(‪CacheWarmupEvent $event): void
438  {
439  if ($event->‪hasGroup('system')) {
441  }
442  }
443 }
‪TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader\PROCESS_IMPORTS
‪const PROCESS_IMPORTS
Definition: YamlFileLoader.php:52
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\$settingsFileName
‪string $settingsFileName
Definition: SiteConfiguration.php:60
‪TYPO3\CMS\Core\Configuration\Exception\SiteConfigurationWriteException
Definition: SiteConfigurationWriteException.php:27
‪$finder
‪if(PHP_SAPI !=='cli') $finder
Definition: header-comment.php:22
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\getAllSiteConfigurationFromFiles
‪getAllSiteConfigurationFromFiles(bool $useCache=true)
Definition: SiteConfiguration.php:204
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\__construct
‪__construct(protected string $configPath, protected EventDispatcherInterface $eventDispatcher, protected PhpFrontend $cache)
Definition: SiteConfiguration.php:83
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\getAllExistingSites
‪Site[] getAllExistingSites(bool $useCache=true)
Definition: SiteConfiguration.php:94
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\findRemoved
‪static findRemoved(array $currentConfiguration, array $newConfiguration)
Definition: SiteConfiguration.php:419
‪TYPO3\CMS\Core\Cache\Frontend\PhpFrontend
Definition: PhpFrontend.php:25
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\rename
‪rename(string $currentIdentifier, string $newIdentifier)
Definition: SiteConfiguration.php:337
‪TYPO3\CMS\Core\Exception\SiteNotFoundException
Definition: SiteNotFoundException.php:25
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\writeSettings
‪writeSettings(string $siteIdentifier, array $settings)
Definition: SiteConfiguration.php:279
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\warmupCaches
‪warmupCaches(CacheWarmupEvent $event)
Definition: SiteConfiguration.php:436
‪TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent
Definition: CacheWarmupEvent.php:24
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\getSiteSettings
‪getSiteSettings(string $siteIdentifier, array $siteConfiguration)
Definition: SiteConfiguration.php:257
‪TYPO3\CMS\Core\Utility\GeneralUtility\mkdir_deep
‪static mkdir_deep(string $directory)
Definition: GeneralUtility.php:1650
‪TYPO3\CMS\Core\Configuration\SiteConfiguration
Definition: SiteConfiguration.php:47
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\load
‪load(string $siteIdentifier)
Definition: SiteConfiguration.php:241
‪TYPO3\CMS\Core\Site\Entity\Site
Definition: Site.php:42
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\getAllSiteConfigurationPaths
‪getAllSiteConfigurationPaths()
Definition: SiteConfiguration.php:182
‪TYPO3\CMS\Core\Configuration\Loader\Exception\YamlPlaceholderException
Definition: YamlPlaceholderException.php:20
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\resolveAllExistingSites
‪Site[] resolveAllExistingSites(bool $useCache=true)
Definition: SiteConfiguration.php:134
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\resolveAllExistingSitesRaw
‪Site[] resolveAllExistingSitesRaw()
Definition: SiteConfiguration.php:160
‪TYPO3\CMS\Core\Utility\GeneralUtility\writeFile
‪static bool writeFile(string $file, string $content, bool $changePermissions=false)
Definition: GeneralUtility.php:1465
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\sortConfiguration
‪catch(YamlPlaceholderException $exception) sortConfiguration(array $newConfiguration)
Definition: SiteConfiguration.php:388
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\$firstLevelCache
‪array null $firstLevelCache
Definition: SiteConfiguration.php:81
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\createNewBasicSite
‪createNewBasicSite(string $identifier, int $rootPageId, string $base)
Definition: SiteConfiguration.php:107
‪TYPO3\CMS\Core\Cache\Exception\InvalidDataException
Definition: InvalidDataException.php:23
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\$configFileName
‪string $configFileName
Definition: SiteConfiguration.php:53
‪TYPO3\CMS\Core\Configuration\Event\SiteConfigurationLoadedEvent
Definition: SiteConfigurationLoadedEvent.php:25
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\$cacheIdentifier
‪string $cacheIdentifier
Definition: SiteConfiguration.php:74
‪TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader
Definition: YamlFileLoader.php:47
‪TYPO3\CMS\Core\Configuration\Event\SiteConfigurationBeforeWriteEvent
Definition: SiteConfigurationBeforeWriteEvent.php:25
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\getContentSecurityPolicies
‪getContentSecurityPolicies(string $siteIdentifier)
Definition: SiteConfiguration.php:269
‪TYPO3\CMS\Core\Utility\ArrayUtility
Definition: ArrayUtility.php:26
‪TYPO3\CMS\Core\SingletonInterface
Definition: SingletonInterface.php:22
‪TYPO3\CMS\Core\Configuration
Definition: CKEditor5Migrator.php:18
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\$contentSecurityFileName
‪string $contentSecurityFileName
Definition: SiteConfiguration.php:67
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\findModified
‪static findModified(array $currentConfiguration, array $newConfiguration)
Definition: SiteConfiguration.php:399
‪TYPO3\CMS\Webhooks\Message\$siteIdentifier
‪identifier readonly int readonly array readonly string readonly string $siteIdentifier
Definition: PageModificationMessage.php:38
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Site\Entity\SiteSettings
Definition: SiteSettings.php:28
‪TYPO3\CMS\Core\Configuration\Loader\YamlPlaceholderGuard
Definition: YamlPlaceholderGuard.php:27
‪TYPO3\CMS\Webhooks\Message\$identifier
‪identifier readonly string $identifier
Definition: FileAddedMessage.php:37
‪TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent\hasGroup
‪hasGroup(string $group)
Definition: CacheWarmupEvent.php:34
‪TYPO3\CMS\Core\Configuration\SiteConfiguration\write
‪write(string $siteIdentifier, array $configuration, bool $protectPlaceholders=false)
Definition: SiteConfiguration.php:295