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 | |
23 | using namespace KDAV; |
24 | |
25 | namespace KDAV |
26 | { |
27 | class DavItemsListJobPrivate : public DavJobBasePrivate |
28 | { |
29 | public: |
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 | |
45 | DavItemsListJob::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 | |
53 | DavItemsListJob::~DavItemsListJob() = default; |
54 | |
55 | void DavItemsListJob::setContentMimeTypes(const QStringList &types) |
56 | { |
57 | Q_D(DavItemsListJob); |
58 | d->mMimeTypes = types; |
59 | } |
60 | |
61 | void DavItemsListJob::setTimeRange(const QString &start, const QString &end) |
62 | { |
63 | Q_D(DavItemsListJob); |
64 | d->mRangeStart = start; |
65 | d->mRangeEnd = end; |
66 | } |
67 | |
68 | void 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 | |
115 | DavItem::List DavItemsListJob::items() const |
116 | { |
117 | Q_D(const DavItemsListJob); |
118 | return d->mItems; |
119 | } |
120 | |
121 | DavItem::List DavItemsListJob::changedItems() const |
122 | { |
123 | Q_D(const DavItemsListJob); |
124 | return d->mChangedItems; |
125 | } |
126 | |
127 | QStringList DavItemsListJob::deletedItems() const |
128 | { |
129 | Q_D(const DavItemsListJob); |
130 | return d->mDeletedItems; |
131 | } |
132 | |
133 | void 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 | |