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();
}
}