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 | |
23 | using namespace KDAV; |
24 | |
25 | namespace KDAV |
26 | { |
27 | class DavCollectionsFetchJobPrivate : public DavJobBasePrivate |
28 | { |
29 | public: |
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 | |
43 | DavCollectionsFetchJob::DavCollectionsFetchJob(const DavUrl &url, QObject *parent) |
44 | : DavJobBase(new DavCollectionsFetchJobPrivate, parent) |
45 | { |
46 | Q_D(DavCollectionsFetchJob); |
47 | d->mUrl = url; |
48 | } |
49 | |
50 | void 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 | |
64 | DavCollection::List DavCollectionsFetchJob::collections() const |
65 | { |
66 | Q_D(const DavCollectionsFetchJob); |
67 | return d->mCollections; |
68 | } |
69 | |
70 | DavUrl DavCollectionsFetchJob::davUrl() const |
71 | { |
72 | Q_D(const DavCollectionsFetchJob); |
73 | return d->mUrl; |
74 | } |
75 | |
76 | void 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 | |
89 | void 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 | |
137 | void 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 | |
351 | void DavCollectionsFetchJobPrivate::subjobFinished() |
352 | { |
353 | if (--mSubJobCount == 0) { |
354 | emitResult(); |
355 | } |
356 | } |
357 | |
358 | #include "moc_davcollectionsfetchjob.cpp" |
359 | |