diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index e839c3e..d2d7710 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -1,32 +1,38 @@ set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}) add_definitions(-DAUTOTEST_DATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data") ecm_add_test(davcollectiontest.cpp TEST_NAME davcollection NAME_PREFIX "kdav-" LINK_LIBRARIES KF5::DAV Qt5::Test Qt5::Core Qt5::Gui ) ecm_add_test(davitemtest.cpp TEST_NAME davitem NAME_PREFIX "kdav-" LINK_LIBRARIES KF5::DAV Qt5::Test Qt5::Core ) ecm_add_test(davurltest.cpp TEST_NAME davurl NAME_PREFIX "kdav-" LINK_LIBRARIES KF5::DAV Qt5::Test Qt5::Core ) +ecm_add_test(davcollectionsmultifetchjobtest.cpp fakeserver.cpp + TEST_NAME davcollectionsmultifetchjobtest + NAME_PREFIX "kdav-" + LINK_LIBRARIES KF5::DAV Qt5::Test Qt5::Core Qt5::Network +) + ecm_add_test(davitemfetchjobtest.cpp fakeserver.cpp TEST_NAME davitemfetchjob NAME_PREFIX "kdav-" LINK_LIBRARIES KF5::DAV Qt5::Test Qt5::Core Qt5::Network ) ecm_add_test(davitemslistjobtest.cpp fakeserver.cpp TEST_NAME davitemslistjob NAME_PREFIX "kdav-" LINK_LIBRARIES KF5::DAV Qt5::Test Qt5::Core Qt5::Network ) diff --git a/autotests/data/dataitemmultifetchjob-caldav-collections.txt b/autotests/data/dataitemmultifetchjob-caldav-collections.txt new file mode 100644 index 0000000..5d29dbc --- /dev/null +++ b/autotests/data/dataitemmultifetchjob-caldav-collections.txt @@ -0,0 +1,37 @@ +C: PROPFIND /caldav/dfaure%40example.com/ HTTP/1.1 +S: HTTP/1.1 207 Multi-Status +S: Date: Wed, 04 Jan 2017 18:26:48 GMT +S: Last-Modified: Wed, 04 Jan 2017 18:26:47 GMT +S: DAV: 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, calendar-access, calendar-proxy, calendar-schedule, calendar-auto-schedule, addressbook, 2 +S: Content-Type: application/xml; charset=utf-8 +D: +D: +D: +D: /caldav.php/test1.user/home/ +D: +D: +D: +D: +D: +D: +D: +D: +D: +D: +D: +D: +D: +D: +D: Test1 User +D: +D: +D: +D: +D: +D: 12345 +D: +D: HTTP/1.1 200 OK +D: +D: +D: +X diff --git a/autotests/data/dataitemmultifetchjob-caldav.txt b/autotests/data/dataitemmultifetchjob-caldav.txt new file mode 100644 index 0000000..e876f57 --- /dev/null +++ b/autotests/data/dataitemmultifetchjob-caldav.txt @@ -0,0 +1,21 @@ +C: PROPFIND /caldav HTTP/1.1 +S: HTTP/1.1 207 Multi-Status +S: Date: Wed, 04 Jan 2017 18:26:48 GMT +S: Last-Modified: Wed, 04 Jan 2017 18:26:47 GMT +S: DAV: 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, calendar-access, calendar-proxy, calendar-schedule, calendar-auto-schedule, addressbook, 2 +S: Content-Type: application/xml; charset=utf-8 +D: +D: +D: +D: /principals/users/dfaure%40example.com/ +D: +D: +D: +D: /caldav/dfaure%40example.com/ +D: +D: +D: HTTP/1.1 200 OK +D: +D: +D: +X diff --git a/autotests/data/dataitemmultifetchjob-carddav-collections.txt b/autotests/data/dataitemmultifetchjob-carddav-collections.txt new file mode 100644 index 0000000..e63496d --- /dev/null +++ b/autotests/data/dataitemmultifetchjob-carddav-collections.txt @@ -0,0 +1,24 @@ +C: PROPFIND /carddav/dfaure%40example.com/ HTTP/1.1 +S: HTTP/1.1 207 Multi-Status +S: Date: Wed, 04 Jan 2017 18:26:48 GMT +S: Last-Modified: Wed, 04 Jan 2017 18:26:47 GMT +S: DAV: 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, calendar-access, calendar-proxy, calendar-schedule, calendar-auto-schedule, addressbook, 2 +S: Content-Type: application/xml; charset=utf-8 +D: +D: +D: +D: /carddav.php/test1.user/home/ +D: +D: +D: +D: +D: +D: +D: My Address Book +D: 3145 +D: +D: HTTP/1.1 200 OK +D: +D: +D: +X diff --git a/autotests/data/dataitemmultifetchjob-carddav.txt b/autotests/data/dataitemmultifetchjob-carddav.txt new file mode 100644 index 0000000..6a389b7 --- /dev/null +++ b/autotests/data/dataitemmultifetchjob-carddav.txt @@ -0,0 +1,21 @@ +C: PROPFIND /carddav HTTP/1.1 +S: HTTP/1.1 207 Multi-Status +S: Date: Wed, 04 Jan 2017 18:26:48 GMT +S: Last-Modified: Wed, 04 Jan 2017 18:26:47 GMT +S: DAV: 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, calendar-access, calendar-proxy, calendar-schedule, calendar-auto-schedule, addressbook, 2 +S: Content-Type: application/xml; charset=utf-8 +D: +D: +D: +D: /principals/users/dfaure%40example.com/ +D: +D: +D: +D: /carddav/dfaure%40example.com/ +D: +D: +D: HTTP/1.1 200 OK +D: +D: +D: +X diff --git a/autotests/davcollectionsmultifetchjobtest.cpp b/autotests/davcollectionsmultifetchjobtest.cpp new file mode 100644 index 0000000..1eaec3f --- /dev/null +++ b/autotests/davcollectionsmultifetchjobtest.cpp @@ -0,0 +1,66 @@ +/* + SPDX-FileCopyrightText: 2020 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "davcollectionsmultifetchjobtest.h" +#include "fakeserver.h" + +#include + +#include + +using KDAV::DavCollection; + +void DavCollectionsMultiFetchJobTest::initTestCase() +{ + // To avoid a runtime dependency on klauncher + qputenv("KDE_FORK_SLAVES", "yes"); + // To let ctest exit, we shouldn't start kio_http_cache_cleaner + qputenv("KIO_DISABLE_CACHE_CLEANER", "yes"); +} + +void DavCollectionsMultiFetchJobTest::runSuccessfullTest() +{ + FakeServer fakeServer; + QUrl url(QStringLiteral("http://localhost/caldav")); + url.setPort(fakeServer.port()); + KDAV::DavUrl davUrl1(url, KDAV::CalDav); + QUrl url2(url); + url2.setPath(QStringLiteral("/carddav")); + KDAV::DavUrl davUrl2(url2, KDAV::CardDav); + + auto job = new KDAV::DavCollectionsMultiFetchJob({davUrl1, davUrl2}); + // TODO QSignalSpy spy(job, &KDAV::DavCollectionsMultiFetchJob::collectionDiscovered); + + fakeServer.addScenarioFromFile(QLatin1String(AUTOTEST_DATA_DIR)+QStringLiteral("/dataitemmultifetchjob-caldav.txt")); + fakeServer.addScenarioFromFile(QLatin1String(AUTOTEST_DATA_DIR)+QStringLiteral("/dataitemmultifetchjob-caldav-collections.txt")); + fakeServer.addScenarioFromFile(QLatin1String(AUTOTEST_DATA_DIR)+QStringLiteral("/dataitemmultifetchjob-carddav.txt")); + fakeServer.addScenarioFromFile(QLatin1String(AUTOTEST_DATA_DIR)+QStringLiteral("/dataitemmultifetchjob-carddav-collections.txt")); + fakeServer.startAndWait(); + job->exec(); + fakeServer.quit(); + + QVERIFY(fakeServer.isAllScenarioDone()); + QCOMPARE(job->error(), 0); + + const KDAV::DavCollection::List collections = job->collections(); + QCOMPARE(collections.count(), 2); + + const KDAV::DavCollection calendar = collections.at(0); + QCOMPARE(calendar.displayName(), QStringLiteral("Test1 User")); + QCOMPARE(calendar.contentTypes(), DavCollection::Events | DavCollection::Todos | DavCollection::FreeBusy | DavCollection::Journal); + QCOMPARE(calendar.url().url().path(), QStringLiteral("/caldav.php/test1.user/home/")); + QCOMPARE(calendar.CTag(), QStringLiteral("12345")); + QCOMPARE(calendar.privileges(), KDAV::Read); + + const KDAV::DavCollection addressbook = collections.at(1); + QCOMPARE(addressbook.displayName(), QStringLiteral("My Address Book")); + QCOMPARE(addressbook.contentTypes(), DavCollection::Contacts); + QCOMPARE(addressbook.url().url().path(), QStringLiteral("/carddav.php/test1.user/home/")); + QCOMPARE(addressbook.CTag(), QStringLiteral("3145")); + QCOMPARE(addressbook.privileges(), KDAV::All); +} + +QTEST_MAIN(DavCollectionsMultiFetchJobTest) diff --git a/autotests/davcollectionsmultifetchjobtest.h b/autotests/davcollectionsmultifetchjobtest.h new file mode 100644 index 0000000..a165c86 --- /dev/null +++ b/autotests/davcollectionsmultifetchjobtest.h @@ -0,0 +1,21 @@ +/* + SPDX-FileCopyrightText: 2020 David Faure + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef DAVITEMFETCHJOB_TEST_H +#define DAVITEMFETCHJOB_TEST_H + +#include + +class DavCollectionsMultiFetchJobTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void runSuccessfullTest(); +}; + +#endif diff --git a/autotests/fakeserver.cpp b/autotests/fakeserver.cpp index 5073b1d..f48b29c 100644 --- a/autotests/fakeserver.cpp +++ b/autotests/fakeserver.cpp @@ -1,209 +1,220 @@ /* SPDX-FileCopyrightText: 2008 Omat Holding B .V. SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB , a KDAB Group company SPDX-FileContributor: Kevin Ottens SPDX-FileCopyrightText: 2017 Sandro Kanuß SPDX-License-Identifier: GPL-2.0-or-later */ // Own #include "fakeserver.h" // Qt #include #include #include #include FakeServer::FakeServer(QObject *parent) : QThread(parent) , m_port(5989) { moveToThread(this); } FakeServer::~FakeServer() { quit(); wait(); } void FakeServer::startAndWait() { start(); // this will block until the event queue starts QMetaObject::invokeMethod(this, &FakeServer::started, Qt::BlockingQueuedConnection); } void FakeServer::dataAvailable() { QMutexLocker locker(&m_mutex); QTcpSocket *socket = qobject_cast(sender()); Q_ASSERT(socket != nullptr); int scenarioNumber = m_clientSockets.indexOf(socket); + if (scenarioNumber >= m_scenarios.size()) { + qWarning() << "There is no scenario for socket" << scenarioNumber << ", we got more connections than expected"; + } + readClientPart(scenarioNumber); writeServerPart(scenarioNumber); } void FakeServer::newConnection() { QMutexLocker locker(&m_mutex); m_clientSockets << m_tcpServer->nextPendingConnection(); connect(m_clientSockets.last(), &QTcpSocket::readyRead, this, &FakeServer::dataAvailable); } void FakeServer::run() { m_tcpServer = new QTcpServer(); if (!m_tcpServer->listen(QHostAddress(QHostAddress::LocalHost), m_port)) { qFatal("Unable to start the server"); } connect(m_tcpServer, &QTcpServer::newConnection, this, &FakeServer::newConnection); exec(); qDeleteAll(m_clientSockets); delete m_tcpServer; } void FakeServer::started() { // do nothing: this is a dummy slot used by startAndWait() } void FakeServer::setScenario(const QList &scenario) { QMutexLocker locker(&m_mutex); m_scenarios.clear(); m_scenarios << scenario; } void FakeServer::addScenario(const QList &scenario) { QMutexLocker locker(&m_mutex); m_scenarios << scenario; } void FakeServer::addScenarioFromFile(const QString &fileName) { QFile file(fileName); file.open(QFile::ReadOnly); QList scenario; while (!file.atEnd()) { scenario << file.readLine().trimmed(); } file.close(); addScenario(scenario); } bool FakeServer::isScenarioDone(int scenarioNumber) const { QMutexLocker locker(&m_mutex); if (scenarioNumber < m_scenarios.size()) { return m_scenarios[scenarioNumber].isEmpty(); } else { return true; // Non existent hence empty, right? } } bool FakeServer::isAllScenarioDone() const { QMutexLocker locker(&m_mutex); for (const QList &scenario : qAsConst(m_scenarios)) { if (!scenario.isEmpty()) { return false; } } return true; } void FakeServer::writeServerPart(int scenarioNumber) { QList scenario = m_scenarios[scenarioNumber]; QTcpSocket *clientSocket = m_clientSockets[scenarioNumber]; while (!scenario.isEmpty() && scenario.first().startsWith("S: ")) { QByteArray rule = scenario.takeFirst(); QByteArray payload = rule.mid(3); clientSocket->write(payload + "\r\n"); } QByteArray data; while (!scenario.isEmpty() && scenario.first().startsWith("D: ")) { QByteArray rule = scenario.takeFirst(); QByteArray payload = rule.mid(3); data.append(payload + "\r\n"); } clientSocket->write(QStringLiteral("Content-Length: %1\r\n\r\n").arg(data.length()).toLatin1()); clientSocket->write(data); if (!scenario.isEmpty() && scenario.first().startsWith("X")) { scenario.takeFirst(); clientSocket->close(); } if (!scenario.isEmpty()) { QVERIFY(scenario.first().startsWith("C: ")); } m_scenarios[scenarioNumber] = scenario; } void FakeServer::readClientPart(int scenarioNumber) { QList scenario = m_scenarios[scenarioNumber]; QTcpSocket *socket = m_clientSockets[scenarioNumber]; QByteArray line = socket->readLine(); + qDebug() << "Read client request" << line; QVector header; - while(line != "\r\n") { + while (line != "\r\n") { header << line; + if (socket->bytesAvailable() == 0 && !socket->waitForReadyRead()) { + qDebug() << header; + QFAIL("could not read all headers"); + return; + } line = socket->readLine(); } while (!scenario.isEmpty() && scenario.first().startsWith("C: ")) { QByteArray expected = scenario.takeFirst().mid(3) + "\r\n"; if (!header.contains(expected)) { - qWarning() << expected << "not found in header"; + qWarning() << expected << "not found in header. Here's what we got:"; + qWarning() << header; QVERIFY(false); break; } } if (!scenario.isEmpty()) { QVERIFY(scenario.first().startsWith("S: ")); } m_scenarios[scenarioNumber] = scenario; } int FakeServer::port() const { return m_port; } diff --git a/src/common/davcollectionsfetchjob.cpp b/src/common/davcollectionsfetchjob.cpp index 59cad29..aff2258 100644 --- a/src/common/davcollectionsfetchjob.cpp +++ b/src/common/davcollectionsfetchjob.cpp @@ -1,362 +1,362 @@ /* SPDX-FileCopyrightText: 2010 Tobias Koenig SPDX-License-Identifier: LGPL-2.0-or-later */ #include "davcollectionsfetchjob.h" #include "davjobbase_p.h" #include "davmanager_p.h" #include "davprincipalhomesetsfetchjob.h" #include "davprotocolbase_p.h" #include "utils_p.h" #include "daverror.h" #include "libkdav_debug.h" #include #include #include #include #include using namespace KDAV; namespace KDAV { class DavCollectionsFetchJobPrivate : public DavJobBasePrivate { public: DavUrl mUrl; DavCollection::List mCollections; uint mSubJobCount = 0; }; } DavCollectionsFetchJob::DavCollectionsFetchJob(const DavUrl &url, QObject *parent) : DavJobBase(new DavCollectionsFetchJobPrivate, parent) { Q_D(DavCollectionsFetchJob); d->mUrl = url; } void DavCollectionsFetchJob::start() { Q_D(DavCollectionsFetchJob); if (DavManager::davProtocol(d->mUrl.protocol())->supportsPrincipals()) { DavPrincipalHomeSetsFetchJob *job = new DavPrincipalHomeSetsFetchJob(d->mUrl); connect(job, &DavPrincipalHomeSetsFetchJob::result, this, &DavCollectionsFetchJob::principalFetchFinished); job->start(); } else { doCollectionsFetch(d->mUrl.url()); } } DavCollection::List DavCollectionsFetchJob::collections() const { Q_D(const DavCollectionsFetchJob); return d->mCollections; } DavUrl DavCollectionsFetchJob::davUrl() const { Q_D(const DavCollectionsFetchJob); return d->mUrl; } void DavCollectionsFetchJob::doCollectionsFetch(const QUrl &url) { Q_D(DavCollectionsFetchJob); ++d->mSubJobCount; const QDomDocument collectionQuery = DavManager::davProtocol(d->mUrl.protocol())->collectionsQuery()->buildQuery(); KIO::DavJob *job = DavManager::self()->createPropFindJob(url, collectionQuery); connect(job, &KIO::DavJob::result, this, &DavCollectionsFetchJob::collectionsFetchFinished); job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true")); } void DavCollectionsFetchJob::principalFetchFinished(KJob *job) { Q_D(DavCollectionsFetchJob); const DavPrincipalHomeSetsFetchJob *davJob = qobject_cast(job); if (davJob->error()) { if (davJob->latestResponseCode()) { // If we have a HTTP response code then this may mean that // the URL was not a principal URL. Retry as if it were a calendar URL. qCDebug(KDAV_LOG) << job->errorText(); doCollectionsFetch(d->mUrl.url()); } else { // Just give up here. setDavError(davJob->davError()); setErrorTextFromDavError(); emitResult(); } return; } const QStringList homeSets = davJob->homeSets(); - qCDebug(KDAV_LOG) << "Found " << homeSets.size() << " homesets"; + qCDebug(KDAV_LOG) << "Found" << homeSets.size() << "homesets"; qCDebug(KDAV_LOG) << homeSets; if (homeSets.isEmpty()) { // Same as above, retry as if it were a calendar URL. doCollectionsFetch(d->mUrl.url()); return; } for (const QString &homeSet : homeSets) { QUrl url = d->mUrl.url(); if (homeSet.startsWith(QLatin1Char('/'))) { // homeSet is only a path, use request url to complete url.setPath(homeSet, QUrl::TolerantMode); } else { // homeSet is a complete url QUrl tmpUrl(homeSet); tmpUrl.setUserName(url.userName()); tmpUrl.setPassword(url.password()); url = tmpUrl; } doCollectionsFetch(url); } } void DavCollectionsFetchJob::collectionsFetchFinished(KJob *job) { Q_D(DavCollectionsFetchJob); KIO::DavJob *davJob = qobject_cast(job); const QString responseCodeStr = davJob->queryMetaData(QStringLiteral("responsecode")); const int responseCode = responseCodeStr.isEmpty() ? 0 : responseCodeStr.toInt(); // KIO::DavJob does not set error() even if the HTTP status code is a 4xx or a 5xx if (davJob->error() || (responseCode >= 400 && responseCode < 600)) { if (davJob->url() != d->mUrl.url()) { // Retry as if the initial URL was a calendar URL. // We can end up here when retrieving a homeset on // which a PROPFIND resulted in an error doCollectionsFetch(d->mUrl.url()); --d->mSubJobCount; return; } setLatestResponseCode(responseCode); setError(ERR_PROBLEM_WITH_REQUEST); setJobErrorText(davJob->errorText()); setJobError(davJob->error()); setErrorTextFromDavError(); } else { // For use in the collectionDiscovered() signal QUrl _jobUrl = d->mUrl.url(); _jobUrl.setUserInfo(QString()); const QString jobUrl = _jobUrl.toDisplayString(); // Validate that we got a valid PROPFIND response QDomElement rootElement = davJob->response().documentElement(); if (rootElement.tagName().compare(QLatin1String("multistatus"), Qt::CaseInsensitive) != 0) { setError(ERR_COLLECTIONFETCH); setErrorTextFromDavError(); subjobFinished(); return; } QByteArray resp(davJob->response().toByteArray()); QBuffer buffer(&resp); buffer.open(QIODevice::ReadOnly); QXmlQuery xquery; if (!xquery.setFocus(&buffer)) { setError(ERR_COLLECTIONFETCH_XQUERY_SETFOCUS); setErrorTextFromDavError(); subjobFinished(); return; } xquery.setQuery(DavManager::davProtocol(d->mUrl.protocol())->collectionsXQuery()); if (!xquery.isValid()) { setError(ERR_COLLECTIONFETCH_XQUERY_INVALID); setErrorTextFromDavError(); subjobFinished(); return; } QString responsesStr; xquery.evaluateTo(&responsesStr); responsesStr.prepend(QLatin1String("")); responsesStr.append(QLatin1String("")); QDomDocument document; if (!document.setContent(responsesStr, true)) { setError(ERR_COLLECTIONFETCH); setErrorTextFromDavError(); subjobFinished(); return; } if (!error()) { /* * Extract information from a document like the following: * * * * /caldav.php/test1.user/home/ * * * * * * * * * * * * * * * Test1 User * * * * * * 12345 * * HTTP/1.1 200 OK * * * */ const QDomElement responsesElement = document.documentElement(); 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(QLatin1String("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")); 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); } // don't add this resource if it has already been detected bool alreadySeen = false; for (const DavCollection &seen : qAsConst(d->mCollections)) { if (seen.url().toDisplayString() == url.toDisplayString()) { alreadySeen = true; } } if (alreadySeen) { 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 is either #RRGGBBAA or #RRGGBB but QColor expects #AARRGGBB // so we put the AA in front if the string's length is 9. if (colorValue.size() == 9) { QString fixedColorValue = QStringLiteral("#") + colorValue.mid(7, 2) + colorValue.mid(1, 6); color.setNamedColor(fixedColorValue); } else { color.setNamedColor(colorValue); } } } // extract allowed content types const DavCollection::ContentTypes contentTypes = DavManager::davProtocol(d->mUrl.protocol())->collectionContentTypes(propstatElement); auto _url = url; _url.setUserInfo(d->mUrl.url().userInfo()); DavCollection collection(DavUrl(_url, d->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(KDAV::All); } else { Privileges privileges = Utils::extractPrivileges(currentPrivsElement); collection.setPrivileges(privileges); } qCDebug(KDAV_LOG) << url.toDisplayString() << "PRIVS: " << collection.privileges(); d->mCollections << collection; Q_EMIT collectionDiscovered(d->mUrl.protocol(), url.toDisplayString(), jobUrl); responseElement = Utils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response")); } } } subjobFinished(); } void DavCollectionsFetchJob::subjobFinished() { Q_D(DavCollectionsFetchJob); if (--d->mSubJobCount == 0) { emitResult(); } }