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

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