diff --git a/3rdparty/qwebdavlib/qwebdav.h b/3rdparty/qwebdavlib/qwebdav.h --- a/3rdparty/qwebdavlib/qwebdav.h +++ b/3rdparty/qwebdavlib/qwebdav.h @@ -102,6 +102,9 @@ QNetworkReply* put(const QString& path, const QByteArray& data, const QMap &headers); QNetworkReply* mkdir(const QString& dir ); + // The extended MKCOL used in CardDAV + QNetworkReply* mkdir(const QString& dir, const QByteArray& query); + QNetworkReply* mkcalendar(const QString& dir, const QByteArray& query); QNetworkReply* copy(const QString& pathFrom, const QString& pathTo, bool overwrite = false); QNetworkReply* move(const QString& pathFrom, const QString& pathTo, bool overwrite = false); QNetworkReply* remove(const QString& path ); @@ -126,8 +129,8 @@ void sslErrors(QNetworkReply *reply,const QList &errors); protected: - QNetworkReply* createRequest(const QString& method, QNetworkRequest& req, QIODevice* outgoingData = 0 ); - QNetworkReply* createRequest(const QString& method, QNetworkRequest& req, const QByteArray& outgoingData); + QNetworkReply* createDAVRequest(const QString& method, QNetworkRequest& req, QIODevice* outgoingData = 0 ); + QNetworkReply* createDAVRequest(const QString& method, QNetworkRequest& req, const QByteArray& outgoingData); //! creates the absolute path from m_rootPath and relPath QString absolutePath(const QString &relPath); diff --git a/3rdparty/qwebdavlib/qwebdav.cpp b/3rdparty/qwebdavlib/qwebdav.cpp --- a/3rdparty/qwebdavlib/qwebdav.cpp +++ b/3rdparty/qwebdavlib/qwebdav.cpp @@ -258,15 +258,15 @@ } -QNetworkReply* QWebdav::createRequest(const QString& method, QNetworkRequest& req, QIODevice* outgoingData) +QNetworkReply* QWebdav::createDAVRequest(const QString& method, QNetworkRequest& req, QIODevice* outgoingData) { if(outgoingData != 0 && outgoingData->size() !=0) { req.setHeader(QNetworkRequest::ContentLengthHeader, outgoingData->size()); req.setHeader(QNetworkRequest::ContentTypeHeader, "text/xml; charset=utf-8"); } #ifdef DEBUG_WEBDAV - qDebug() << " QWebdav::createRequest1"; + qDebug() << " QWebdav::createDAVRequest1"; qDebug() << " " << method << " " << req.url().toString(); QList rawHeaderList = req.rawHeaderList(); QByteArray rawHeaderItem; @@ -278,23 +278,23 @@ return sendCustomRequest(req, method.toLatin1(), outgoingData); } -QNetworkReply* QWebdav::createRequest(const QString& method, QNetworkRequest& req, const QByteArray& outgoingData ) +QNetworkReply* QWebdav::createDAVRequest(const QString& method, QNetworkRequest& req, const QByteArray& outgoingData ) { QBuffer* dataIO = new QBuffer; dataIO->setData(outgoingData); dataIO->open(QIODevice::ReadOnly); #ifdef DEBUG_WEBDAV - qDebug() << " QWebdav::createRequest2"; + qDebug() << " QWebdav::createDAVRequest2"; qDebug() << " " << method << " " << req.url().toString(); QList rawHeaderList = req.rawHeaderList(); QByteArray rawHeaderItem; foreach(rawHeaderItem, rawHeaderList) { qDebug() << " " << rawHeaderItem << ": " << req.rawHeader(rawHeaderItem); } #endif - QNetworkReply* reply = createRequest(method, req, dataIO); + QNetworkReply* reply = createDAVRequest(method, req, dataIO); m_outDataDevices.insert(reply, dataIO); return reply; } @@ -355,7 +355,7 @@ req.setUrl(reqUrl); - return this->createRequest("SEARCH", req, query); + return this->createDAVRequest("SEARCH", req, query); } QNetworkReply* QWebdav::get(const QString& path, const QMap &headers) @@ -477,7 +477,7 @@ req.setUrl(reqUrl); req.setRawHeader("Depth", depth == 2 ? QString("infinity").toUtf8() : QString::number(depth).toUtf8()); - return createRequest("PROPFIND", req, query); + return createDAVRequest("PROPFIND", req, query); } QNetworkReply* QWebdav::report(const QString& path, const QByteArray& query, int depth) @@ -490,7 +490,7 @@ req.setUrl(reqUrl); req.setRawHeader("Depth", depth == 2 ? QString("infinity").toUtf8() : QString::number(depth).toUtf8()); - return createRequest("REPORT", req, query); + return createDAVRequest("REPORT", req, query); } QNetworkReply* QWebdav::proppatch(const QString& path, const QWebdav::PropValues& props) @@ -531,7 +531,7 @@ req.setUrl(reqUrl); - return createRequest("PROPPATCH", req, query); + return createDAVRequest("PROPPATCH", req, query); } QNetworkReply* QWebdav::mkdir (const QString& path) @@ -543,7 +543,31 @@ req.setUrl(reqUrl); - return createRequest("MKCOL", req); + return createDAVRequest("MKCOL", req); +} + +QNetworkReply* QWebdav::mkdir (const QString& path, const QByteArray& query) +{ + QNetworkRequest req; + + QUrl reqUrl(m_baseUrl); + reqUrl.setPath(absolutePath(path)); + + req.setUrl(reqUrl); + + return createDAVRequest("MKCOL", req, query); +} + +QNetworkReply* QWebdav::mkcalendar (const QString& path, const QByteArray& query) +{ + QNetworkRequest req; + + QUrl reqUrl(m_baseUrl); + reqUrl.setPath(absolutePath(path)); + + req.setUrl(reqUrl); + + return createDAVRequest("MKCALENDAR", req, query); } QNetworkReply* QWebdav::copy(const QString& pathFrom, const QString& pathTo, bool overwrite) @@ -567,7 +591,7 @@ req.setRawHeader("Depth", "infinity"); req.setRawHeader("Overwrite", overwrite ? "T" : "F"); - return createRequest("COPY", req); + return createDAVRequest("COPY", req); } QNetworkReply* QWebdav::move(const QString& pathFrom, const QString& pathTo, bool overwrite) @@ -591,7 +615,7 @@ req.setRawHeader("Depth", "infinity"); req.setRawHeader("Overwrite", overwrite ? "T" : "F"); - return createRequest("MOVE", req); + return createDAVRequest("MOVE", req); } QNetworkReply* QWebdav::remove(const QString& path) @@ -603,5 +627,5 @@ req.setUrl(reqUrl); - return createRequest("DELETE", req); + return createDAVRequest("DELETE", req); } diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -30,3 +30,15 @@ NAME_PREFIX "kdav2-" LINK_LIBRARIES KPim::KDAV2 Qt5::Test Qt5::Core Qt5::Network ) + +ecm_add_test(davcollectionfetchjobtest.cpp + TEST_NAME davcollectionfetchjob + NAME_PREFIX "kdav2-" + LINK_LIBRARIES KPim::KDAV2 Qt5::Test Qt5::Core Qt5::Network Qt5::Gui +) + +ecm_add_test(davcollectioncreatejobtest.cpp + TEST_NAME davcollectioncreatejob + NAME_PREFIX "kdav2-" + LINK_LIBRARIES KPim::KDAV2 Qt5::Test Qt5::Core Qt5::Network Qt5::Gui +) diff --git a/autotests/davcollectioncreatejobtest.h b/autotests/davcollectioncreatejobtest.h new file mode 100644 --- /dev/null +++ b/autotests/davcollectioncreatejobtest.h @@ -0,0 +1,45 @@ +/* + Copyright (c) 2017 Sandro Knauß + Copyright (c) 2018 Rémi Nicole + + 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. +*/ + +#ifndef DAVCOLLECTIONCREATEJOB_TEST_H +#define DAVCOLLECTIONCREATEJOB_TEST_H + +#include +#include +#include + +class DavCollectionCreateJobTest : public QObject +{ + Q_OBJECT + +private: + QUrl addressbookUrl; + QUrl calendarUrl; + +private Q_SLOTS: + void initTestCase(); + + void runNormalCollectionTest(); + void runAddressbookTest(); + void runCalendarTest(); + + void cleanupTestCase(); +}; + +#endif diff --git a/autotests/davcollectioncreatejobtest.cpp b/autotests/davcollectioncreatejobtest.cpp new file mode 100644 --- /dev/null +++ b/autotests/davcollectioncreatejobtest.cpp @@ -0,0 +1,115 @@ +/* + Copyright (c) 2017 Sandro Knauß + Copyright (c) 2018 Rémi Nicole + + 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 "davcollectioncreatejobtest.h" + +#include +#include + +#include +#include + +void DavCollectionCreateJobTest::initTestCase() +{ + QString addressbookUuid = QUuid::createUuid().toString(); + addressbookUrl = "https://apps.kolabnow.com/addressbooks/test1%40kolab.org/" + addressbookUuid; + addressbookUrl.setUserName("test1@kolab.org"); + addressbookUrl.setPassword("Welcome2KolabSystems"); + + QString calendarUuid = QUuid::createUuid().toString(); + calendarUrl = "https://apps.kolabnow.com/calendars/test1%40kolab.org/" + calendarUuid; + calendarUrl.setUserName("test1@kolab.org"); + calendarUrl.setPassword("Welcome2KolabSystems"); +} + +void DavCollectionCreateJobTest::runNormalCollectionTest() +{ +} + +void DavCollectionCreateJobTest::runAddressbookTest() +{ + KDAV2::DavUrl testCollectionUrl(addressbookUrl, KDAV2::CardDav); + KDAV2::DavCollection testCollection; + + testCollection.setDisplayName("Test AddressBook Collection"); + testCollection.setUrl(testCollectionUrl); + + auto collectionCreateJob = new KDAV2::DavCollectionCreateJob(testCollection); + collectionCreateJob->exec(); + + QCOMPARE(collectionCreateJob->error(), 0); + + KDAV2::DavCollection resultCollection = collectionCreateJob->collection(); + + QVERIFY(!resultCollection.CTag().isEmpty()); + QCOMPARE(resultCollection.displayName(), {"Test AddressBook Collection"}); + + delete collectionCreateJob; +} + +void DavCollectionCreateJobTest::runCalendarTest() +{ + KDAV2::DavUrl testCollectionUrl(calendarUrl, KDAV2::CalDav); + KDAV2::DavCollection testCollection; + + testCollection.setDisplayName("Test Calendar Collection"); + testCollection.setUrl(testCollectionUrl); + testCollection.setContentTypes(KDAV2::DavCollection::Events | KDAV2::DavCollection::Todos); + testCollection.setColor("#123456"); + + auto collectionCreateJob = new KDAV2::DavCollectionCreateJob(testCollection); + collectionCreateJob->exec(); + + QCOMPARE(collectionCreateJob->error(), 0); + + KDAV2::DavCollection resultCollection = collectionCreateJob->collection(); + + QVERIFY(!resultCollection.CTag().isEmpty()); + QCOMPARE(resultCollection.displayName(), {"Test Calendar Collection"}); + QCOMPARE(resultCollection.color().name(), {"#123456"}); + QVERIFY(resultCollection.contentTypes().testFlag(KDAV2::DavCollection::Events)); + QVERIFY(resultCollection.contentTypes().testFlag(KDAV2::DavCollection::Todos)); + + delete collectionCreateJob; +} + +void DavCollectionCreateJobTest::cleanupTestCase() +{ + { + KDAV2::DavUrl testCollectionUrl(addressbookUrl, KDAV2::CardDav); + + auto collectionDeleteJob = new KDAV2::DavCollectionDeleteJob(testCollectionUrl); + collectionDeleteJob->exec(); + QCOMPARE(collectionDeleteJob->error(), 0); + + delete collectionDeleteJob; + } + + { + KDAV2::DavUrl testCollectionUrl(calendarUrl, KDAV2::CalDav); + + auto collectionDeleteJob = new KDAV2::DavCollectionDeleteJob(testCollectionUrl); + collectionDeleteJob->exec(); + QCOMPARE(collectionDeleteJob->error(), 0); + + delete collectionDeleteJob; + } +} + +QTEST_GUILESS_MAIN(DavCollectionCreateJobTest) diff --git a/autotests/davcollectionfetchjobtest.h b/autotests/davcollectionfetchjobtest.h new file mode 100644 --- /dev/null +++ b/autotests/davcollectionfetchjobtest.h @@ -0,0 +1,34 @@ +/* + Copyright (c) 2017 Sandro Knauß + Copyright (c) 2018 Rémi Nicole + + 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. +*/ + +#ifndef DAVCOLLECTIONFETCHJOB_TEST_H +#define DAVCOLLECTIONFETCHJOB_TEST_H + +#include + +class DavCollectionFetchJobTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void runAddressbookTest(); + void runCalendarTest(); +}; + +#endif diff --git a/autotests/davcollectionfetchjobtest.cpp b/autotests/davcollectionfetchjobtest.cpp new file mode 100644 --- /dev/null +++ b/autotests/davcollectionfetchjobtest.cpp @@ -0,0 +1,71 @@ +/* + Copyright (c) 2017 Sandro Knauß + Copyright (c) 2018 Rémi Nicole + + 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 "davcollectionfetchjobtest.h" + +#include + +#include +#include + +void DavCollectionFetchJobTest::runAddressbookTest() +{ + QUrl url(QStringLiteral("https://apps.kolabnow.com/addressbooks/test1%40kolab.org/7511b26d-198c-41b0-8cbf-78923ee1ca8c")); + url.setUserName("test1@kolab.org"); + url.setPassword("Welcome2KolabSystems"); + KDAV2::DavUrl testCollectionUrl(url, KDAV2::CalDav); + KDAV2::DavCollection testCollection; + testCollection.setUrl(testCollectionUrl); + auto collectionFetchJob = new KDAV2::DavCollectionFetchJob(testCollection); + collectionFetchJob->exec(); + + QCOMPARE(collectionFetchJob->error(), 0); + + QCOMPARE(testCollection.CTag(), QString()); + + KDAV2::DavCollection resultCollection = collectionFetchJob->collection(); + + QVERIFY(!resultCollection.CTag().isEmpty()); + QCOMPARE(resultCollection.displayName(), {"Contacts"}); +} + +void DavCollectionFetchJobTest::runCalendarTest() +{ + QUrl url(QStringLiteral("https://apps.kolabnow.com/calendars/test1%40kolab.org/52abfc74-2441-49b5-80ef-f058d0fa4bd0")); + url.setUserName("test1@kolab.org"); + url.setPassword("Welcome2KolabSystems"); + KDAV2::DavUrl testCollectionUrl(url, KDAV2::CalDav); + KDAV2::DavCollection testCollection; + testCollection.setUrl(testCollectionUrl); + auto collectionFetchJob = new KDAV2::DavCollectionFetchJob(testCollection); + collectionFetchJob->exec(); + + QCOMPARE(collectionFetchJob->error(), 0); + + QCOMPARE(testCollection.CTag(), QString()); + QCOMPARE(testCollection.color(), QColor()); + + KDAV2::DavCollection resultCollection = collectionFetchJob->collection(); + + QVERIFY(!resultCollection.CTag().isEmpty()); + QCOMPARE(resultCollection.displayName(), {"Calendar"}); + QCOMPARE(resultCollection.color().name(), {"#cc0000"}); +} + +QTEST_GUILESS_MAIN(DavCollectionFetchJobTest) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,7 +8,9 @@ set(libkdav2_SRCS common/davjobbase.cpp common/davcollection.cpp + common/davcollectioncreatejob.cpp common/davcollectiondeletejob.cpp + common/davcollectionfetchjob.cpp common/davcollectionsfetchjob.cpp common/davcollectionmodifyjob.cpp common/davcollectionsmultifetchjob.cpp @@ -40,7 +42,9 @@ HEADER_NAMES DavJobBase DavCollection + DavCollectionCreateJob DavCollectionDeleteJob + DavCollectionFetchJob DavCollectionsFetchJob DavCollectionModifyJob DavCollectionsMultiFetchJob diff --git a/src/common/davcollectioncreatejob.h b/src/common/davcollectioncreatejob.h new file mode 100644 --- /dev/null +++ b/src/common/davcollectioncreatejob.h @@ -0,0 +1,76 @@ +/* + Copyright (c) 2010 Grégory Oestreicher + Copyright (c) 2018 Rémi Nicole + + 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. +*/ + +#ifndef KDAV2_DAVCOLLECTIONCREATEJOB_H +#define KDAV2_DAVCOLLECTIONCREATEJOB_H + +#include "kpimkdav2_export.h" + +#include "davjobbase.h" +#include "davcollection.h" +#include "davurl.h" + +namespace KDAV2 +{ + +/** + * @short A job that creates a DAV collection. + */ +class KPIMKDAV2_EXPORT DavCollectionCreateJob : public DavJobBase +{ + Q_OBJECT + +public: + /** + * Creates a new dav collection create job. + * + * @param collection The collection that shall be created. + * @param parent The parent object. + */ + DavCollectionCreateJob(const DavCollection &collection, QObject *parent = nullptr); + + /** + * Starts the job. + */ + void start() Q_DECL_OVERRIDE; + + /** + * Returns the created DAV item including the correct identifier url + * and current etag information. + */ + DavCollection collection() const; + + QUrl collectionUrl() const; + +private Q_SLOTS: + void collectionCreated(KJob *); + void collectionModified(KJob *); + void collectionRefreshed(KJob *); + +private: + DavCollection mCollection; + int mRedirectCount; + + void createCalendar(); + void createAddressbook(); +}; + +} + +#endif diff --git a/src/common/davcollectioncreatejob.cpp b/src/common/davcollectioncreatejob.cpp new file mode 100644 --- /dev/null +++ b/src/common/davcollectioncreatejob.cpp @@ -0,0 +1,283 @@ +/* + Copyright (c) 2010 Grégory Oestreicher + Copyright (c) 2018 Rémi Nicole + + 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 "davcollectioncreatejob.h" + +#include "davcollectionfetchjob.h" +#include "davcollectionmodifyjob.h" +#include "daverror.h" +#include "davjob.h" +#include "davmanager.h" + +#include +#include + +using namespace KDAV2; + +DavCollectionCreateJob::DavCollectionCreateJob(const DavCollection &collection, QObject *parent) + : DavJobBase(parent), mCollection(collection), mRedirectCount(0) +{ +} + +void DavCollectionCreateJob::start() +{ + auto protocol = mCollection.url().protocol(); + switch (protocol) { + case CalDav: + // This is a calendar, use the MKCALENDAR request + createCalendar(); + break; + case CardDav: + // This is an addressbook, use the extended MKCOL request + createAddressbook(); + break; + default: { + // This is a normal collection + auto job = DavManager::self()->createMkColJob(collectionUrl()); + connect(job, &DavJob::result, this, &DavCollectionCreateJob::collectionCreated); + } + } +} + +DavCollection DavCollectionCreateJob::collection() const +{ + return mCollection; +} + +QUrl DavCollectionCreateJob::collectionUrl() const +{ + return mCollection.url().url(); +} + +static QUrl assembleUrl(QUrl existingUrl, const QString &location) +{ + if (location.isEmpty()) { + return existingUrl; + } else if (location.startsWith(QLatin1Char('/'))) { + auto url = existingUrl; + url.setPath(location, QUrl::TolerantMode); + return url; + } else { + return QUrl::fromUserInput(location); + } + return {}; +} + +void DavCollectionCreateJob::collectionCreated(KJob *job) +{ + auto *storedJob = qobject_cast(job); + const int responseCode = storedJob->responseCode(); + + if (responseCode == 301 || responseCode == 302 || responseCode == 307 || responseCode == 308) { + if (mRedirectCount > 4) { + setLatestResponseCode(responseCode); + setError(UserDefinedError + responseCode); + emitResult(); + } else { + auto url = assembleUrl(storedJob->url(), storedJob->getLocationHeader()); + QUrl _collectionUrl(url); + _collectionUrl.setUserInfo(collectionUrl().userInfo()); + mCollection.setUrl(DavUrl(_collectionUrl, mCollection.url().protocol())); + + ++mRedirectCount; + start(); + } + + return; + } + + if (storedJob->error()) { + setLatestResponseCode(responseCode); + setError(ERR_COLLECTIONCREATE); + setJobErrorText(storedJob->errorText()); + setJobError(storedJob->error()); + setErrorTextFromDavError(); + + emitResult(); + return; + } + + auto url = assembleUrl(storedJob->url(), storedJob->getLocationHeader()); + url.setUserInfo(collectionUrl().userInfo()); + + DavCollectionModifyJob *modifyJob = + new DavCollectionModifyJob(DavUrl(url, mCollection.url().protocol()), this); + + modifyJob->setProperty(QStringLiteral("displayname"), mCollection.displayName()); + + connect(modifyJob, &DavCollectionFetchJob::result, this, &DavCollectionCreateJob::collectionModified); + modifyJob->start(); +} + +void DavCollectionCreateJob::collectionModified(KJob *job) +{ + if (job->error()) { + setError(ERR_PROBLEM_WITH_REQUEST); + setErrorTextFromDavError(); + emitResult(); + return; + } + + DavCollectionFetchJob *fetchJob = new DavCollectionFetchJob(mCollection, this); + connect(fetchJob, &DavCollectionFetchJob::result, this, &DavCollectionCreateJob::collectionRefreshed); + fetchJob->start(); +} + +void DavCollectionCreateJob::collectionRefreshed(KJob *job) +{ + if (job->error()) { + setError(ERR_PROBLEM_WITH_REQUEST); + setErrorTextFromDavError(); + emitResult(); + return; + } + + DavCollectionFetchJob *fetchJob = qobject_cast(job); + mCollection = fetchJob->collection(); + + emitResult(); +} + +void DavCollectionCreateJob::createCalendar() +{ + // clang-format off + /* Create a query like this: + * + * + * + * + * Test Calendar + * #24b0a3ff + * + * + * + * + * + * + * + * + */ + // clang-format on + + QDomDocument document; + + auto mkcalElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("mkcalendar")); + document.appendChild(mkcalElement); + auto setElement = mkcalElement.appendChild( + document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("set"))); + auto propElement = + setElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"))); + + if (!mCollection.displayName().isEmpty()) { + auto displayNameElement = propElement.appendChild( + document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("displayname"))); + displayNameElement.appendChild(document.createTextNode(mCollection.displayName())); + } + + if (mCollection.color().isValid()) { + auto colorElement = propElement.appendChild(document.createElementNS( + QStringLiteral("http://apple.com/ns/ical/"), QStringLiteral("calendar-color"))); + colorElement.appendChild(document.createTextNode(mCollection.color().name() + "FF")); + } + + auto compSetElement = propElement.appendChild(document.createElementNS( + QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("supported-calendar-component-set"))); + + auto supportedComp = mCollection.contentTypes(); + + if (supportedComp.testFlag(DavCollection::Events)) { + auto compElement = document.createElementNS( + QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("comp")); + compElement.setAttribute(QStringLiteral("name"), QStringLiteral("VEVENT")); + compSetElement.appendChild(compElement); + } + + if (supportedComp.testFlag(DavCollection::Todos)) { + auto compElement = document.createElementNS( + QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("comp")); + compElement.setAttribute(QStringLiteral("name"), QStringLiteral("VTODO")); + compSetElement.appendChild(compElement); + } + + if (supportedComp.testFlag(DavCollection::FreeBusy)) { + auto compElement = document.createElementNS( + QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("comp")); + compElement.setAttribute(QStringLiteral("name"), QStringLiteral("VFREEBUSY")); + compSetElement.appendChild(compElement); + } + + if (supportedComp.testFlag(DavCollection::Journal)) { + auto compElement = document.createElementNS( + QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("comp")); + compElement.setAttribute(QStringLiteral("name"), QStringLiteral("VJOURNAL")); + compSetElement.appendChild(compElement); + } + + auto job = DavManager::self()->createMkCalendarJob(collectionUrl(), document); + // Skip the modification + connect(job, &DavJob::result, this, &DavCollectionCreateJob::collectionModified); +} + +void DavCollectionCreateJob::createAddressbook() +{ + // clang-format off + /* Create a query like this: + * + * + * + * + * + * + * + * + * Lisa's Contacts + * + * + * + */ + // clang-format on + + QDomDocument document; + + auto mkcolElement = document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("mkcol")); + document.appendChild(mkcolElement); + auto setElement = mkcolElement.appendChild( + document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("set"))); + auto propElement = + setElement.appendChild(document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"))); + + auto resourceTypeElement = propElement.appendChild( + document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("resourcetype"))); + + resourceTypeElement.appendChild( + document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("collection"))); + + resourceTypeElement.appendChild(document.createElementNS( + QStringLiteral("urn:ietf:params:xml:ns:carddav"), QStringLiteral("addressbook"))); + + if (!mCollection.displayName().isEmpty()) { + auto displayNameElement = propElement.appendChild( + document.createElementNS(QStringLiteral("DAV:"), QStringLiteral("displayname"))); + displayNameElement.appendChild(document.createTextNode(mCollection.displayName())); + } + + auto job = DavManager::self()->createMkColJob(collectionUrl(), document); + // Skip the modification + connect(job, &DavJob::result, this, &DavCollectionCreateJob::collectionModified); +} diff --git a/src/common/davcollectionfetchjob.h b/src/common/davcollectionfetchjob.h new file mode 100644 --- /dev/null +++ b/src/common/davcollectionfetchjob.h @@ -0,0 +1,67 @@ +/* + Copyright (c) 2010 Tobias Koenig + Copyright (c) 2018 Rémi Nicole + + 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. +*/ + +#ifndef KDAV2_DAVCOLLECTIONFETCHJOB_H +#define KDAV2_DAVCOLLECTIONFETCHJOB_H + +#include "kpimkdav2_export.h" + +#include "davcollection.h" +#include "davjobbase.h" +#include "davurl.h" + +namespace KDAV2 +{ + +/** + * @short A job that fetches a DAV collection from the DAV server. + */ +class KPIMKDAV2_EXPORT DavCollectionFetchJob : public DavJobBase +{ + Q_OBJECT + +public: + /** + * Creates a new dav collection fetch job. + * + * @param item The collection that shall be fetched. + * @param parent The parent object. + */ + DavCollectionFetchJob(const DavCollection &collection, QObject *parent = nullptr); + + /** + * Starts the job. + */ + void start() Q_DECL_OVERRIDE; + + /** + * Returns the fetched collection including current etag information. + */ + DavCollection collection() const; + +private Q_SLOTS: + void davJobFinished(KJob *); + +private: + DavCollection mCollection; +}; + +} + +#endif diff --git a/src/common/davcollectionfetchjob.cpp b/src/common/davcollectionfetchjob.cpp new file mode 100644 --- /dev/null +++ b/src/common/davcollectionfetchjob.cpp @@ -0,0 +1,109 @@ +/* + Copyright (c) 2010 Tobias Koenig + Copyright (c) 2018 Rémi Nicole + + 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 "davcollectionfetchjob.h" + +#include "daverror.h" +#include "davjob.h" +#include "davmanager.h" +#include "davprotocolbase.h" +#include "utils.h" + +using namespace KDAV2; + +DavCollectionFetchJob::DavCollectionFetchJob(const DavCollection &collection, QObject *parent) + : DavJobBase(parent), mCollection(collection) +{ +} + +void DavCollectionFetchJob::start() +{ + const DavProtocolBase *protocol = DavManager::self()->davProtocol(mCollection.url().protocol()); + Q_ASSERT(protocol); + XMLQueryBuilder::Ptr builder(protocol->collectionsQuery()); + + auto job = DavManager::self()->createPropFindJob( + mCollection.url().url(), builder->buildQuery(), /* depth = */ 0); + connect(job, &DavJob::result, this, &DavCollectionFetchJob::davJobFinished); +} + +DavCollection DavCollectionFetchJob::collection() const +{ + return mCollection; +} + +void DavCollectionFetchJob::davJobFinished(KJob *job) +{ + auto *storedJob = qobject_cast(job); + const int responseCode = storedJob->responseCode(); + + if (storedJob->error()) { + setLatestResponseCode(responseCode); + setError(ERR_PROBLEM_WITH_REQUEST); + setJobErrorText(storedJob->errorText()); + setJobError(storedJob->error()); + setErrorTextFromDavError(); + } else { + /* + * Extract data from a document like the following: + * + * + * + * /path/to/collection/ + * + * + * collection name + * + * + * + * + * ctag + * + * HTTP/1.1 200 OK + * + * + * + */ + + const QDomDocument document = storedJob->response(); + + //qWarning() << document.toString(); + + const QDomElement documentElement = document.documentElement(); + QDomElement responseElement = Utils::firstChildElementNS( + documentElement, QStringLiteral("DAV:"), QStringLiteral("response")); + + // Validate that we got a valid PROPFIND response + if (documentElement.localName().compare(QStringLiteral("multistatus"), Qt::CaseInsensitive) != 0) { + setError(ERR_COLLECTIONFETCH); + setErrorTextFromDavError(); + emitResult(); + return; + } + + if (!Utils::extractCollection(responseElement, mCollection.url(), mCollection)) { + setError(ERR_COLLECTIONFETCH); + setErrorTextFromDavError(); + emitResult(); + return; + } + } + + emitResult(); +} diff --git a/src/common/davcollectionsfetchjob.cpp b/src/common/davcollectionsfetchjob.cpp --- a/src/common/davcollectionsfetchjob.cpp +++ b/src/common/davcollectionsfetchjob.cpp @@ -222,48 +222,16 @@ const QDomElement responsesElement = document.documentElement(); - QDomElement responseElement = Utils::firstChildElementNS(responsesElement, QStringLiteral("DAV:"), QStringLiteral("response")); + QDomElement responseElement = Utils::firstChildElementNS( + responsesElement, QStringLiteral("DAV:"), QStringLiteral("response")); while (!responseElement.isNull()) { - QDomElement propstatElement; - - // check for the valid propstat, without giving up on first error - { - const QDomNodeList propstats = responseElement.elementsByTagNameNS(QStringLiteral("DAV:"), QStringLiteral("propstat")); - for (int i = 0; i < propstats.length(); ++i) { - const QDomElement propstatCandidate = propstats.item(i).toElement(); - const QDomElement statusElement = Utils::firstChildElementNS(propstatCandidate, QStringLiteral("DAV:"), QStringLiteral("status")); - if (statusElement.text().contains(QStringLiteral("200"))) { - propstatElement = propstatCandidate; - } - } - } - - if (propstatElement.isNull()) { - responseElement = Utils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response")); - continue; - } - // extract url - const QDomElement hrefElement = Utils::firstChildElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("href")); - if (hrefElement.isNull()) { - responseElement = Utils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response")); + DavCollection collection; + if (!Utils::extractCollection(responseElement, mUrl, collection)) { continue; } - QString href = hrefElement.text(); - if (!href.endsWith(QLatin1Char('/'))) { - href.append(QLatin1Char('/')); - } - - QUrl url = davJob->url(); - url.setUserInfo(QString()); - if (href.startsWith(QLatin1Char('/'))) { - // href is only a path, use request url to complete - url.setPath(href, QUrl::TolerantMode); - } else { - // href is a complete url - url = QUrl::fromUserInput(href); - } + QUrl url = collection.url().url(); // don't add this resource if it has already been detected bool alreadySeen = false; @@ -273,59 +241,16 @@ } } if (alreadySeen) { - responseElement = Utils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response")); + responseElement = Utils::nextSiblingElementNS( + responseElement, QStringLiteral("DAV:"), QStringLiteral("response")); continue; } - // extract display name - const QDomElement propElement = Utils::firstChildElementNS(propstatElement, QStringLiteral("DAV:"), QStringLiteral("prop")); - const QDomElement displaynameElement = Utils::firstChildElementNS(propElement, QStringLiteral("DAV:"), QStringLiteral("displayname")); - const QString displayName = displaynameElement.text(); - - // Extract CTag - const QDomElement CTagElement = Utils::firstChildElementNS(propElement, QStringLiteral("http://calendarserver.org/ns/"), QStringLiteral("getctag")); - QString CTag; - if (!CTagElement.isNull()) { - CTag = CTagElement.text(); - } - - // extract calendar color if provided - const QDomElement colorElement = Utils::firstChildElementNS(propElement, QStringLiteral("http://apple.com/ns/ical/"), QStringLiteral("calendar-color")); - QColor color; - if (!colorElement.isNull()) { - QString colorValue = colorElement.text(); - if (QColor::isValidColor(colorValue)) { - color.setNamedColor(colorValue); - } - } - - // extract allowed content types - const DavCollection::ContentTypes contentTypes = DavManager::self()->davProtocol(mUrl.protocol())->collectionContentTypes(propstatElement); - - auto _url = url; - _url.setUserInfo(mUrl.url().userInfo()); - DavCollection collection(DavUrl(_url, mUrl.protocol()), displayName, contentTypes); - - collection.setCTag(CTag); - if (color.isValid()) { - collection.setColor(color); - } - - // extract privileges - const QDomElement currentPrivsElement = Utils::firstChildElementNS(propElement, QStringLiteral("DAV:"), QStringLiteral("current-user-privilege-set")); - if (currentPrivsElement.isNull()) { - // Assume that we have all privileges - collection.setPrivileges(KDAV2::All); - } else { - Privileges privileges = Utils::extractPrivileges(currentPrivsElement); - collection.setPrivileges(privileges); - } - - qCDebug(KDAV2_LOG) << url.toDisplayString() << "PRIVS: " << collection.privileges(); mCollections << collection; Q_EMIT collectionDiscovered(mUrl.protocol(), url.toDisplayString(), jobUrl); - responseElement = Utils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response")); + responseElement = Utils::nextSiblingElementNS( + responseElement, QStringLiteral("DAV:"), QStringLiteral("response")); } } } diff --git a/src/common/daverror.h b/src/common/daverror.h --- a/src/common/daverror.h +++ b/src/common/daverror.h @@ -40,6 +40,7 @@ ERR_COLLECTIONMODIFY = ERR_PROBLEM_WITH_REQUEST + 30, ERR_COLLECTIONMODIFY_NO_PROPERITES, ERR_COLLECTIONMODIFY_RESPONSE, + ERR_COLLECTIONCREATE = ERR_PROBLEM_WITH_REQUEST + 40, ERR_ITEMCREATE = ERR_PROBLEM_WITH_REQUEST + 100, ERR_ITEMDELETE = ERR_PROBLEM_WITH_REQUEST + 110, ERR_ITEMMODIFY = ERR_PROBLEM_WITH_REQUEST + 120, diff --git a/src/common/daverror.cpp b/src/common/daverror.cpp --- a/src/common/daverror.cpp +++ b/src/common/daverror.cpp @@ -128,6 +128,10 @@ } break; } + case ERR_COLLECTIONCREATE: { + result = QStringLiteral("There was an error when creating the collection"); + break; + } case ERR_ITEMCREATE: { result = QStringLiteral("There was a problem with the request. The item has not been created on the server.\n" "%1 (%2).").arg(err).arg(mResponseCode); diff --git a/src/common/davmanager.h b/src/common/davmanager.h --- a/src/common/davmanager.h +++ b/src/common/davmanager.h @@ -118,6 +118,29 @@ */ DavJob *createPropPatchJob(const QUrl &url, const QDomDocument &document); + /** + * Returns a preconfigured DAV MKCOL job. + * + * @param url The url to MKCOL (may be empty). + */ + DavJob *createMkColJob(const QUrl &url); + + /** + * Returns a preconfigured extended CardDAV MKCOL job. + * + * @param url The url to MKCOL (may be empty). + * @param document The query of the extended MKCOL request + */ + DavJob *createMkColJob(const QUrl &url, const QDomDocument &document); + + /** + * Returns a preconfigured DAV MKCALENDAR job. + * + * @param url The url of the new calendar + * @param document The query of the MKCALENDAR request + */ + DavJob *createMkCalendarJob(const QUrl &url, const QDomDocument &document); + /** * Returns the DAV protocol dialect object for the given DAV @p protocol. */ @@ -130,9 +153,9 @@ /** * Ignore all ssl errors. - * + * * If you want to handle ssl errors yourself via the networkAccessManager, then set to false. - * + * * Enabled by default. */ void setIgnoreSslErrors(bool); diff --git a/src/common/davmanager.cpp b/src/common/davmanager.cpp --- a/src/common/davmanager.cpp +++ b/src/common/davmanager.cpp @@ -119,6 +119,27 @@ return new DavJob{reply, url}; } +DavJob *DavManager::createMkColJob(const QUrl &url) +{ + setConnectionSettings(url); + auto reply = mWebDav->mkdir(url.path()); + return new DavJob{reply, url}; +} + +DavJob *DavManager::createMkColJob(const QUrl &url, const QDomDocument &document) +{ + setConnectionSettings(url); + auto reply = mWebDav->mkdir(url.path(), document.toByteArray()); + return new DavJob{reply, url}; +} + +DavJob *DavManager::createMkCalendarJob(const QUrl &url, const QDomDocument &document) +{ + setConnectionSettings(url); + auto reply = mWebDav->mkcalendar(url.path(), document.toByteArray()); + return new DavJob{reply, url}; +} + const DavProtocolBase *DavManager::davProtocol(Protocol protocol) { if (createProtocol(protocol)) { diff --git a/src/common/utils.h b/src/common/utils.h --- a/src/common/utils.h +++ b/src/common/utils.h @@ -21,6 +21,7 @@ #include "kpimkdav2_export.h" +#include "davcollection.h" #include "enums.h" #include @@ -75,6 +76,13 @@ * Returns the mimetype that shall be used for contact DAV resources using @p protocol. */ QString KPIMKDAV2_EXPORT contactsMimeType(Protocol protocol); + +/** + * Extract a DavCollection from the response element of a PROPFIND result. + * + * @return false if a collection could not be extracted. + */ +bool extractCollection(const QDomElement &response, DavUrl url, DavCollection &collection); } } diff --git a/src/common/utils.cpp b/src/common/utils.cpp --- a/src/common/utils.cpp +++ b/src/common/utils.cpp @@ -23,7 +23,9 @@ #include "davitem.h" #include "davmanager.h" #include "davprotocolbase.h" +#include "davurl.h" +#include #include #include #include @@ -179,3 +181,110 @@ return ret; } + +bool Utils::extractCollection(const QDomElement &response, DavUrl davUrl, DavCollection &collection) +{ + QDomElement propstatElement; + + // check for the valid propstat, without giving up on first error + { + const QDomNodeList propstats = + response.elementsByTagNameNS(QStringLiteral("DAV:"), QStringLiteral("propstat")); + for (int i = 0; i < propstats.length(); ++i) { + const QDomElement propstatCandidate = propstats.item(i).toElement(); + const QDomElement statusElement = Utils::firstChildElementNS( + propstatCandidate, QStringLiteral("DAV:"), QStringLiteral("status")); + if (statusElement.text().contains(QStringLiteral("200"))) { + propstatElement = propstatCandidate; + } + } + } + + if (propstatElement.isNull()) { + return false; + } + + // extract url + const QDomElement hrefElement = + Utils::firstChildElementNS(response, QStringLiteral("DAV:"), QStringLiteral("href")); + + if (hrefElement.isNull()) { + return false; + } + + + QString href = hrefElement.text(); + if (!href.endsWith(QLatin1Char('/'))) { + href.append(QLatin1Char('/')); + } + + QUrl url = davUrl.url(); + url.setUserInfo(QString()); + if (href.startsWith(QLatin1Char('/'))) { + // href is only a path, use request url to complete + url.setPath(href, QUrl::TolerantMode); + } else { + // href is a complete url + url = QUrl::fromUserInput(href); + } + + // extract display name + const QDomElement propElement = + Utils::firstChildElementNS(propstatElement, QStringLiteral("DAV:"), QStringLiteral("prop")); + const QDomElement displaynameElement = + Utils::firstChildElementNS(propElement, QStringLiteral("DAV:"), QStringLiteral("displayname")); + const QString displayName = displaynameElement.text(); + + // Extract CTag + const QDomElement CTagElement = Utils::firstChildElementNS( + propElement, QStringLiteral("http://calendarserver.org/ns/"), QStringLiteral("getctag")); + QString CTag; + if (!CTagElement.isNull()) { + CTag = CTagElement.text(); + } + + // extract calendar color if provided + const QDomElement colorElement = Utils::firstChildElementNS( + propElement, QStringLiteral("http://apple.com/ns/ical/"), QStringLiteral("calendar-color")); + QColor color; + if (!colorElement.isNull()) { + QString colorValue = colorElement.text(); + if(colorValue[0] == '#' && colorValue.size() == 9) { + // Put the alpha part at the beginning for Qt: + // Qt wants #AARRGGBB instead of #RRGGBBAA + colorValue = QStringLiteral("#") + colorValue.right(2) + colorValue.mid(1, 6); + } + + if (QColor::isValidColor(colorValue)) { + color.setNamedColor(colorValue); + } + } + + // extract allowed content types + const DavCollection::ContentTypes contentTypes = + DavManager::self()->davProtocol(davUrl.protocol())->collectionContentTypes(propstatElement); + + auto _url = url; + _url.setUserInfo(davUrl.url().userInfo()); + collection = DavCollection(DavUrl(_url, davUrl.protocol()), displayName, contentTypes); + + collection.setCTag(CTag); + if (color.isValid()) { + collection.setColor(color); + } + + // extract privileges + const QDomElement currentPrivsElement = Utils::firstChildElementNS( + propElement, QStringLiteral("DAV:"), QStringLiteral("current-user-privilege-set")); + if (currentPrivsElement.isNull()) { + // Assume that we have all privileges + collection.setPrivileges(KDAV2::All); + } else { + Privileges privileges = Utils::extractPrivileges(currentPrivsElement); + collection.setPrivileges(privileges); + } + + qCDebug(KDAV2_LOG) << url.toDisplayString() << "PRIVS: " << collection.privileges(); + + return true; +}