1/*
2 SPDX-FileCopyrightText: 2010 Tobias Koenig <tokoe@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "davcollectionsfetchjob.h"
8#include "davjobbase_p.h"
9
10#include "daverror.h"
11#include "davmanager_p.h"
12#include "davprincipalhomesetsfetchjob.h"
13#include "davprotocolbase_p.h"
14#include "utils_p.h"
15
16#include "libkdav_debug.h"
17#include <KIO/DavJob>
18#include <KIO/Job>
19
20#include <QBuffer>
21#include <QColor>
22
23using namespace KDAV;
24
25namespace KDAV
26{
27class DavCollectionsFetchJobPrivate : public DavJobBasePrivate
28{
29public:
30 void principalFetchFinished(KJob *job);
31 void collectionsFetchFinished(KJob *job);
32 void doCollectionsFetch(const QUrl &url);
33 void subjobFinished();
34
35 DavUrl mUrl;
36 DavCollection::List mCollections;
37 uint mSubJobCount = 0;
38
39 Q_DECLARE_PUBLIC(DavCollectionsFetchJob)
40};
41}
42
43DavCollectionsFetchJob::DavCollectionsFetchJob(const DavUrl &url, QObject *parent)
44 : DavJobBase(new DavCollectionsFetchJobPrivate, parent)
45{
46 Q_D(DavCollectionsFetchJob);
47 d->mUrl = url;
48}
49
50void DavCollectionsFetchJob::start()
51{
52 Q_D(DavCollectionsFetchJob);
53 if (DavManager::davProtocol(protocol: d->mUrl.protocol())->supportsPrincipals()) {
54 DavPrincipalHomeSetsFetchJob *job = new DavPrincipalHomeSetsFetchJob(d->mUrl);
55 connect(sender: job, signal: &DavPrincipalHomeSetsFetchJob::result, context: this, slot: [d](KJob *job) {
56 d->principalFetchFinished(job);
57 });
58 job->start();
59 } else {
60 d->doCollectionsFetch(url: d->mUrl.url());
61 }
62}
63
64DavCollection::List DavCollectionsFetchJob::collections() const
65{
66 Q_D(const DavCollectionsFetchJob);
67 return d->mCollections;
68}
69
70DavUrl DavCollectionsFetchJob::davUrl() const
71{
72 Q_D(const DavCollectionsFetchJob);
73 return d->mUrl;
74}
75
76void DavCollectionsFetchJobPrivate::doCollectionsFetch(const QUrl &url)
77{
78 ++mSubJobCount;
79
80 const QDomDocument collectionQuery = DavManager::davProtocol(protocol: mUrl.protocol())->collectionsQuery()->buildQuery();
81
82 KIO::DavJob *job = DavManager::self()->createPropFindJob(url, document: collectionQuery.toString());
83 QObject::connect(sender: job, signal: &KIO::DavJob::result, context: q_ptr, slot: [this](KJob *job) {
84 collectionsFetchFinished(job);
85 });
86 job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
87}
88
89void DavCollectionsFetchJobPrivate::principalFetchFinished(KJob *job)
90{
91 const DavPrincipalHomeSetsFetchJob *davJob = qobject_cast<DavPrincipalHomeSetsFetchJob *>(object: job);
92
93 if (davJob->error()) {
94 if (davJob->canRetryLater()) {
95 // If we have a non-persistent HTTP error code then this may mean that
96 // the URL was not a principal URL. Retry as if it were a calendar URL.
97 qCDebug(KDAV_LOG) << job->errorText();
98 doCollectionsFetch(url: mUrl.url());
99 } else {
100 // Just give up here.
101 setDavError(davJob->davError());
102 setErrorTextFromDavError();
103 emitResult();
104 }
105
106 return;
107 }
108
109 const QStringList homeSets = davJob->homeSets();
110 qCDebug(KDAV_LOG) << "Found" << homeSets.size() << "homesets";
111 qCDebug(KDAV_LOG) << homeSets;
112
113 if (homeSets.isEmpty()) {
114 // Same as above, retry as if it were a calendar URL.
115 doCollectionsFetch(url: mUrl.url());
116 return;
117 }
118
119 for (const QString &homeSet : homeSets) {
120 QUrl url = mUrl.url();
121
122 if (homeSet.startsWith(c: QLatin1Char('/'))) {
123 // homeSet is only a path, use request url to complete
124 url.setPath(path: homeSet, mode: QUrl::TolerantMode);
125 } else {
126 // homeSet is a complete url
127 QUrl tmpUrl(homeSet);
128 tmpUrl.setUserName(userName: url.userName());
129 tmpUrl.setPassword(password: url.password());
130 url = tmpUrl;
131 }
132
133 doCollectionsFetch(url);
134 }
135}
136
137void DavCollectionsFetchJobPrivate::collectionsFetchFinished(KJob *job)
138{
139 Q_Q(DavCollectionsFetchJob);
140 KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(object: job);
141 const QString responseCodeStr = davJob->queryMetaData(QStringLiteral("responsecode"));
142 const int responseCode = responseCodeStr.isEmpty() ? 0 : responseCodeStr.toInt();
143
144 // KIO::DavJob does not set error() even if the HTTP status code is a 4xx or a 5xx
145 if (davJob->error() || (responseCode >= 400 && responseCode < 600)) {
146 if (davJob->url() != mUrl.url()) {
147 // Retry as if the initial URL was a calendar URL.
148 // We can end up here when retrieving a homeset on
149 // which a PROPFIND resulted in an error
150 doCollectionsFetch(url: mUrl.url());
151 --mSubJobCount;
152 return;
153 }
154
155 setLatestResponseCode(responseCode);
156 setError(ERR_PROBLEM_WITH_REQUEST);
157 setJobErrorText(davJob->errorText());
158 setJobError(davJob->error());
159 setErrorTextFromDavError();
160 } else {
161 // For use in the collectionDiscovered() signal
162 QUrl _jobUrl = mUrl.url();
163 _jobUrl.setUserInfo(userInfo: QString());
164 const QString jobUrl = _jobUrl.toDisplayString();
165
166 // Validate that we got a valid PROPFIND response
167 QDomDocument response;
168 response.setContent(data: davJob->responseData(), options: QDomDocument::ParseOption::UseNamespaceProcessing);
169 QDomElement rootElement = response.documentElement();
170 if (rootElement.tagName().compare(other: QLatin1String("multistatus"), cs: Qt::CaseInsensitive) != 0) {
171 setError(ERR_COLLECTIONFETCH);
172 setErrorTextFromDavError();
173 subjobFinished();
174 return;
175 }
176
177 QByteArray resp = davJob->responseData();
178 QDomDocument document;
179 if (!document.setContent(data: resp, options: QDomDocument::ParseOption::UseNamespaceProcessing)) {
180 setError(ERR_COLLECTIONFETCH);
181 setErrorTextFromDavError();
182 subjobFinished();
183 return;
184 }
185
186 if (!q->error()) {
187 /*
188 * Extract information from a document like the following:
189 *
190 * <responses>
191 * <response xmlns="DAV:">
192 * <href xmlns="DAV:">/caldav.php/test1.user/home/</href>
193 * <propstat xmlns="DAV:">
194 * <prop xmlns="DAV:">
195 * <C:supported-calendar-component-set xmlns:C="urn:ietf:params:xml:ns:caldav">
196 * <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VEVENT"/>
197 * <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VTODO"/>
198 * <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VJOURNAL"/>
199 * <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VTIMEZONE"/>
200 * <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VFREEBUSY"/>
201 * </C:supported-calendar-component-set>
202 * <resourcetype xmlns="DAV:">
203 * <collection xmlns="DAV:"/>
204 * <C:calendar xmlns:C="urn:ietf:params:xml:ns:caldav"/>
205 * <C:schedule-calendar xmlns:C="urn:ietf:params:xml:ns:caldav"/>
206 * </resourcetype>
207 * <displayname xmlns="DAV:">Test1 User</displayname>
208 * <current-user-privilege-set xmlns="DAV:">
209 * <privilege xmlns="DAV:">
210 * <read xmlns="DAV:"/>
211 * </privilege>
212 * </current-user-privilege-set>
213 * <getctag xmlns="http://calendarserver.org/ns/">12345</getctag>
214 * </prop>
215 * <status xmlns="DAV:">HTTP/1.1 200 OK</status>
216 * </propstat>
217 * </response>
218 * </responses>
219 */
220
221 const QDomElement responsesElement = document.documentElement();
222
223 QDomElement responseElement = Utils::firstChildElementNS(parent: responsesElement, QStringLiteral("DAV:"), QStringLiteral("response"));
224 while (!responseElement.isNull()) {
225 QDomElement propstatElement;
226
227 // check for the valid propstat, without giving up on first error
228 {
229 const QDomNodeList propstats = responseElement.elementsByTagNameNS(QStringLiteral("DAV:"), QStringLiteral("propstat"));
230 for (int i = 0; i < propstats.length(); ++i) {
231 const QDomElement propstatCandidate = propstats.item(index: i).toElement();
232 const QDomElement statusElement = Utils::firstChildElementNS(parent: propstatCandidate, QStringLiteral("DAV:"), QStringLiteral("status"));
233 if (statusElement.text().contains(s: QLatin1String("200"))) {
234 propstatElement = propstatCandidate;
235 }
236 }
237 }
238
239 if (propstatElement.isNull()) {
240 responseElement = Utils::nextSiblingElementNS(element: responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
241 continue;
242 }
243
244 // extract url
245 const QDomElement hrefElement = Utils::firstChildElementNS(parent: responseElement, QStringLiteral("DAV:"), QStringLiteral("href"));
246 if (hrefElement.isNull()) {
247 responseElement = Utils::nextSiblingElementNS(element: responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
248 continue;
249 }
250
251 QString href = hrefElement.text();
252 if (!href.endsWith(c: QLatin1Char('/'))) {
253 href.append(c: QLatin1Char('/'));
254 }
255
256 QUrl url = davJob->url();
257 url.setUserInfo(userInfo: QString());
258 if (href.startsWith(c: QLatin1Char('/'))) {
259 // href is only a path, use request url to complete
260 url.setPath(path: href, mode: QUrl::TolerantMode);
261 } else {
262 // href is a complete url
263 url = QUrl::fromUserInput(userInput: href);
264 }
265
266 // don't add this resource if it has already been detected
267 bool alreadySeen = false;
268 for (const DavCollection &seen : std::as_const(t&: mCollections)) {
269 if (seen.url().toDisplayString() == url.toDisplayString()) {
270 alreadySeen = true;
271 }
272 }
273 if (alreadySeen) {
274 responseElement = Utils::nextSiblingElementNS(element: responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
275 continue;
276 }
277
278 // extract display name
279 const QDomElement propElement = Utils::firstChildElementNS(parent: propstatElement, QStringLiteral("DAV:"), QStringLiteral("prop"));
280 if (!DavManager::davProtocol(protocol: mUrl.protocol())->containsCollection(propElem: propElement)) {
281 responseElement = Utils::nextSiblingElementNS(element: responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
282 continue;
283 }
284 const QDomElement displaynameElement = Utils::firstChildElementNS(parent: propElement, QStringLiteral("DAV:"), QStringLiteral("displayname"));
285 const QString displayName = displaynameElement.text();
286
287 // Extract CTag
288 const QDomElement CTagElement = Utils::firstChildElementNS(parent: propElement, //
289 QStringLiteral("http://calendarserver.org/ns/"),
290 QStringLiteral("getctag"));
291 QString CTag;
292 if (!CTagElement.isNull()) {
293 CTag = CTagElement.text();
294 }
295
296 // extract calendar color if provided
297 const QDomElement colorElement = Utils::firstChildElementNS(parent: propElement, //
298 QStringLiteral("http://apple.com/ns/ical/"),
299 QStringLiteral("calendar-color"));
300 QColor color;
301 if (!colorElement.isNull()) {
302 QString colorValue = colorElement.text();
303 if (QColor::isValidColorName(colorValue)) {
304 // Color is either #RRGGBBAA or #RRGGBB but QColor expects #AARRGGBB
305 // so we put the AA in front if the string's length is 9.
306 if (colorValue.size() == 9) {
307 QString fixedColorValue = QStringLiteral("#") + colorValue.mid(position: 7, n: 2) + colorValue.mid(position: 1, n: 6);
308 color = QColor::fromString(name: fixedColorValue);
309 } else {
310 color = QColor::fromString(name: colorValue);
311 }
312 }
313 }
314
315 // extract allowed content types
316 const DavCollection::ContentTypes contentTypes = DavManager::davProtocol(protocol: mUrl.protocol())->collectionContentTypes(propstat: propstatElement);
317
318 auto _url = url;
319 _url.setUserInfo(userInfo: mUrl.url().userInfo());
320 DavCollection collection(DavUrl(_url, mUrl.protocol()), displayName, contentTypes);
321
322 collection.setCTag(CTag);
323 if (color.isValid()) {
324 collection.setColor(color);
325 }
326
327 // extract privileges
328 const QDomElement currentPrivsElement = Utils::firstChildElementNS(parent: propElement, //
329 QStringLiteral("DAV:"),
330 QStringLiteral("current-user-privilege-set"));
331 if (currentPrivsElement.isNull()) {
332 // Assume that we have all privileges
333 collection.setPrivileges(KDAV::All);
334 } else {
335 Privileges privileges = Utils::extractPrivileges(element: currentPrivsElement);
336 collection.setPrivileges(privileges);
337 }
338
339 qCDebug(KDAV_LOG) << url.toDisplayString() << "PRIVS: " << collection.privileges();
340 mCollections << collection;
341 Q_EMIT q->collectionDiscovered(protocol: mUrl.protocol(), collectionUrl: url.toDisplayString(), configuredUrl: jobUrl);
342
343 responseElement = Utils::nextSiblingElementNS(element: responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
344 }
345 }
346 }
347
348 subjobFinished();
349}
350
351void DavCollectionsFetchJobPrivate::subjobFinished()
352{
353 if (--mSubJobCount == 0) {
354 emitResult();
355 }
356}
357
358#include "moc_davcollectionsfetchjob.cpp"
359

source code of kdav/src/common/davcollectionsfetchjob.cpp