From 761328989492db9bd603c2d7f1134d20e485d2f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Nicole?= Date: Tue, 27 Mar 2018 18:26:11 +0200 Subject: Add CalDAV support Summary: Notes: - Add a `webdavcommon` folder for WebDAV generic resource code - Move `davresource` to `carddaveresource` and make it use the WebDAV code - For now it tests the CalDAV resource directly on KolabNow (to be changed) - Only synchronization, not adding / changing / removing WebDAV collections or items (to be implemented) - Only events are currently supported (todo, freebusy, etc. are to be implemented but should be straightforward) Fixes T8224 Reviewers: cmollekopf Tags: #sink Maniphest Tasks: T8224 Differential Revision: https://phabricator.kde.org/D11741 --- examples/webdavcommon/CMakeLists.txt | 8 + examples/webdavcommon/webdav.cpp | 277 +++++++++++++++++++++++++++++++++++ examples/webdavcommon/webdav.h | 78 ++++++++++ 3 files changed, 363 insertions(+) create mode 100644 examples/webdavcommon/CMakeLists.txt create mode 100644 examples/webdavcommon/webdav.cpp create mode 100644 examples/webdavcommon/webdav.h (limited to 'examples/webdavcommon') diff --git a/examples/webdavcommon/CMakeLists.txt b/examples/webdavcommon/CMakeLists.txt new file mode 100644 index 0000000..318756e --- /dev/null +++ b/examples/webdavcommon/CMakeLists.txt @@ -0,0 +1,8 @@ +project(sink_webdav_common) + +set(CMAKE_CXX_STANDARD 14) + +find_package(KPimKDAV2 REQUIRED) + +add_library(${PROJECT_NAME} STATIC webdav.cpp) +target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Network KPim::KDAV2) diff --git a/examples/webdavcommon/webdav.cpp b/examples/webdavcommon/webdav.cpp new file mode 100644 index 0000000..35f7fc2 --- /dev/null +++ b/examples/webdavcommon/webdav.cpp @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2018 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) any later version. + * + * 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, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "webdav.h" + +#include "applicationdomaintype.h" +#include "resourceconfig.h" + +#include +#include +#include +#include + +#include + +static int translateDavError(KJob *job) +{ + using Sink::ApplicationDomain::ErrorCode; + + const int responseCode = dynamic_cast(job)->latestResponseCode(); + + switch (responseCode) { + case QNetworkReply::HostNotFoundError: + return ErrorCode::NoServerError; + // Since we don't login we will just not have the necessary permissions ot view the object + case QNetworkReply::OperationCanceledError: + return ErrorCode::LoginError; + } + return ErrorCode::UnknownError; +} + +static KAsync::Job runJob(QSharedPointer job) +{ + return KAsync::start([job(std::move(job))] { + if (job->exec()) { + SinkTrace() << "Job exec success"; + } else { + SinkTrace() << "Job exec failure"; + } + }); + + // For some reason, this code doesn't work + + /* + return KAsync::start([job](KAsync::Future &future) { + QObject::connect(job, &KJob::result, [&future](KJob *job) { + SinkTrace() << "Job done: " << job->metaObject()->className(); + if (job->error()) { + SinkWarning() + << "Job failed: " << job->errorString() << job->metaObject()->className() + << job->error() << static_cast(job)->latestResponseCode(); + future.setError(translateDavError(job), job->errorString()); + } else { + future.setFinished(); + } + }); + SinkTrace() << "Starting job: " << job->metaObject()->className(); + job->start(); + }); + */ +} + +WebDavSynchronizer::WebDavSynchronizer(const Sink::ResourceContext &context, + KDAV2::Protocol protocol, QByteArray collectionName, QByteArray itemName) + : Sink::Synchronizer(context), + protocol(protocol), + collectionName(std::move(collectionName)), + itemName(std::move(itemName)) +{ + auto config = ResourceConfig::getConfiguration(context.instanceId()); + + server = QUrl::fromUserInput(config.value("server").toString()); + username = config.value("username").toString(); +} + +QList WebDavSynchronizer::getSyncRequests(const Sink::QueryBase &query) +{ + QList list; + if (!query.type().isEmpty()) { + // We want to synchronize something specific + list << Synchronizer::SyncRequest{ query }; + } else { + // We want to synchronize everything + + // Item synchronization does the collections anyway + // list << Synchronizer::SyncRequest{ Sink::QueryBase(collectionName) }; + list << Synchronizer::SyncRequest{ Sink::QueryBase(itemName) }; + } + return list; +} + +KAsync::Job WebDavSynchronizer::synchronizeWithSource(const Sink::QueryBase &query) +{ + if (query.type() != collectionName && query.type() != itemName) { + return KAsync::null(); + } + + SinkLog() << "Synchronizing" << query.type() << "through WebDAV at:" << serverUrl().url(); + + auto collectionsFetchJob = QSharedPointer::create(serverUrl()); + + auto job = runJob(collectionsFetchJob).then([this, collectionsFetchJob](const KAsync::Error &error) { + if (error) { + SinkWarning() << "Failed to synchronize collections:" << collectionsFetchJob->errorString(); + } else { + updateLocalCollections(collectionsFetchJob->collections()); + } + + return collectionsFetchJob->collections(); + }); + + if (query.type() == collectionName) { + // Do nothing more + return job; + } else if (query.type() == itemName) { + auto progress = QSharedPointer::create(0); + auto total = QSharedPointer::create(0); + + // Will contain the resource Id of all collections to be able to scan + // for collections to be removed. + auto collectionResourceIDs = QSharedPointer>::create(); + + // Same but for items. + // Quirk: may contain a collection Id (see below) + auto itemsResourceIDs = QSharedPointer>::create(); + + return job + .serialEach([this, progress(std::move(progress)), total(std::move(total)), collectionResourceIDs, + itemsResourceIDs](const KDAV2::DavCollection &collection) { + auto collectionResourceID = resourceID(collection); + + collectionResourceIDs->insert(collectionResourceID); + + if (unchanged(collection)) { + SinkTrace() << "Collection unchanged:" << collectionResourceID; + + // It seems that doing this prevent the items in the + // collection to be removed when doing scanForRemovals + // below (since the collection is unchanged, we do not go + // through all of its items). + // Behaviour copied from the previous code. + itemsResourceIDs->insert(collectionResourceID); + + return KAsync::null(); + } + + SinkTrace() << "Syncing collection:" << collectionResourceID; + return synchronizeCollection(collection, progress, total, itemsResourceIDs); + }) + .then([this, collectionResourceIDs(std::move(collectionResourceIDs)), + itemsResourceIDs(std::move(itemsResourceIDs))]() { + scanForRemovals(collectionName, [&collectionResourceIDs](const QByteArray &remoteId) { + return collectionResourceIDs->contains(remoteId); + }); + scanForRemovals(itemName, [&itemsResourceIDs](const QByteArray &remoteId) { + return itemsResourceIDs->contains(remoteId); + }); + }); + } else { + SinkWarning() << "Unknown query type"; + return KAsync::null(); + } +} + +KAsync::Job WebDavSynchronizer::synchronizeCollection(const KDAV2::DavCollection &collection, + QSharedPointer progress, QSharedPointer total, + QSharedPointer> itemsResourceIDs) +{ + auto collectionRid = resourceID(collection); + auto ctag = collection.CTag().toLatin1(); + + auto localRid = collectionLocalResourceID(collection); + + // The ETag cache is useless here, since `sinkStore()` IS the cache. + auto cache = std::make_shared(); + auto davItemsListJob = QSharedPointer::create(collection.url(), std::move(cache)); + + return runJob(davItemsListJob) + .then([this, davItemsListJob, total] { + auto items = davItemsListJob->items(); + *total += items.size(); + return KAsync::value(items); + }) + .serialEach([this, collectionRid, localRid, progress(std::move(progress)), total(std::move(total)), + itemsResourceIDs(std::move(itemsResourceIDs))](const KDAV2::DavItem &item) { + auto itemRid = resourceID(item); + + itemsResourceIDs->insert(itemRid); + + if (unchanged(item)) { + SinkTrace() << "Item unchanged:" << itemRid; + return KAsync::null(); + } + + SinkTrace() << "Syncing item:" << itemRid; + return synchronizeItem(item, localRid, progress, total); + }) + .then([this, collectionRid, ctag] { + // Update the local CTag to be able to tell if the collection is unchanged + syncStore().writeValue(collectionRid + "_ctag", ctag); + }); +} + +KAsync::Job WebDavSynchronizer::synchronizeItem(const KDAV2::DavItem &item, + const QByteArray &collectionLocalRid, QSharedPointer progress, QSharedPointer total) +{ + auto etag = item.etag().toLatin1(); + + auto itemFetchJob = QSharedPointer::create(item); + return runJob(itemFetchJob) + .then([this, itemFetchJob(std::move(itemFetchJob)), collectionLocalRid] { + auto item = itemFetchJob->item(); + updateLocalItem(item, collectionLocalRid); + return item; + }) + .then([this, etag, progress(std::move(progress)), total(std::move(total))](const KDAV2::DavItem &item) { + // Update the local ETag to be able to tell if the item is unchanged + syncStore().writeValue(resourceID(item) + "_etag", etag); + + *progress += 1; + reportProgress(*progress, *total); + if ((*progress % 5) == 0) { + commit(); + } + }); +} + +QByteArray WebDavSynchronizer::resourceID(const KDAV2::DavCollection &collection) +{ + return collection.url().toDisplayString().toUtf8(); +} + +QByteArray WebDavSynchronizer::resourceID(const KDAV2::DavItem &item) +{ + return item.url().toDisplayString().toUtf8(); +} + +bool WebDavSynchronizer::unchanged(const KDAV2::DavCollection &collection) +{ + auto ctag = collection.CTag().toLatin1(); + return ctag == syncStore().readValue(resourceID(collection) + "_ctag"); +} + +bool WebDavSynchronizer::unchanged(const KDAV2::DavItem &item) +{ + auto etag = item.etag().toLatin1(); + return etag == syncStore().readValue(resourceID(item) + "_etag"); +} + +KDAV2::DavUrl WebDavSynchronizer::serverUrl() const +{ + if (secret().isEmpty()) { + return {}; + } + + auto result = server; + result.setUserName(username); + result.setPassword(secret()); + + return KDAV2::DavUrl{ result, protocol }; +} diff --git a/examples/webdavcommon/webdav.h b/examples/webdavcommon/webdav.h new file mode 100644 index 0000000..3a4977c --- /dev/null +++ b/examples/webdavcommon/webdav.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2018 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) any later version. + * + * 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, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include "synchronizer.h" + +#include +#include +#include + +class WebDavSynchronizer : public Sink::Synchronizer +{ +public: + WebDavSynchronizer(const Sink::ResourceContext &, KDAV2::Protocol, QByteArray collectionName, + QByteArray itemName); + + QList getSyncRequests(const Sink::QueryBase &query) Q_DECL_OVERRIDE; + KAsync::Job synchronizeWithSource(const Sink::QueryBase &query) Q_DECL_OVERRIDE; + +protected: + /** + * Called with the list of discovered collections. It's purpose should be + * adding the said collections to the store. + */ + virtual void updateLocalCollections(KDAV2::DavCollection::List collections) = 0; + + /** + * Called when discovering a new item, or when an item has been modified. + * It's purpose should be adding the said item to the store. + * + * `collectionLocalRid` is the local resource id of the collection the item + * is in. + */ + virtual void updateLocalItem(KDAV2::DavItem item, const QByteArray &collectionLocalRid) = 0; + + /** + * Get the local resource id from a collection. + */ + virtual QByteArray collectionLocalResourceID(const KDAV2::DavCollection &collection) = 0; + + KAsync::Job synchronizeCollection(const KDAV2::DavCollection &, + QSharedPointer progress, QSharedPointer total, QSharedPointer> itemsResourceIDs); + KAsync::Job synchronizeItem(const KDAV2::DavItem &, const QByteArray &collectionLocalRid, + QSharedPointer progress, QSharedPointer total); + + static QByteArray resourceID(const KDAV2::DavCollection &); + static QByteArray resourceID(const KDAV2::DavItem &); + + bool unchanged(const KDAV2::DavCollection &); + bool unchanged(const KDAV2::DavItem &); + + KDAV2::DavUrl serverUrl() const; + +private: + KDAV2::Protocol protocol; + const QByteArray collectionName; + const QByteArray itemName; + + QUrl server; + QString username; +}; -- cgit v1.2.3