$rootPageId, 'base' => $base, 'languages' => [ 0 => [ 'title' => 'English', 'enabled' => true, 'languageId' => 0, 'base' => '/', 'locale' => 'en_US.UTF-8', 'navigationTitle' => 'English', 'flag' => 'us', ], ], 'errorHandling' => [], 'routes' => [], 'dependencies' => $dependencies, ]; $this->write($identifier, $configuration); } public function writeSettings(string $siteIdentifier, array $settings): void { $fileName = $this->configPath . '/' . $siteIdentifier . '/' . self::SETTINGS_FILE_NAME; if ($settings === []) { if (!is_file($fileName)) { return; } $yamlFileContents = '# No site specific settings defined'; } else { $yamlFileContents = Yaml::dump($settings, 99, 2, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_OBJECT_AS_MAP); } if (!GeneralUtility::writeFile($fileName, $yamlFileContents, true)) { throw new SiteConfigurationWriteException('Unable to write site settings in sites/' . $siteIdentifier . '/' . self::SETTINGS_FILE_NAME, 1590487411); } } /** * Add or update a site configuration * * @param bool $protectPlaceholders whether to disallow introducing new placeholders * @todo enforce $protectPlaceholders with TYPO3 v13.0 * @throws SiteConfigurationWriteException */ public function write(string $siteIdentifier, array $configuration, bool $protectPlaceholders = false): void { $folder = $this->configPath . '/' . $siteIdentifier; $fileName = $folder . '/' . self::CONFIG_FILE_NAME; $newConfiguration = $configuration; if (!file_exists($folder)) { GeneralUtility::mkdir_deep($folder); if ($protectPlaceholders && $newConfiguration !== []) { $newConfiguration = $this->protectPlaceholders([], $newConfiguration); } } elseif (file_exists($fileName)) { // load without any processing to have the unprocessed base to modify $newConfiguration = $this->yamlFileLoader->load(GeneralUtility::fixWindowsFilePath($fileName), 0); // load the processed configuration to diff changed values, // but don't process placeholders, because all properties that // were modified via GUI are unprocessed values as well $processed = $this->yamlFileLoader->load(GeneralUtility::fixWindowsFilePath($fileName), YamlFileLoader::PROCESS_IMPORTS); // find properties that were modified via GUI $newModified = array_replace_recursive( self::findRemoved($processed, $configuration), self::findModified($processed, $configuration) ); if ($protectPlaceholders && $newModified !== []) { $newModified = $this->protectPlaceholders($newConfiguration, $newModified); } // change _only_ the modified keys, leave the original non-changed areas alone ArrayUtility::mergeRecursiveWithOverrule($newConfiguration, $newModified); } $event = $this->eventDispatcher->dispatch(new SiteConfigurationBeforeWriteEvent($siteIdentifier, $newConfiguration)); $newConfiguration = $this->sortConfiguration($event->getConfiguration()); $yamlFileContents = Yaml::dump($newConfiguration, 99, 2); if (!GeneralUtility::writeFile($fileName, $yamlFileContents, true)) { throw new SiteConfigurationWriteException('Unable to write site configuration in sites/' . $siteIdentifier . '/' . self::CONFIG_FILE_NAME, 1590487011); } $this->eventDispatcher->dispatch(new SiteConfigurationChangedEvent($siteIdentifier)); } /** * Renames a site identifier (and moves the folder) * * @throws SiteConfigurationWriteException */ public function rename(string $currentIdentifier, string $newIdentifier): void { if (!rename($this->configPath . '/' . $currentIdentifier, $this->configPath . '/' . $newIdentifier)) { throw new SiteConfigurationWriteException('Unable to rename folder sites/' . $currentIdentifier, 1522491300); } $this->eventDispatcher->dispatch(new SiteConfigurationChangedEvent($newIdentifier)); } /** * Removes the config.yaml file of a site configuration. * Also clears the cache. * * @throws SiteNotFoundException|SiteConfigurationWriteException */ public function delete(string $siteIdentifier): void { $fileName = $this->configPath . '/' . $siteIdentifier . '/' . self::CONFIG_FILE_NAME; if (!file_exists($fileName)) { throw new SiteNotFoundException('Site configuration file ' . self::CONFIG_FILE_NAME . ' within the site ' . $siteIdentifier . ' not found.', 1522866184); } if (!unlink($fileName)) { throw new SiteConfigurationWriteException('Unable to delete folder sites/' . $siteIdentifier, 1596462020); } $this->eventDispatcher->dispatch(new SiteConfigurationChangedEvent($siteIdentifier)); } /** * Detects placeholders that have been introduced and handles* them. * (*) currently throws an exception, but could be purged or escaped as well * * @param array $existingConfiguration * @param array $modifiedConfiguration * @return array sanitized configuration (currently not used, exception thrown before) * @throws SiteConfigurationWriteException */ protected function protectPlaceholders(array $existingConfiguration, array $modifiedConfiguration): array { try { return GeneralUtility::makeInstance(YamlPlaceholderGuard::class, $existingConfiguration) ->process($modifiedConfiguration); } catch (YamlPlaceholderException $exception) { throw new SiteConfigurationWriteException($exception->getMessage(), 1670361271, $exception); } } protected function sortConfiguration(array $newConfiguration): array { ksort($newConfiguration); if (isset($newConfiguration['imports'])) { $imports = $newConfiguration['imports']; unset($newConfiguration['imports']); $newConfiguration['imports'] = $imports; } return $newConfiguration; } protected static function findModified(array $currentConfiguration, array $newConfiguration): array { $differences = []; foreach ($newConfiguration as $key => $value) { if (!isset($currentConfiguration[$key]) || $currentConfiguration[$key] !== $value) { if (!isset($value) && isset($currentConfiguration[$key])) { $differences[$key] = '__UNSET'; } elseif (isset($currentConfiguration[$key]) && is_array($value) && is_array($currentConfiguration[$key]) ) { $differences[$key] = self::findModified($currentConfiguration[$key], $value); } else { $differences[$key] = $value; } } } return $differences; } protected static function findRemoved(array $currentConfiguration, array $newConfiguration): array { $removed = []; foreach ($currentConfiguration as $key => $value) { if (!isset($newConfiguration[$key])) { $removed[$key] = '__UNSET'; } elseif (isset($value) && is_array($value) && is_array($newConfiguration[$key])) { $removedInRecursion = self::findRemoved($value, $newConfiguration[$key]); if (!empty($removedInRecursion)) { $removed[$key] = $removedInRecursion; } } } return $removed; } }