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"