1/*
2 SPDX-FileCopyrightText: 2010 Tobias Koenig <tokoe@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "davitemslistjob.h"
8#include "davjobbase_p.h"
9
10#include "daverror.h"
11#include "davmanager_p.h"
12#include "davprotocolbase_p.h"
13#include "davurl.h"
14#include "etagcache.h"
15#include "libkdav_debug.h"
16#include "utils_p.h"
17
18#include <KIO/DavJob>
19#include <KIO/Job>
20
21#include <set>
22
23using namespace KDAV;
24
25namespace KDAV
26{
27class DavItemsListJobPrivate : public DavJobBasePrivate
28{
29public:
30 void davJobFinished(KJob *job);
31
32 DavUrl mUrl;
33 std::shared_ptr<EtagCache> mEtagCache;
34 QStringList mMimeTypes;
35 QString mRangeStart;
36 QString mRangeEnd;
37 DavItem::List mItems;
38 std::set<QString> mSeenUrls; // to prevent events duplication with some servers
39 DavItem::List mChangedItems;
40 QStringList mDeletedItems;
41 uint mSubJobCount = 0;
42};
43}
44
45DavItemsListJob::DavItemsListJob(const DavUrl &url, const std::shared_ptr<EtagCache> &cache, QObject *parent)
46 : DavJobBase(new DavItemsListJobPrivate, parent)
47{
48 Q_D(DavItemsListJob);
49 d->mUrl = url;
50 d->mEtagCache = cache;
51}
52
53DavItemsListJob::~DavItemsListJob() = default;
54
55void DavItemsListJob::setContentMimeTypes(const QStringList &types)
56{
57 Q_D(DavItemsListJob);
58 d->mMimeTypes = types;
59}
60
61void DavItemsListJob::setTimeRange(const QString &start, const QString &end)
62{
63 Q_D(DavItemsListJob);
64 d->mRangeStart = start;
65 d->mRangeEnd = end;
66}
67
68void DavItemsListJob::start()
69{
70 Q_D(DavItemsListJob);
71 const DavProtocolBase *protocol = DavManager::davProtocol(protocol: d->mUrl.protocol());
72 Q_ASSERT(protocol);
73
74 const auto queries = protocol->itemsQueries();
75 for (XMLQueryBuilder::Ptr builder : queries) {
76 if (!d->mRangeStart.isEmpty()) {
77 builder->setParameter(QStringLiteral("start"), value: d->mRangeStart);
78 }
79 if (!d->mRangeEnd.isEmpty()) {
80 builder->setParameter(QStringLiteral("end"), value: d->mRangeEnd);
81 }
82
83 const QDomDocument props = builder->buildQuery();
84 const QString mimeType = builder->mimeType();
85
86 if (d->mMimeTypes.isEmpty() || d->mMimeTypes.contains(str: mimeType)) {
87 ++d->mSubJobCount;
88 if (protocol->useReport()) {
89 KIO::DavJob *job = DavManager::self()->createReportJob(url: d->mUrl.url(), document: props.toString());
90 job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
91 job->setProperty(name: "davType", QStringLiteral("report"));
92 job->setProperty(name: "itemsMimeType", value: mimeType);
93 connect(sender: job, signal: &KIO::DavJob::result, context: this, slot: [d](KJob *job) {
94 d->davJobFinished(job);
95 });
96 } else {
97 KIO::DavJob *job = DavManager::self()->createPropFindJob(url: d->mUrl.url(), document: props.toString());
98 job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
99 job->setProperty(name: "davType", QStringLiteral("propFind"));
100 job->setProperty(name: "itemsMimeType", value: mimeType);
101 connect(sender: job, signal: &KIO::DavJob::result, context: this, slot: [d](KJob *job) {
102 d->davJobFinished(job);
103 });
104 }
105 }
106 }
107
108 if (d->mSubJobCount == 0) {
109 setError(ERR_ITEMLIST_NOMIMETYPE);
110 d->setErrorTextFromDavError();
111 emitResult();
112 }
113}
114
115DavItem::List DavItemsListJob::items() const
116{
117 Q_D(const DavItemsListJob);
118 return d->mItems;
119}
120
121DavItem::List DavItemsListJob::changedItems() const
122{
123 Q_D(const DavItemsListJob);
124 return d->mChangedItems;
125}
126
127QStringList DavItemsListJob::deletedItems() const
128{
129 Q_D(const DavItemsListJob);
130 return d->mDeletedItems;
131}
132
133void DavItemsListJobPrivate::davJobFinished(KJob *job)
134{
135 KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(object: job);
136 const int responseCode = davJob->queryMetaData(QStringLiteral("responsecode")).isEmpty() //
137 ? 0
138 : davJob->queryMetaData(QStringLiteral("responsecode")).toInt();
139
140 // KIO::DavJob does not set error() even if the HTTP status code is a 4xx or a 5xx
141 if (davJob->error() || (responseCode >= 400 && responseCode < 600)) {
142 setLatestResponseCode(responseCode);
143 setError(ERR_PROBLEM_WITH_REQUEST);
144 setJobErrorText(davJob->errorText());
145 setJobError(davJob->error());
146 setErrorTextFromDavError();
147 } else {
148 /*
149 * Extract data from a document like the following:
150 *
151 * <multistatus xmlns="DAV:">
152 * <response xmlns="DAV:">
153 * <href xmlns="DAV:">/caldav.php/test1.user/home/KOrganizer-166749289.780.ics</href>
154 * <propstat xmlns="DAV:">
155 * <prop xmlns="DAV:">
156 * <getetag xmlns="DAV:">"b4bbea0278f4f63854c4167a7656024a"</getetag>
157 * </prop>
158 * <status xmlns="DAV:">HTTP/1.1 200 OK</status>
159 * </propstat>
160 * </response>
161 * <response xmlns="DAV:">
162 * <href xmlns="DAV:">/caldav.php/test1.user/home/KOrganizer-399416366.464.ics</href>
163 * <propstat xmlns="DAV:">
164 * <prop xmlns="DAV:">
165 * <getetag xmlns="DAV:">"52eb129018398a7da4f435b2bc4c6cd5"</getetag>
166 * </prop>
167 * <status xmlns="DAV:">HTTP/1.1 200 OK</status>
168 * </propstat>
169 * </response>
170 * </multistatus>
171 */
172
173 const QString itemsMimeType = job->property(name: "itemsMimeType").toString();
174 QDomDocument document;
175 document.setContent(data: davJob->responseData(), options: QDomDocument::ParseOption::UseNamespaceProcessing);
176 const QDomElement documentElement = document.documentElement();
177
178 QDomElement responseElement = Utils::firstChildElementNS(parent: documentElement, QStringLiteral("DAV:"), QStringLiteral("response"));
179 while (!responseElement.isNull()) {
180 QDomElement propstatElement;
181
182 // check for the valid propstat, without giving up on first error
183 {
184 const QDomNodeList propstats = responseElement.elementsByTagNameNS(QStringLiteral("DAV:"), QStringLiteral("propstat"));
185 for (int i = 0; i < propstats.length(); ++i) {
186 const QDomElement propstatCandidate = propstats.item(index: i).toElement();
187 const QDomElement statusElement = Utils::firstChildElementNS(parent: propstatCandidate, QStringLiteral("DAV:"), QStringLiteral("status"));
188 if (statusElement.text().contains(s: QLatin1String("200"))) {
189 propstatElement = propstatCandidate;
190 }
191 }
192 }
193
194 if (propstatElement.isNull()) {
195 responseElement = Utils::nextSiblingElementNS(element: responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
196 continue;
197 }
198
199 const QDomElement propElement = Utils::firstChildElementNS(parent: propstatElement, QStringLiteral("DAV:"), QStringLiteral("prop"));
200
201 // check whether it is a DAV collection ...
202 const QDomElement resourcetypeElement = Utils::firstChildElementNS(parent: propElement, QStringLiteral("DAV:"), QStringLiteral("resourcetype"));
203 if (!resourcetypeElement.isNull()) {
204 const QDomElement collectionElement = Utils::firstChildElementNS(parent: resourcetypeElement, QStringLiteral("DAV:"), QStringLiteral("collection"));
205 if (!collectionElement.isNull()) {
206 responseElement = Utils::nextSiblingElementNS(element: responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
207 continue;
208 }
209 }
210
211 // ... if not it is an item
212 DavItem item;
213 item.setContentType(itemsMimeType);
214
215 // extract path
216 const QDomElement hrefElement = Utils::firstChildElementNS(parent: responseElement, QStringLiteral("DAV:"), QStringLiteral("href"));
217 const QString href = hrefElement.text();
218
219 QUrl url = davJob->url();
220 url.setUserInfo(userInfo: QString());
221 if (href.startsWith(c: QLatin1Char('/'))) {
222 // href is only a path, use request url to complete
223 url.setPath(path: href, mode: QUrl::TolerantMode);
224 } else {
225 // href is a complete url
226 url = QUrl::fromUserInput(userInput: href);
227 }
228
229 const QString itemUrl = url.toDisplayString();
230 const auto [it, isInserted] = mSeenUrls.insert(x: itemUrl);
231 if (!isInserted) {
232 responseElement = Utils::nextSiblingElementNS(element: responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
233 continue;
234 }
235
236 qCDebug(KDAV_LOG) << href << "->" << itemUrl;
237 auto _url = url;
238 _url.setUserInfo(userInfo: mUrl.url().userInfo());
239 item.setUrl(DavUrl(_url, mUrl.protocol()));
240
241 // extract ETag
242 const QDomElement getetagElement = Utils::firstChildElementNS(parent: propElement, QStringLiteral("DAV:"), QStringLiteral("getetag"));
243
244 item.setEtag(getetagElement.text());
245
246 mItems << item;
247
248 if (mEtagCache->etagChanged(remoteId: itemUrl, refEtag: item.etag())) {
249 mChangedItems << item;
250 }
251
252 responseElement = Utils::nextSiblingElementNS(element: responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
253 }
254 }
255
256 mDeletedItems.clear();
257
258 const auto map = mEtagCache->etagCacheMap();
259 for (auto it = map.cbegin(); it != map.cend(); ++it) {
260 const QString remoteId = it.key();
261 if (mSeenUrls.find(x: remoteId) == mSeenUrls.cend()) {
262 mDeletedItems.append(t: remoteId);
263 }
264 }
265
266 if (--mSubJobCount == 0) {
267 emitResult();
268 }
269}
270
271#include "moc_davitemslistjob.cpp"
272

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