summaryrefslogtreecommitdiffstats
path: root/application/clicommands/MigrateCommand.php
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--application/clicommands/MigrateCommand.php835
1 files changed, 835 insertions, 0 deletions
diff --git a/application/clicommands/MigrateCommand.php b/application/clicommands/MigrateCommand.php
new file mode 100644
index 0000000..6d034ee
--- /dev/null
+++ b/application/clicommands/MigrateCommand.php
@@ -0,0 +1,835 @@
+<?php
+
+/* Icinga DB Web | (c) 2023 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Clicommands;
+
+use Icinga\Application\Config;
+use Icinga\Application\Logger;
+use Icinga\Cli\Command;
+use Icinga\Data\ConfigObject;
+use Icinga\Exception\NotReadableError;
+use Icinga\Exception\NotWritableError;
+use Icinga\Module\Icingadb\Compat\UrlMigrator;
+use Icinga\Util\DirectoryIterator;
+use Icinga\Web\Request;
+use ipl\Stdlib\Str;
+use ipl\Web\Filter\QueryString;
+use ipl\Web\Url;
+
+class MigrateCommand extends Command
+{
+ /** @var bool Skip the migration, only perform transformations */
+ protected $skipMigration = false;
+
+ public function init(): void
+ {
+ Logger::getInstance()->setLevel(Logger::INFO);
+ }
+
+ /**
+ * Migrate monitoring navigation items to Icinga DB Web
+ *
+ * USAGE
+ *
+ * icingacli icingadb migrate navigation [options]
+ *
+ * REQUIRED OPTIONS:
+ *
+ * --user=<name> Migrate navigation items whose owner matches the given
+ * name or owners matching the given pattern. Wildcard
+ * matching by `*` possible.
+ *
+ * OPTIONS:
+ *
+ * --override Replace existing or already migrated items
+ * (Attention: Actions are not backed up)
+ *
+ * --no-backup Remove monitoring actions and don't back up menu items
+ */
+ public function navigationAction(): void
+ {
+ /** @var string $user */
+ $user = $this->params->getRequired('user');
+ $noBackup = $this->params->get('no-backup');
+
+ $preferencesPath = Config::resolvePath('preferences');
+ $sharedNavigation = Config::resolvePath('navigation');
+ if (! file_exists($preferencesPath) && ! file_exists($sharedNavigation)) {
+ Logger::info('There are no user navigation items to migrate');
+ return;
+ }
+
+ $rc = 0;
+ $directories = file_exists($preferencesPath) ? new DirectoryIterator($preferencesPath) : [];
+
+ $anythingChanged = false;
+
+ /** @var string $directory */
+ foreach ($directories as $directory) {
+ /** @var string $username */
+ $username = $directories->key() === false ? '' : $directories->key();
+ if (fnmatch($user, $username) === false) {
+ continue;
+ }
+
+ $menuItems = $this->readFromIni($directory . '/menu.ini', $rc);
+ $hostActions = $this->readFromIni($directory . '/host-actions.ini', $rc);
+ $serviceActions = $this->readFromIni($directory . '/service-actions.ini', $rc);
+ $icingadbHostActions = $this->readFromIni($directory . '/icingadb-host-actions.ini', $rc);
+ $icingadbServiceActions = $this->readFromIni($directory . '/icingadb-service-actions.ini', $rc);
+
+ $menuUpdated = false;
+ $originalMenuItems = $this->readFromIni($directory . '/menu.ini', $rc);
+
+ Logger::info(
+ 'Transforming legacy wildcard filters of existing Icinga DB Web items for user "%s"',
+ $username
+ );
+
+ if (! $menuItems->isEmpty()) {
+ $menuUpdated = $this->transformNavigationItems($menuItems, $username, $rc);
+ $anythingChanged |= $menuUpdated;
+ }
+
+ if (! $icingadbHostActions->isEmpty()) {
+ $anythingChanged |= $this->transformNavigationItems($icingadbHostActions, $username, $rc);
+ }
+
+ if (! $icingadbServiceActions->isEmpty()) {
+ $anythingChanged |= $this->transformNavigationItems(
+ $icingadbServiceActions,
+ $username,
+ $rc
+ );
+ }
+
+ if (! $this->skipMigration) {
+ Logger::info('Migrating monitoring navigation items for user "%s" to Icinga DB Web', $username);
+
+ if (! $menuItems->isEmpty()) {
+ $menuUpdated = $this->migrateNavigationItems($menuItems, $username, $directory . '/menu.ini', $rc);
+ $anythingChanged |= $menuUpdated;
+ }
+
+ if (! $hostActions->isEmpty()) {
+ $anythingChanged |= $this->migrateNavigationItems(
+ $hostActions,
+ $username,
+ $directory . '/icingadb-host-actions.ini',
+ $rc
+ );
+ }
+
+ if (! $serviceActions->isEmpty()) {
+ $anythingChanged |= $this->migrateNavigationItems(
+ $serviceActions,
+ $username,
+ $directory . '/icingadb-service-actions.ini',
+ $rc
+ );
+ }
+ }
+
+ if ($menuUpdated && ! $noBackup) {
+ $this->createBackupIni("$directory/menu", $originalMenuItems);
+ }
+ }
+
+ // Start migrating shared navigation items
+ $menuItems = $this->readFromIni($sharedNavigation . '/menu.ini', $rc);
+ $hostActions = $this->readFromIni($sharedNavigation . '/host-actions.ini', $rc);
+ $serviceActions = $this->readFromIni($sharedNavigation . '/service-actions.ini', $rc);
+ $icingadbHostActions = $this->readFromIni($sharedNavigation . '/icingadb-host-actions.ini', $rc);
+ $icingadbServiceActions = $this->readFromIni($sharedNavigation . '/icingadb-service-actions.ini', $rc);
+
+ $menuUpdated = false;
+ $originalMenuItems = $this->readFromIni($sharedNavigation . '/menu.ini', $rc);
+
+ Logger::info('Transforming legacy wildcard filters of existing shared Icinga DB Web navigation items');
+
+ if (! $menuItems->isEmpty()) {
+ $menuUpdated = $this->transformNavigationItems($menuItems, $user, $rc);
+ $anythingChanged |= $menuUpdated;
+ }
+
+ if (! $icingadbHostActions->isEmpty()) {
+ $anythingChanged |= $this->transformNavigationItems($icingadbHostActions, $user, $rc);
+ }
+
+ if (! $icingadbServiceActions->isEmpty()) {
+ $anythingChanged |= $this->transformNavigationItems(
+ $icingadbServiceActions,
+ $user,
+ $rc
+ );
+ }
+
+ if (! $this->skipMigration) {
+ Logger::info('Migrating shared monitoring navigation items to the Icinga DB Web items');
+
+ if (! $menuItems->isEmpty()) {
+ $menuUpdated = $this->migrateNavigationItems($menuItems, $user, $sharedNavigation . '/menu.ini', $rc);
+ $anythingChanged |= $menuUpdated;
+ }
+
+ if (! $hostActions->isEmpty()) {
+ $anythingChanged |= $this->migrateNavigationItems(
+ $hostActions,
+ $user,
+ $sharedNavigation . '/icingadb-host-actions.ini',
+ $rc
+ );
+ }
+
+ if (! $serviceActions->isEmpty()) {
+ $anythingChanged |= $this->migrateNavigationItems(
+ $serviceActions,
+ $user,
+ $sharedNavigation . '/icingadb-service-actions.ini',
+ $rc
+ );
+ }
+ }
+
+ if ($menuUpdated && ! $noBackup) {
+ $this->createBackupIni("$sharedNavigation/menu", $originalMenuItems);
+ }
+
+ if ($rc > 0) {
+ if ($this->skipMigration) {
+ Logger::error('Failed to transform some icingadb navigation items');
+ } else {
+ Logger::error('Failed to migrate some monitoring navigation items');
+ }
+
+ exit($rc);
+ }
+
+ if (! $anythingChanged) {
+ Logger::info('Nothing to do');
+ } elseif ($this->skipMigration) {
+ Logger::info('Successfully transformed all icingadb navigation item filters');
+ } else {
+ Logger::info('Successfully migrated all monitoring navigation items');
+ }
+ }
+
+
+ /**
+ * Migrate monitoring restrictions and permissions to Icinga DB Web
+ *
+ * Migrated roles do not grant general or full access to users afterward.
+ * It is recommended to review any changes made by this command, before
+ * manually granting access.
+ *
+ * USAGE
+ *
+ * icingacli icingadb migrate role [options]
+ *
+ * REQUIRED OPTIONS: (Use either, not both)
+ *
+ * --group=<name> Update roles that are assigned to the given group or to
+ * groups matching the pattern. Wildcard matching by `*`
+ * possible.
+ *
+ * --role=<name> Update role with the given name or roles whose names
+ * match the pattern. Wildcard matching by `*` possible.
+ *
+ * OPTIONS:
+ *
+ * --override Reset any existing Icinga DB Web rules
+ *
+ * --no-backup Don't back up roles
+ */
+ public function roleAction(): void
+ {
+ /** @var ?bool $override */
+ $override = $this->params->get('override');
+ $noBackup = $this->params->get('no-backup');
+
+ /** @var ?string $groupName */
+ $groupName = $this->params->get('group');
+ /** @var ?string $roleName */
+ $roleName = $this->params->get('role');
+
+ if ($roleName === null && $groupName === null) {
+ $this->fail("One of the parameters 'group' or 'role' must be supplied");
+ } elseif ($roleName !== null && $groupName !== null) {
+ $this->fail("Use either 'group' or 'role'. Both cannot be used as role overrules group.");
+ }
+
+ $rc = 0;
+ $changed = false;
+
+ $restrictions = Config::$configDir . '/roles.ini';
+ $rolesConfig = $this->readFromIni($restrictions, $rc);
+ $monitoringRestriction = 'monitoring/filter/objects';
+ $monitoringPropertyBlackList = 'monitoring/blacklist/properties';
+ $icingadbRestrictions = [
+ 'objects' => 'icingadb/filter/objects',
+ 'hosts' => 'icingadb/filter/hosts',
+ 'services' => 'icingadb/filter/services'
+ ];
+
+ $icingadbPropertyDenyList = 'icingadb/denylist/variables';
+ foreach ($rolesConfig as $name => $role) {
+ /** @var string[] $role */
+ $role = iterator_to_array($role);
+
+ if ($roleName === '*' || $groupName === '*') {
+ $roleMatch = true;
+ } elseif ($roleName !== null && fnmatch($roleName, $name)) {
+ $roleMatch = true;
+ } elseif ($groupName !== null && isset($role['groups'])) {
+ $roleGroups = array_map('trim', explode(',', $role['groups']));
+ $roleMatch = false;
+ foreach ($roleGroups as $roleGroup) {
+ if (fnmatch($groupName, $roleGroup)) {
+ $roleMatch = true;
+ break;
+ }
+ }
+ } else {
+ $roleMatch = false;
+ }
+
+ if ($roleMatch && ! $this->skipMigration && $this->shouldUpdateRole($role, $override)) {
+ if (isset($role[$monitoringRestriction])) {
+ Logger::info(
+ 'Migrating monitoring restriction filter for role "%s" to the Icinga DB Web restrictions',
+ $name
+ );
+ $transformedFilter = UrlMigrator::transformFilter(
+ QueryString::parse($role[$monitoringRestriction])
+ );
+
+ if ($transformedFilter) {
+ $role[$icingadbRestrictions['objects']] = QueryString::render($transformedFilter);
+ $changed = true;
+ }
+ }
+
+ if (isset($role[$monitoringPropertyBlackList])) {
+ Logger::info(
+ 'Migrating monitoring blacklisted properties for role "%s" to the Icinga DB Web deny list',
+ $name
+ );
+
+ $icingadbProperties = [];
+ foreach (explode(',', $role[$monitoringPropertyBlackList]) as $property) {
+ $icingadbProperties[] = preg_replace('/^(?:host|service)\.vars\./i', '', $property, 1);
+ }
+
+ $role[$icingadbPropertyDenyList] = str_replace(
+ '**',
+ '*',
+ implode(',', array_unique($icingadbProperties))
+ );
+
+ $changed = true;
+ }
+
+ if (isset($role['permissions'])) {
+ $updatedPermissions = [];
+ Logger::info(
+ 'Migrating monitoring permissions for role "%s" to the Icinga DB Web permissions',
+ $name
+ );
+
+ if (strpos($role['permissions'], 'monitoring')) {
+ $monitoringProtection = Config::module('monitoring')
+ ->get('security', 'protected_customvars');
+
+ if ($monitoringProtection !== null) {
+ $role['icingadb/protect/variables'] = $monitoringProtection;
+ $changed = true;
+ }
+ }
+
+ foreach (explode(',', $role['permissions']) as $permission) {
+ if (Str::startsWith($permission, 'icingadb/') || $permission === 'module/icingadb') {
+ continue;
+ } elseif (Str::startsWith($permission, 'monitoring/command/')) {
+ $changed = true;
+ $updatedPermissions[] = $permission;
+ $updatedPermissions[] = str_replace('monitoring/', 'icingadb/', $permission);
+ } elseif ($permission === 'no-monitoring/contacts') {
+ $changed = true;
+ $updatedPermissions[] = $permission;
+ $role['icingadb/denylist/routes'] = 'users,usergroups';
+ } else {
+ $updatedPermissions[] = $permission;
+ }
+ }
+
+ $role['permissions'] = implode(',', $updatedPermissions);
+ }
+
+ if (isset($role['refusals']) && is_string($role['refusals'])) {
+ $updatedRefusals = [];
+ Logger::info(
+ 'Migrating monitoring refusals for role "%s" to the Icinga DB Web refusals',
+ $name
+ );
+
+ foreach (explode(',', $role['refusals']) as $refusal) {
+ if (Str::startsWith($refusal, 'icingadb/') || $refusal === 'module/icingadb') {
+ continue;
+ } elseif (Str::startsWith($refusal, 'monitoring/command/')) {
+ $changed = true;
+ $updatedRefusals[] = $refusal;
+ $updatedRefusals[] = str_replace('monitoring/', 'icingadb/', $refusal);
+ } else {
+ $updatedRefusals[] = $refusal;
+ }
+ }
+
+ $role['refusals'] = implode(',', $updatedRefusals);
+ }
+ }
+
+ if ($roleMatch) {
+ foreach ($icingadbRestrictions as $object => $icingadbRestriction) {
+ if (isset($role[$icingadbRestriction]) && is_string($role[$icingadbRestriction])) {
+ $filter = QueryString::parse($role[$icingadbRestriction]);
+ $filter = UrlMigrator::transformLegacyWildcardFilter($filter);
+
+ if ($filter) {
+ $filter = QueryString::render($filter);
+ if ($filter !== $role[$icingadbRestriction]) {
+ Logger::info(
+ 'Icinga Db Web restriction of role "%s" for %s changed from "%s" to "%s"',
+ $name,
+ $object,
+ $role[$icingadbRestriction],
+ $filter
+ );
+
+ $role[$icingadbRestriction] = $filter;
+ $changed = true;
+ }
+ }
+ }
+ }
+ }
+
+ $rolesConfig->setSection($name, $role);
+ }
+
+ if ($changed) {
+ if (! $noBackup) {
+ $this->createBackupIni(Config::$configDir . '/roles');
+ }
+
+ try {
+ $rolesConfig->saveIni();
+ } catch (NotWritableError $error) {
+ Logger::error($error);
+ if ($this->skipMigration) {
+ Logger::error('Failed to transform icingadb restrictions');
+ } else {
+ Logger::error('Failed to migrate monitoring restrictions');
+ }
+
+ exit(256);
+ }
+
+ if ($this->skipMigration) {
+ Logger::info('Successfully transformed all icingadb restrictions');
+ } else {
+ Logger::info('Successfully migrated monitoring restrictions and permissions in roles');
+ }
+ } else {
+ Logger::info('Nothing to do');
+ }
+ }
+
+ /**
+ * Migrate monitoring dashboards to Icinga DB Web
+ *
+ * USAGE
+ *
+ * icingacli icingadb migrate dashboard [options]
+ *
+ * REQUIRED OPTIONS:
+ *
+ * --user=<name> Migrate dashboards whose owner matches the given
+ * name or owners matching the given pattern. Wildcard
+ * matching by `*` possible.
+ *
+ * OPTIONS:
+ *
+ * --no-backup Don't back up dashboards
+ */
+ public function dashboardAction(): void
+ {
+ /** @var string $user */
+ $user = $this->params->getRequired('user');
+ $noBackup = $this->params->get('no-backup');
+
+ $dashboardsPath = Config::resolvePath('dashboards');
+ if (! file_exists($dashboardsPath)) {
+ Logger::info('There are no dashboards to migrate');
+ return;
+ }
+
+ $rc = 0;
+ $directories = new DirectoryIterator($dashboardsPath);
+
+ $anythingChanged = false;
+
+ /** @var string $directory */
+ foreach ($directories as $directory) {
+ /** @var string $userName */
+ $userName = $directories->key() === false ? '' : $directories->key();
+ if (fnmatch($user, $userName) === false) {
+ continue;
+ }
+
+ $dashboardsConfig = $this->readFromIni($directory . '/dashboard.ini', $rc);
+ $backupConfig = $this->readFromIni($directory . '/dashboard.ini', $rc);
+
+ Logger::info(
+ 'Migrating monitoring dashboards to Icinga DB Web dashboards for user "%s"',
+ $userName
+ );
+
+ $changed = false;
+ /** @var ConfigObject $dashboardConfig */
+ foreach ($dashboardsConfig->getConfigObject() as $name => $dashboardConfig) {
+ /** @var ?string $dashboardUrlString */
+ $dashboardUrlString = $dashboardConfig->get('url');
+ if ($dashboardUrlString !== null) {
+ $dashBoardUrl = Url::fromPath($dashboardUrlString, [], new Request());
+ if (! $this->skipMigration && Str::startsWith(ltrim($dashboardUrlString, '/'), 'monitoring/')) {
+ $dashboardConfig->url = UrlMigrator::transformUrl($dashBoardUrl)->getRelativeUrl();
+ $changed = true;
+ }
+
+ if (Str::startsWith(ltrim($dashboardUrlString, '/'), 'icingadb/')) {
+ $finalUrl = $dashBoardUrl->onlyWith(['sort', 'limit', 'view', 'columns', 'page']);
+ $params = $dashBoardUrl->without(['sort', 'limit', 'view', 'columns', 'page'])->getParams();
+ $filter = QueryString::parse($params->toString());
+ $filter = UrlMigrator::transformLegacyWildcardFilter($filter);
+ if ($filter) {
+ $oldFilterString = $params->toString();
+ $newFilterString = QueryString::render($filter);
+
+ if ($oldFilterString !== $newFilterString) {
+ Logger::info(
+ 'Icinga Db Web filter of dashboard "%s" has changed from "%s" to "%s"',
+ $name,
+ $params->toString(),
+ QueryString::render($filter)
+ );
+ $finalUrl->setFilter($filter);
+
+ $dashboardConfig->url = $finalUrl->getRelativeUrl();
+ $changed = true;
+ }
+ }
+ }
+ }
+ }
+
+
+ if ($changed && $noBackup === null) {
+ $this->createBackupIni("$directory/dashboard", $backupConfig);
+ }
+
+ if ($changed) {
+ $anythingChanged = true;
+ }
+
+ try {
+ $dashboardsConfig->saveIni();
+ } catch (NotWritableError $error) {
+ Logger::error($error);
+ $rc = 256;
+ }
+ }
+
+ if ($rc > 0) {
+ if ($this->skipMigration) {
+ Logger::error('Failed to transform some icingadb dashboards');
+ } else {
+ Logger::error('Failed to migrate some monitoring dashboards');
+ }
+
+ exit($rc);
+ }
+
+ if (! $anythingChanged) {
+ Logger::info('Nothing to do');
+ } elseif ($this->skipMigration) {
+ Logger::info('Successfully transformed all icingadb dashboards');
+ } else {
+ Logger::info('Successfully migrated dashboards for all the matched users');
+ }
+ }
+
+ /**
+ * Migrate Icinga DB Web wildcard filters of navigation items, dashboards and roles
+ *
+ * USAGE
+ *
+ * icingacli icingadb migrate filter
+ *
+ * OPTIONS:
+ *
+ * --no-backup Don't back up menu items, dashboards and roles
+ */
+ public function filterAction(): void
+ {
+ $this->skipMigration = true;
+
+ $this->params->set('user', '*');
+ $this->navigationAction();
+ $this->dashboardAction();
+
+ $this->params->set('role', '*');
+ $this->roleAction();
+ }
+
+ private function transformNavigationItems(Config $config, string $owner, int &$rc): bool
+ {
+ $updated = false;
+ /** @var ConfigObject $newConfigObject */
+ foreach ($config->getConfigObject() as $section => $newConfigObject) {
+ /** @var string $configOwner */
+ $configOwner = $newConfigObject->get('owner') ?? '';
+ if ($configOwner && $configOwner !== $owner) {
+ continue;
+ }
+
+ if (
+ $newConfigObject->get('type') === 'icingadb-host-action'
+ || $newConfigObject->get('type') === 'icingadb-service-action'
+ ) {
+ /** @var ?string $legacyFilter */
+ $legacyFilter = $newConfigObject->get('filter');
+ if ($legacyFilter !== null) {
+ $filter = QueryString::parse($legacyFilter);
+ $filter = UrlMigrator::transformLegacyWildcardFilter($filter);
+ if ($filter) {
+ $filter = QueryString::render($filter);
+ if ($legacyFilter !== $filter) {
+ $newConfigObject->filter = $filter;
+ $updated = true;
+ Logger::info(
+ 'Icinga DB Web filter of action "%s" is changed from %s to "%s"',
+ $section,
+ $legacyFilter,
+ $filter
+ );
+ }
+ }
+ }
+ }
+
+ /** @var string $url */
+ $url = $newConfigObject->get('url');
+ if ($url && Str::startsWith(ltrim($url, '/'), 'icingadb/')) {
+ $url = Url::fromPath($url, [], new Request());
+ $finalUrl = $url->onlyWith(['sort', 'limit', 'view', 'columns', 'page']);
+ $params = $url->without(['sort', 'limit', 'view', 'columns', 'page'])->getParams();
+ $filter = QueryString::parse($params->toString());
+ $filter = UrlMigrator::transformLegacyWildcardFilter($filter);
+ if ($filter) {
+ $oldFilterString = $params->toString();
+ $newFilterString = QueryString::render($filter);
+
+ if ($oldFilterString !== $newFilterString) {
+ Logger::info(
+ 'Icinga Db Web filter of navigation item "%s" has changed from "%s" to "%s"',
+ $section,
+ $oldFilterString,
+ $newFilterString
+ );
+
+ $newConfigObject->url = $finalUrl->setFilter($filter)->getRelativeUrl();
+ $updated = true;
+ }
+ }
+ }
+ }
+
+ if ($updated) {
+ try {
+ $config->saveIni();
+ } catch (NotWritableError $error) {
+ Logger::error($error);
+ $rc = 256;
+
+ return false;
+ }
+ }
+
+ return $updated;
+ }
+
+ /**
+ * Migrate the given config to the given new config path
+ *
+ * @param Config $config
+ * @param string $owner
+ * @param string $path
+ * @param int $rc
+ *
+ * @return bool
+ */
+ private function migrateNavigationItems(Config $config, string $owner, string $path, int &$rc): bool
+ {
+ $deleteLegacyFiles = $this->params->get('no-backup');
+ $override = $this->params->get('override');
+ $newConfig = $config->getConfigFile() === $path ? $config : $this->readFromIni($path, $rc);
+
+ $updated = false;
+ /** @var ConfigObject $configObject */
+ foreach ($config->getConfigObject() as $configObject) {
+ /** @var string $configOwner */
+ $configOwner = $configObject->get('owner') ?? '';
+ if ($configOwner && $configOwner !== $owner) {
+ continue;
+ }
+
+ $migrateFilter = false;
+ if ($configObject->type === 'host-action') {
+ $updated = true;
+ $migrateFilter = true;
+ $configObject->type = 'icingadb-host-action';
+ } elseif ($configObject->type === 'service-action') {
+ $updated = true;
+ $migrateFilter = true;
+ $configObject->type = 'icingadb-service-action';
+ }
+
+ /** @var ?string $urlString */
+ $urlString = $configObject->get('url');
+ if ($urlString !== null) {
+ $urlString = str_replace(
+ ['$SERVICEDESC$', '$HOSTNAME$', '$HOSTADDRESS$', '$HOSTADDRESS6$'],
+ ['$service.name$', '$host.name$', '$host.address$', '$host.address6$'],
+ $urlString
+ );
+ if ($urlString !== $configObject->url) {
+ $configObject->url = $urlString;
+ $updated = true;
+ }
+
+ $url = Url::fromPath($urlString, [], new Request());
+
+ try {
+ $urlString = UrlMigrator::transformUrl($url)->getRelativeUrl();
+ $configObject->url = $urlString;
+ $updated = true;
+ } catch (\InvalidArgumentException $err) {
+ // Do nothing
+ }
+ }
+
+ /** @var ?string $legacyFilter */
+ $legacyFilter = $configObject->get('filter');
+ if ($migrateFilter && $legacyFilter) {
+ $updated = true;
+ $filter = QueryString::parse($legacyFilter);
+ $filter = UrlMigrator::transformFilter($filter);
+ if ($filter !== false) {
+ $configObject->filter = QueryString::render($filter);
+ } else {
+ unset($configObject->filter);
+ }
+ }
+
+ $section = $config->key();
+ if (! $newConfig->hasSection($section) || $newConfig === $config || $override) {
+ $newConfig->setSection($section, $configObject);
+ }
+ }
+
+ if ($updated) {
+ try {
+ $newConfig->saveIni();
+
+ // Remove the legacy file only if explicitly requested
+ if ($deleteLegacyFiles && $newConfig !== $config) {
+ unlink($config->getConfigFile());
+ }
+ } catch (NotWritableError $error) {
+ Logger::error($error);
+ $rc = 256;
+
+ return false;
+ }
+ }
+
+ return $updated;
+ }
+
+ /**
+ * Get the navigation items config from the given ini path
+ *
+ * @param string $path Absolute path of the ini file
+ * @param int $rc The return code used to exit the action
+ *
+ * @return Config
+ */
+ private function readFromIni($path, &$rc)
+ {
+ try {
+ $config = Config::fromIni($path);
+ } catch (NotReadableError $error) {
+ Logger::error($error);
+
+ $config = new Config();
+ $rc = 128;
+ }
+
+ return $config;
+ }
+
+ private function createBackupIni(string $path, Config $config = null): void
+ {
+ $counter = 0;
+ while (true) {
+ $filepath = $counter > 0
+ ? "$path.backup$counter.ini"
+ : "$path.backup.ini";
+
+ if (! file_exists($filepath)) {
+ if ($config) {
+ $config->saveIni($filepath);
+ } else {
+ copy("$path.ini", $filepath);
+ }
+
+ break;
+ } else {
+ $counter++;
+ }
+ }
+ }
+
+ /**
+ * Checks if the given role should be updated
+ *
+ * @param string[] $role
+ * @param bool $override
+ *
+ * @return bool
+ */
+ private function shouldUpdateRole(array $role, ?bool $override): bool
+ {
+ return ! (
+ isset($role['icingadb/filter/objects'])
+ || isset($role['icingadb/filter/hosts'])
+ || isset($role['icingadb/filter/services'])
+ || isset($role['icingadb/denylist/routes'])
+ || isset($role['icingadb/denylist/variables'])
+ || isset($role['icingadb/protect/variables'])
+ || (isset($role['permissions']) && str_contains($role['permissions'], 'icingadb'))
+ )
+ || $override;
+ }
+}