File indexing completed on 2024-12-29 04:48:27

0001 /*
0002     SPDX-FileCopyrightText: 2020 Daniel Vrátil <dvratil@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "googleresourcemigrator.h"
0008 #include "googlesettingsinterface.h"
0009 #include "migration_debug.h"
0010 
0011 #include <Akonadi/AgentInstanceCreateJob>
0012 #include <Akonadi/ServerManager>
0013 
0014 #include <KLocalizedString>
0015 #include <KWallet>
0016 
0017 #include <QDBusConnection>
0018 #include <QDBusServiceWatcher>
0019 #include <QSettings>
0020 
0021 #include <memory>
0022 
0023 GoogleResourceMigrator::GoogleResourceMigrator()
0024     : MigratorBase(QStringLiteral("googleresourcemigrator"))
0025 {
0026 }
0027 
0028 QString GoogleResourceMigrator::displayName() const
0029 {
0030     return i18nc("Name of the Migrator (intended for advanced users).", "Google Resource Migrator");
0031 }
0032 
0033 QString GoogleResourceMigrator::description() const
0034 {
0035     return i18nc("Description of the migrator", "Migrates the old Google Calendar and Google Contacts resources to the new unified Google Groupware Resource");
0036 }
0037 
0038 bool GoogleResourceMigrator::shouldAutostart() const
0039 {
0040     return true;
0041 }
0042 
0043 namespace
0044 {
0045 static const QStringView akonadiGoogleCalendarResource = {u"akonadi_googlecalendar_resource"};
0046 static const QStringView akonadiGoogleContactsResource = {u"akonadi_googlecontacts_resource"};
0047 static const QStringView akonadiGoogleGroupwareResource = {u"akonadi_google_resource"};
0048 
0049 bool isLegacyGoogleResource(const Akonadi::AgentInstance &instance)
0050 {
0051     return instance.type().identifier() == akonadiGoogleCalendarResource || instance.type().identifier() == akonadiGoogleContactsResource;
0052 }
0053 
0054 bool isGoogleGroupwareResource(const Akonadi::AgentInstance &instance)
0055 {
0056     return instance.type().identifier() == akonadiGoogleGroupwareResource;
0057 }
0058 
0059 std::unique_ptr<QSettings> settingsForResource(const Akonadi::AgentInstance &instance)
0060 {
0061     Q_ASSERT(instance.isValid());
0062     if (!instance.isValid()) {
0063         return {};
0064     }
0065 
0066     const QString configFile = Akonadi::ServerManager::self()->addNamespace(instance.identifier()) + QStringLiteral("rc");
0067     const auto configPath = QStandardPaths::locate(QStandardPaths::ConfigLocation, configFile);
0068     return std::make_unique<QSettings>(configPath, QSettings::IniFormat);
0069 }
0070 
0071 QString getAccountNameFromResourceSettings(const Akonadi::AgentInstance &instance)
0072 {
0073     Q_ASSERT(instance.isValid());
0074     if (!instance.isValid()) {
0075         return {};
0076     }
0077 
0078     const auto config = settingsForResource(instance);
0079     QString account = config->value(QStringLiteral("Account")).toString();
0080     if (account.isEmpty()) {
0081         account = config->value(QStringLiteral("AccountName")).toString();
0082     }
0083 
0084     return account;
0085 }
0086 
0087 static const auto WalletFolder = QStringLiteral("Akonadi Google");
0088 
0089 std::unique_ptr<KWallet::Wallet> getWallet()
0090 {
0091     std::unique_ptr<KWallet::Wallet> wallet{KWallet::Wallet::openWallet(KWallet::Wallet::NetworkWallet(), 0, KWallet::Wallet::Synchronous)};
0092     if (!wallet) {
0093         qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: failed to open KWallet.";
0094         return {};
0095     }
0096 
0097     if (!wallet->hasFolder(WalletFolder)) {
0098         qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: couldn't find wallet folder for Google resources.";
0099         return {};
0100     }
0101     wallet->setFolder(WalletFolder);
0102 
0103     return wallet;
0104 }
0105 
0106 QMap<QString, QString> backupKWalletData(const QString &account)
0107 {
0108     qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: backing up KWallet data for" << account;
0109 
0110     const auto wallet = getWallet();
0111     if (!wallet) {
0112         return {};
0113     }
0114 
0115     if (!wallet->entryList().contains(account)) {
0116         qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: couldn't find KWallet data for account" << account;
0117         return {};
0118     }
0119 
0120     QMap<QString, QString> map;
0121     wallet->readMap(account, map);
0122     return map;
0123 }
0124 
0125 void restoreKWalletData(const QString &account, const QMap<QString, QString> &data)
0126 {
0127     qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: restoring KWallet data for" << account;
0128 
0129     auto wallet = getWallet();
0130     if (!wallet) {
0131         return;
0132     }
0133 
0134     wallet->writeMap(account, data);
0135 }
0136 
0137 void removeInstanceAndWait(const Akonadi::AgentInstance &instance)
0138 {
0139     // Make sure we wait for the resource to actually stop - otherwise we are risking
0140     // race when we restore the KWallet secrets from backup before the removed resource
0141     // actually tries to remove them from the wallet.
0142     const QString serviceName = Akonadi::ServerManager::agentServiceName(Akonadi::ServerManager::Resource, instance.identifier());
0143     if (!QDBusConnection::sessionBus().interface()->isServiceRegistered(serviceName)) {
0144         Akonadi::AgentManager::self()->removeInstance(instance);
0145     } else {
0146         QDBusServiceWatcher watcher(Akonadi::ServerManager::agentServiceName(Akonadi::ServerManager::Resource, instance.identifier()),
0147                                     QDBusConnection::sessionBus(),
0148                                     QDBusServiceWatcher::WatchForUnregistration);
0149         QEventLoop loop;
0150         QObject::connect(&watcher, &QDBusServiceWatcher::serviceUnregistered, &loop, [&loop, &instance]() {
0151             qCDebug(MIGRATION_LOG) << "GoogleResourceMigrator: resource" << instance.identifier() << "has disappeared from DBus";
0152             loop.quit();
0153         });
0154         QTimer::singleShot(std::chrono::seconds(20), &loop, [&loop, &instance]() {
0155             qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: timeout while waiting for resource" << instance.identifier() << "to be removed";
0156             loop.quit();
0157         });
0158 
0159         Akonadi::AgentManager::self()->removeInstance(instance);
0160         qCDebug(MIGRATION_LOG) << "GoogleResourceMigrator: waiting for" << instance.identifier() << "to disappear from DBus";
0161         loop.exec();
0162     }
0163 
0164     qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: removed the legacy calendar resource" << instance.identifier();
0165 }
0166 } // namespace
0167 
0168 void GoogleResourceMigrator::startWork()
0169 {
0170     // Discover all existing Google Contacts and Google Calendar resources
0171     const auto allInstances = Akonadi::AgentManager::self()->instances();
0172     for (const auto &instance : allInstances) {
0173         if (isLegacyGoogleResource(instance)) {
0174             const auto account = getAccountNameFromResourceSettings(instance);
0175             if (account.isEmpty()) {
0176                 qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: resource" << instance.identifier() << "is not configured, removing";
0177                 Akonadi::AgentManager::self()->removeInstance(instance);
0178                 continue;
0179             }
0180             qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: discovered resource" << instance.identifier() << "for account" << account;
0181             if (instance.type().identifier() == akonadiGoogleCalendarResource) {
0182                 mMigrations[account].calendarResource = instance;
0183             } else if (instance.type().identifier() == akonadiGoogleContactsResource) {
0184                 mMigrations[account].contactResource = instance;
0185             }
0186         } else if (isGoogleGroupwareResource(instance)) {
0187             const auto account = getAccountNameFromResourceSettings(instance);
0188             mMigrations[account].alreadyExists = true;
0189         }
0190     }
0191 
0192     mMigrationCount = mMigrations.size();
0193     migrateNextAccount();
0194 }
0195 
0196 void GoogleResourceMigrator::removeLegacyInstances(const QString &account, const Instances &instances)
0197 {
0198     // Legacy resources wipe KWallet data when removed, so we need to back the data up
0199     // before removing them and restore it afterwards
0200     const auto kwalletData = backupKWalletData(account);
0201 
0202     if (instances.calendarResource.isValid()) {
0203         removeInstanceAndWait(instances.calendarResource);
0204     }
0205     if (instances.contactResource.isValid()) {
0206         removeInstanceAndWait(instances.contactResource);
0207     }
0208 
0209     restoreKWalletData(account, kwalletData);
0210 }
0211 
0212 void GoogleResourceMigrator::migrateNextAccount()
0213 {
0214     setProgress((static_cast<float>(mMigrationsDone) / mMigrationCount) * 100);
0215     if (mMigrations.empty()) {
0216         setMigrationState(MigratorBase::Complete);
0217         return;
0218     }
0219 
0220     QString account;
0221     Instances instances;
0222     std::tie(account, instances) = *mMigrations.constKeyValueBegin();
0223     mMigrations.erase(mMigrations.begin());
0224 
0225     if (instances.alreadyExists) {
0226         Q_EMIT message(Info, i18n("Google Groupware Resource for account %1 already exists, skipping.", account));
0227         // Just to be sure, check that there are no left-over legacy instances
0228         removeLegacyInstances(account, instances);
0229 
0230         ++mMigrationsDone;
0231         QMetaObject::invokeMethod(this, &GoogleResourceMigrator::migrateNextAccount, Qt::QueuedConnection);
0232         return;
0233     }
0234 
0235     qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: starting migration of account" << account;
0236     Q_EMIT message(Info, i18n("Starting migration of account %1", account));
0237     qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: creating new" << akonadiGoogleGroupwareResource;
0238     Q_EMIT message(Info, i18n("Creating new instance of Google Gropware Resource"));
0239     auto job = new Akonadi::AgentInstanceCreateJob(akonadiGoogleGroupwareResource.toString(), this);
0240     connect(job, &Akonadi::AgentInstanceCreateJob::finished, this, [this, job, account, instances](KJob *) {
0241         if (job->error()) {
0242             qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: Failed to create new Google Groupware Resource:" << job->errorString();
0243             Q_EMIT message(Error, i18n("Failed to create a new Google Groupware Resource: %1", job->errorString()));
0244             setMigrationState(MigratorBase::Failed);
0245             return;
0246         }
0247 
0248         const auto newInstance = job->instance();
0249         if (!migrateAccount(account, instances, newInstance)) {
0250             qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: failed to migrate account" << account;
0251             Q_EMIT message(Error, i18n("Failed to migrate account %1", account));
0252             setMigrationState(MigratorBase::Failed);
0253             return;
0254         }
0255 
0256         removeLegacyInstances(account, instances);
0257 
0258         // Reconfigure and restart the new instance
0259         newInstance.reconfigure();
0260         newInstance.restart();
0261 
0262         if (instances.calendarResource.isValid() ^ instances.contactResource.isValid()) {
0263             const auto res = instances.calendarResource.isValid() ? instances.calendarResource.identifier() : instances.contactResource.identifier();
0264             qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: migrated configuration from" << res << "to" << newInstance.identifier();
0265         } else {
0266             qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: migrated configuration from" << instances.calendarResource.identifier() << "and"
0267                                   << instances.contactResource.identifier() << "to" << newInstance.identifier();
0268         }
0269         Q_EMIT message(Success, i18n("Migrated account %1 to new Google Groupware Resource", account));
0270 
0271         ++mMigrationsDone;
0272         migrateNextAccount();
0273     });
0274     job->start();
0275 }
0276 
0277 QString GoogleResourceMigrator::mergeAccountNames(const ResourceValues<QString> &accountName, const Instances &oldInstances) const
0278 {
0279     if (!accountName.calendar.isEmpty() && !accountName.contacts.isEmpty()) {
0280         if (accountName.calendar == accountName.contacts) {
0281             return accountName.calendar;
0282         } else {
0283             qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: account name mismatch:" << oldInstances.calendarResource.identifier() << "="
0284                                      << accountName.calendar << "," << oldInstances.contactResource.identifier() << "=" << accountName.contacts
0285                                      << ". Ignoring both.";
0286         }
0287     } else if (!accountName.calendar.isEmpty()) {
0288         return accountName.calendar;
0289     } else if (!accountName.contacts.isEmpty()) {
0290         return accountName.contacts;
0291     }
0292 
0293     return {};
0294 }
0295 
0296 int GoogleResourceMigrator::mergeAccountIds(ResourceValues<int> accountId, const Instances &oldInstances) const
0297 {
0298     if (accountId.calendar > 0 && accountId.contacts > 0) {
0299         if (accountId.calendar == accountId.contacts) {
0300             return accountId.calendar;
0301         } else {
0302             qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: account id mismatch:" << oldInstances.calendarResource.identifier() << "="
0303                                      << accountId.calendar << "," << oldInstances.contactResource.identifier() << "=" << accountId.contacts
0304                                      << ". Ignoring both.";
0305         }
0306         return 0;
0307     }
0308 
0309     // Return the non-zero entry
0310     return std::max(accountId.calendar, accountId.contacts);
0311 }
0312 
0313 bool GoogleResourceMigrator::migrateAccount(const QString &account, const Instances &oldInstances, const Akonadi::AgentInstance &newInstance)
0314 {
0315     org::kde::Akonadi::Google::Settings resourceSettings{
0316         Akonadi::ServerManager::self()->agentServiceName(Akonadi::ServerManager::Resource, newInstance.identifier()),
0317         QStringLiteral("/Settings"),
0318         QDBusConnection::sessionBus()};
0319     if (!resourceSettings.isValid()) {
0320         qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: failed to obtain settings DBus interface of " << newInstance.identifier();
0321         return false;
0322     }
0323 
0324     resourceSettings.setAccount(account);
0325 
0326     ResourceValues<QString> accountName;
0327     ResourceValues<int> accountId;
0328     ResourceValues<bool> enableIntervalCheck;
0329     ResourceValues<int> intervalCheck{60, 60};
0330 
0331     if (oldInstances.calendarResource.isValid()) {
0332         const auto calendarSettings = settingsForResource(oldInstances.calendarResource);
0333         // Calendar-specific
0334         resourceSettings.setCalendars(calendarSettings->value(QStringLiteral("Calendars")).toStringList());
0335         resourceSettings.setTaskLists(calendarSettings->value(QStringLiteral("TaskLists")).toStringList());
0336         resourceSettings.setEventsSince(calendarSettings->value(QStringLiteral("EventsSince")).toString());
0337 
0338         enableIntervalCheck.calendar = calendarSettings->value(QStringLiteral("EnableIntervalCheck"), false).toBool();
0339         intervalCheck.calendar = calendarSettings->value(QStringLiteral("IntervalCheckTime"), 60).toInt();
0340 
0341         accountName.calendar = calendarSettings->value(QStringLiteral("AccountName")).toString();
0342         accountId.calendar = calendarSettings->value(QStringLiteral("AccountId"), 0).toInt();
0343     }
0344 
0345     if (oldInstances.contactResource.isValid()) {
0346         const auto contactsSettings = settingsForResource(oldInstances.contactResource);
0347 
0348         enableIntervalCheck.contacts = contactsSettings->value(QStringLiteral("EnableIntervalCheck"), false).toBool();
0349         intervalCheck.contacts = contactsSettings->value(QStringLiteral("IntervalCheckTime"), 60).toInt();
0350 
0351         accountName.contacts = contactsSettings->value(QStringLiteral("AccountName")).toString();
0352         accountId.contacts = contactsSettings->value(QStringLiteral("AccountId"), 0).toInt();
0353     }
0354 
0355     // And now some merging:
0356     resourceSettings.setEnableIntervalCheck(enableIntervalCheck.calendar || enableIntervalCheck.contacts);
0357     resourceSettings.setIntervalCheckTime(std::min(intervalCheck.calendar, intervalCheck.contacts));
0358 
0359     resourceSettings.setAccountName(mergeAccountNames(accountName, oldInstances));
0360     resourceSettings.setAccountId(mergeAccountIds(accountId, oldInstances));
0361 
0362     resourceSettings.save();
0363 
0364     return true;
0365 }
0366 
0367 #include "moc_googleresourcemigrator.cpp"