diff --git a/agents/migration/CMakeLists.txt b/agents/migration/CMakeLists.txt index c3b1528e5..e41641971 100644 --- a/agents/migration/CMakeLists.txt +++ b/agents/migration/CMakeLists.txt @@ -1,41 +1,42 @@ include_directories( ${CMAKE_SOURCE_DIR}/migration ${CMAKE_SOURCE_DIR} ) add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_migration_agent\") kde_enable_exceptions() set(migrationagent_SRCS migrationagent.cpp migrationstatuswidget.cpp migrationexecutor.cpp migrationscheduler.cpp autotests/dummymigrator.cpp ) add_executable(akonadi_migration_agent ${migrationagent_SRCS}) if( APPLE ) set_target_properties(akonadi_migration_agent PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/../Info.plist.template) set_target_properties(akonadi_migration_agent PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.migrationagent") set_target_properties(akonadi_migration_agent PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KDE Akonadi Migrationagent") endif () target_link_libraries(akonadi_migration_agent gidmigration + googlegroupwaremigration KF5::AkonadiCore KF5::AkonadiAgentBase KF5::Contacts KF5::WindowSystem KF5::JobWidgets ) install(TARGETS akonadi_migration_agent ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES migrationagent.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}//akonadi/agents") if(BUILD_TESTING) add_subdirectory(autotests) endif() diff --git a/agents/migration/migrationagent.cpp b/agents/migration/migrationagent.cpp index 2c8e8cd89..15ccbff70 100644 --- a/agents/migration/migrationagent.cpp +++ b/agents/migration/migrationagent.cpp @@ -1,67 +1,70 @@ /* * Copyright 2013 Christian Mollekopf * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "migrationagent.h" #include "migrationstatuswidget.h" #include +#include + #include #include #include #include #include #include #include namespace Akonadi { MigrationAgent::MigrationAgent(const QString &id) : AgentBase(id) , mScheduler(new KUiServerJobTracker) { KLocalizedString::setApplicationDomain("akonadi_migration_agent"); - mScheduler.addMigrator(QSharedPointer(new GidMigrator(KContacts::Addressee::mimeType()))); + mScheduler.addMigrator(QSharedPointer::create(KContacts::Addressee::mimeType())); + mScheduler.addMigrator(QSharedPointer::create()); } void MigrationAgent::configure(WId windowId) { QDialog *dlg = new QDialog(); QVBoxLayout *topLayout = new QVBoxLayout(dlg); MigrationStatusWidget *widget = new MigrationStatusWidget(mScheduler, dlg); topLayout->addWidget(widget); dlg->setAttribute(Qt::WA_DeleteOnClose); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, dlg); connect(buttonBox, &QDialogButtonBox::accepted, dlg, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, dlg, &QDialog::reject); topLayout->addWidget(buttonBox); dlg->setWindowTitle(i18nc("Title of the window that shows the status of the migration agent and offers controls to start/stop individual migration jobs.", "Migration Status")); dlg->resize(600, 300); if (windowId) { dlg->setAttribute(Qt::WA_NativeWindow, true); KWindowSystem::setMainWindow(dlg->windowHandle(), windowId); } dlg->show(); } } AKONADI_AGENT_MAIN(Akonadi::MigrationAgent) diff --git a/migration/CMakeLists.txt b/migration/CMakeLists.txt index a9538c8b6..c083ec5d4 100644 --- a/migration/CMakeLists.txt +++ b/migration/CMakeLists.txt @@ -1,32 +1,33 @@ include_directories( ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ) set(migrationshared_SRCS kmigratorbase.cpp infodialog.cpp entitytreecreatejob.cpp migratorbase.cpp ) ecm_qt_declare_logging_category(migrationshared_SRCS HEADER migration_debug.h IDENTIFIER MIGRATION_LOG CATEGORY_NAME org.kde.pim.migration DESCRIPTION "migration (kdepim-runtime)" EXPORT KDEPIMRUNTIME ) add_library(migrationshared STATIC ${migrationshared_SRCS}) target_link_libraries(migrationshared KF5::AkonadiCore KF5::ConfigCore KF5::I18n Qt5::Widgets ) add_subdirectory(gid) +add_subdirectory(googlegroupware) diff --git a/migration/googlegroupware/CMakeLists.txt b/migration/googlegroupware/CMakeLists.txt new file mode 100644 index 000000000..bf269c9bc --- /dev/null +++ b/migration/googlegroupware/CMakeLists.txt @@ -0,0 +1,29 @@ +include_directories(${CMAKE_BINARY_DIR}/resources/google-groupware) + +set(googlegroupwaremigration_SRCS + googleresourcemigrator.cpp + ${MIGRATION_AKONADI_SHARED_SOURCES} + ) + +kconfig_add_kcfg_files(googlegroupwaremigration_SRCS + ${CMAKE_SOURCE_DIR}/resources/google-groupware/settingsbase.kcfgc +) +kcfg_generate_dbus_interface( + ${CMAKE_SOURCE_DIR}/resources/google-groupware/settingsbase.kcfg + org.kde.Akonadi.Google.Settings +) + +qt5_add_dbus_interface(googlegroupwaremigration_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.Google.Settings.xml + googlesettingsinterface +) + +add_library(googlegroupwaremigration STATIC ${googlegroupwaremigration_SRCS}) +target_link_libraries(googlegroupwaremigration + KF5::AkonadiCore + KF5::ConfigGui + KF5::I18n + KF5::Wallet + KPim::GAPICore # dependency of Google Settings + migrationshared +) diff --git a/migration/googlegroupware/googleresourcemigrator.cpp b/migration/googlegroupware/googleresourcemigrator.cpp new file mode 100644 index 000000000..24c7ab690 --- /dev/null +++ b/migration/googlegroupware/googleresourcemigrator.cpp @@ -0,0 +1,291 @@ +/* + Copyright (c) 2020 Daniel Vrátil + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "googleresourcemigrator.h" +#include "googlesettingsinterface.h" +#include "migration_debug.h" + +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include + +GoogleResourceMigrator::GoogleResourceMigrator() + : MigratorBase(QLatin1String("googleresourcemigrator")) +{} + +QString GoogleResourceMigrator::displayName() const +{ + return i18nc("Name of the Migrator (intended for advanced users).", "Google Resource Migrator"); +} + +QString GoogleResourceMigrator::description() const +{ + return i18nc("Description of the migrator", + "Migrates the old Google Calendar and Google Contacts resources to the new unified Google Groupware Resource"); +} + +bool GoogleResourceMigrator::shouldAutostart() const +{ + return true; +} + +namespace { + +static const QStringView akonadiGoogleCalendarResource = {u"akonadi_googlecalendar_resource"}; +static const QStringView akonadiGoogleContactsResource = {u"akonadi_googlecontacts_resource"}; +static const QStringView akonadiGoogleGroupwareResource = {u"akonadi_google_resource"}; + +bool isLegacyGoogleResource(const Akonadi::AgentInstance &instance) +{ + return instance.type().identifier() == akonadiGoogleCalendarResource + || instance.type().identifier() == akonadiGoogleContactsResource; +} + +bool isGoogleGroupwareResource(const Akonadi::AgentInstance &instance) +{ + return instance.type().identifier() == akonadiGoogleGroupwareResource; +} + +std::unique_ptr settingsForResource(const Akonadi::AgentInstance &instance) +{ + const auto configFile = Akonadi::ServerManager::self()->addNamespace(instance.identifier()) + QStringLiteral("rc"); + const auto configPath = QStandardPaths::locate(QStandardPaths::ConfigLocation, configFile); + return std::unique_ptr{new QSettings{configPath, QSettings::IniFormat}}; +} + +QString getAccountNameFromResourceSettings(const Akonadi::AgentInstance &instance) +{ + const auto config = settingsForResource(instance); + QString account = config->value(QStringLiteral("Account")).toString(); + if (account.isEmpty()) { + account = config->value(QStringLiteral("AccountName")).toString(); + } + + return account; +} + +static const auto WalletFolder = QStringLiteral("Akonadi Google"); + +std::unique_ptr getWallet() +{ + std::unique_ptr wallet{KWallet::Wallet::openWallet(KWallet::Wallet::NetworkWallet(), 0, KWallet::Wallet::Synchronous)}; + if (!wallet) { + qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: failed to open KWallet."; + return {}; + } + + if (!wallet->hasFolder(WalletFolder)) { + qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: couldn't find wallet folder for Google resources."; + return {}; + } + wallet->setFolder(WalletFolder); + + return wallet; +} + +QMap backupKWalletData(const QString &account) +{ + qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: backing up KWallet data for" << account; + + const auto wallet = getWallet(); + if (!wallet) { + return {}; + } + + if (!wallet->entryList().contains(account)) { + qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: couldn't find KWallet data for account" << account; + return {}; + } + + QMap map; + wallet->readMap(account, map); + return map; +} + +void restoreKWalletData(const QString &account, const QMap &data) +{ + qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: restoring KWallet data for" << account; + + auto wallet = getWallet(); + if (!wallet) { + return; + } + + wallet->writeMap(account, data); +} + +} // namespace + +void GoogleResourceMigrator::startWork() +{ + // Discover all existing Google Contacts and Google Calendar resources + const auto allInstances = Akonadi::AgentManager::self()->instances(); + for (const auto &instance : allInstances) { + if (isLegacyGoogleResource(instance)) { + const auto account = getAccountNameFromResourceSettings(instance); + if (account.isEmpty()) { + qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: resource" << instance.identifier() << "is not configued, removing"; + Akonadi::AgentManager::self()->removeInstance(instance); + continue; + } + qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: discovered resource" << instance.identifier() + << "for account" << account; + if (instance.type().identifier() == akonadiGoogleCalendarResource) { + mMigrations[account].calendarResource = instance; + } else if (instance.type().identifier() == akonadiGoogleContactsResource) { + mMigrations[account].contactResource = instance; + } + } else if (isGoogleGroupwareResource(instance)) { + const auto account = getAccountNameFromResourceSettings(instance); + mMigrations[account].alreadyExists = true; + } + } + + mMigrationCount = mMigrations.size(); + migrateNextAccount(); +} + +void GoogleResourceMigrator::removeLegacyInstances(const Instances &instances) +{ + + if (instances.calendarResource.isValid()) { + Akonadi::AgentManager::self()->removeInstance(instances.calendarResource); + qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: removed the legacy calendar resource" << instances.calendarResource.identifier(); + } + if (instances.contactResource.isValid()) { + Akonadi::AgentManager::self()->removeInstance(instances.contactResource); + qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: removed the legacy contacts resource" << instances.contactResource.identifier(); + } +} + +void GoogleResourceMigrator::migrateNextAccount() +{ + setProgress((static_cast(mMigrationsDone) / mMigrationCount) * 100); + if (mMigrations.empty()) { + setMigrationState(MigratorBase::Complete); + return; + } + + QString account; + Instances instances; + std::tie(account, instances) = *mMigrations.constKeyValueBegin(); + mMigrations.erase(mMigrations.begin()); + + if (instances.alreadyExists) { + message(Info, i18n("Google Groupware Resource for account %1 already exists, skipping.", account)); + // Just to be sure, check that there are no left-over legacy instances + removeLegacyInstances(instances); + + ++mMigrationsDone; + QMetaObject::invokeMethod(this, &GoogleResourceMigrator::migrateNextAccount, Qt::QueuedConnection); + return; + } + + qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: starting migration of account" << account; + message(Info, i18n("Starting migration of account %1", account)); + qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: creating new" << akonadiGoogleGroupwareResource; + message(Info, i18n("Creating new instance of Google Gropware Resource")); + auto job = new Akonadi::AgentInstanceCreateJob(akonadiGoogleGroupwareResource.toString(), this); + connect(job, &Akonadi::AgentInstanceCreateJob::finished, + this, [this, job, account, instances](KJob *) { + if (job->error()) { + qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: Failed to create new Google Groupware Resource:" << job->errorString(); + message(Error, i18n("Failed to create a new Google Groupware Resource: %1", job->errorString())); + setMigrationState(MigratorBase::Failed); + return; + } + + const auto newInstance = job->instance(); + if (!migrateAccount(account, instances, newInstance)) { + qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: failed to migrate account" << account; + message(Error, i18n("Failed to migrate account %1", account)); + setMigrationState(MigratorBase::Failed); + return; + } + + // Legacy resources wipe KWallet data when removed, so we need to back the data up + // before removing them and restore it afterwards + const auto kwalletData = backupKWalletData(account); + + removeLegacyInstances(instances); + + restoreKWalletData(account, kwalletData); + + // Reconfigure and restart the new instance + newInstance.reconfigure(); + newInstance.restart(); + + if (instances.calendarResource.isValid() ^ instances.contactResource.isValid()) { + const auto res = instances.calendarResource.isValid() + ? instances.calendarResource.identifier() + : instances.contactResource.identifier(); + qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: migrated configuration from" << res + << "to" << newInstance.identifier(); + } else { + qCInfo(MIGRATION_LOG) << "GoogleResourceMigrator: migrated configuration from" + << instances.calendarResource.identifier() << "and" + << instances.contactResource.identifier() << "to" + << newInstance.identifier(); + } + message(Success, i18n("Migrated account %1 to new Google Groupware Resource", account)); + + ++mMigrationsDone; + migrateNextAccount(); + }); + job->start(); +} + +bool GoogleResourceMigrator::migrateAccount(const QString &account, const Instances &oldInstances, + const Akonadi::AgentInstance &newInstance) +{ + org::kde::Akonadi::Google::Settings resourceSettings{ + Akonadi::ServerManager::self()->agentServiceName(Akonadi::ServerManager::Resource, newInstance.identifier()), + QStringLiteral("/Settings"), QDBusConnection::sessionBus()}; + if (!resourceSettings.isValid()) { + qCWarning(MIGRATION_LOG) << "GoogleResourceMigrator: failed to obtain settings DBus interface of " << newInstance.identifier(); + return false; + } + + resourceSettings.setAccount(account); + + // Calendar-specific + const auto calendarSettings = settingsForResource(oldInstances.calendarResource); + resourceSettings.setCalendars(calendarSettings->value(QStringLiteral("Calendars")).toStringList()); + resourceSettings.setTaskLists(calendarSettings->value(QStringLiteral("TaskLists")).toStringList()); + resourceSettings.setEventsSince(calendarSettings->value(QStringLiteral("EventsSince")).toString()); + resourceSettings.setEnableIntervalCheck(calendarSettings->value(QStringLiteral("EnableIntervalCheck"), false).toBool()); + resourceSettings.setIntervalCheckTime(calendarSettings->value(QStringLiteral("IntervalCheckTime"), 60).toInt()); + + // KAccounts support, should be common in both legacy resources when set + resourceSettings.setAccountName(calendarSettings->value(QStringLiteral("AccountName")).toString()); + resourceSettings.setAccountId(calendarSettings->value(QStringLiteral("AccountId"), 0).toInt()); + + resourceSettings.save(); + + return true; +}; diff --git a/migration/googlegroupware/googleresourcemigrator.h b/migration/googlegroupware/googleresourcemigrator.h new file mode 100644 index 000000000..c9a448d8d --- /dev/null +++ b/migration/googlegroupware/googleresourcemigrator.h @@ -0,0 +1,60 @@ +/* + Copyright (c) 2020 Daniel Vrátil + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef GOOGLERESOURCEMIGRATOR_H +#define GOOGLERESOURCEMIGRATOR_H + +#include +#include + +#include + +#include + +class GoogleResourceMigrator : public MigratorBase +{ + Q_OBJECT +public: + explicit GoogleResourceMigrator(); + + QString displayName() const override; + QString description() const override; + + bool shouldAutostart() const override; + +protected: + void startWork() override; + void migrateNextAccount(); + +private: + struct Instances { + Akonadi::AgentInstance calendarResource; + Akonadi::AgentInstance contactResource; + bool alreadyExists = false; + }; + + bool migrateAccount(const QString &account, const Instances &oldInstances, const Akonadi::AgentInstance &newInstance); + void removeLegacyInstances(const Instances &instances); + + QMap mMigrations; + int mMigrationCount = 0; + int mMigrationsDone = 0; +}; + +#endif