1/*
2 SPDX-FileCopyrightText: 2011 Grégory Oestreicher <greg@kamago.net>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "davprincipalsearchjob.h"
8#include "davjobbase_p.h"
9
10#include "daverror.h"
11#include "davmanager_p.h"
12#include "utils_p.h"
13
14#include <KIO/DavJob>
15#include <KIO/Job>
16
17#include <QUrl>
18
19using namespace KDAV;
20
21namespace KDAV
22{
23class DavPrincipalSearchJobPrivate : public DavJobBasePrivate
24{
25public:
26 void buildReportQuery(QDomDocument &query) const;
27 void principalCollectionSetSearchFinished(KJob *job);
28 void principalPropertySearchFinished(KJob *job);
29
30 DavUrl mUrl;
31 DavPrincipalSearchJob::FilterType mType;
32 QString mFilter;
33 int mPrincipalPropertySearchSubJobCount = 0;
34 bool mPrincipalPropertySearchSubJobSuccessful = false;
35 struct PropertyInfo {
36 QString propNS;
37 QString propName;
38 };
39 std::vector<PropertyInfo> mFetchProperties;
40 QList<DavPrincipalSearchJob::Result> mResults;
41};
42}
43
44DavPrincipalSearchJob::DavPrincipalSearchJob(const DavUrl &url, DavPrincipalSearchJob::FilterType type, const QString &filter, QObject *parent)
45 : DavJobBase(new DavPrincipalSearchJobPrivate, parent)
46{
47 Q_D(DavPrincipalSearchJob);
48 d->mUrl = url;
49 d->mType = type;
50 d->mFilter = filter;
51}
52
53void DavPrincipalSearchJob::fetchProperty(const QString &name, const QString &ns)
54{
55 Q_D(DavPrincipalSearchJob);
56 d->mFetchProperties.push_back(x: {.propNS: !ns.isEmpty() ? ns : QStringLiteral("DAV:"), .propName: name});
57}
58
59DavUrl DavPrincipalSearchJob::davUrl() const
60{
61 Q_D(const DavPrincipalSearchJob);
62 return d->mUrl;
63}
64
65void DavPrincipalSearchJob::start()
66{
67 Q_D(DavPrincipalSearchJob);
68 /*
69 * The first step is to try to locate the URL that contains the principals.
70 * This is done with a PROPFIND request and a XML like this:
71 * <?xml version="1.0" encoding="utf-8" ?>
72 * <D:propfind xmlns:D="DAV:">
73 * <D:prop>
74 * <D:principal-collection-set/>
75 * </D:prop>
76 * </D:propfind>
77 */
78 QDomDocument query;
79
80 QDomElement propfind = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("propfind"));
81 query.appendChild(newChild: propfind);
82
83 QDomElement prop = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
84 propfind.appendChild(newChild: prop);
85
86 QDomElement principalCollectionSet = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("principal-collection-set"));
87 prop.appendChild(newChild: principalCollectionSet);
88
89 KIO::DavJob *job = DavManager::self()->createPropFindJob(url: d->mUrl.url(), document: query.toString());
90 job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
91 connect(sender: job, signal: &KIO::DavJob::result, context: this, slot: [d](KJob *job) {
92 d->principalCollectionSetSearchFinished(job);
93 });
94 job->start();
95}
96
97void DavPrincipalSearchJobPrivate::principalCollectionSetSearchFinished(KJob *job)
98{
99 KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(object: job);
100 const QString responseCodeStr = davJob->queryMetaData(QStringLiteral("responsecode"));
101 const int responseCode = responseCodeStr.isEmpty() ? 0 : responseCodeStr.toInt();
102 // KIO::DavJob does not set error() even if the HTTP status code is a 4xx or a 5xx
103 if (davJob->error() || (responseCode >= 400 && responseCode < 600)) {
104 setLatestResponseCode(responseCode);
105 setError(ERR_PROBLEM_WITH_REQUEST);
106 setJobErrorText(davJob->errorText());
107 setJobError(davJob->error());
108 setErrorTextFromDavError();
109
110 emitResult();
111 return;
112 }
113
114 if (job->error()) {
115 setError(job->error());
116 setErrorText(job->errorText());
117 emitResult();
118 return;
119 }
120
121 /*
122 * Extract information from a document like the following:
123 *
124 * <?xml version="1.0" encoding="utf-8" ?>
125 * <D:multistatus xmlns:D="DAV:">
126 * <D:response>
127 * <D:href>http://www.example.com/papers/</D:href>
128 * <D:propstat>
129 * <D:prop>
130 * <D:principal-collection-set>
131 * <D:href>http://www.example.com/acl/users/</D:href>
132 * <D:href>http://www.example.com/acl/groups/</D:href>
133 * </D:principal-collection-set>
134 * </D:prop>
135 * <D:status>HTTP/1.1 200 OK</D:status>
136 * </D:propstat>
137 * </D:response>
138 * </D:multistatus>
139 */
140
141 QDomDocument document;
142 document.setContent(data: davJob->responseData(), options: QDomDocument::ParseOption::UseNamespaceProcessing);
143 QDomElement documentElement = document.documentElement();
144
145 QDomElement responseElement = Utils::firstChildElementNS(parent: documentElement, QStringLiteral("DAV:"), QStringLiteral("response"));
146 if (responseElement.isNull()) {
147 emitResult();
148 return;
149 }
150
151 // check for the valid propstat, without giving up on first error
152 QDomElement propstatElement;
153 {
154 const QDomNodeList propstats = responseElement.elementsByTagNameNS(QStringLiteral("DAV:"), QStringLiteral("propstat"));
155 for (int i = 0; i < propstats.length(); ++i) {
156 const QDomElement propstatCandidate = propstats.item(index: i).toElement();
157 const QDomElement statusElement = Utils::firstChildElementNS(parent: propstatCandidate, QStringLiteral("DAV:"), QStringLiteral("status"));
158 if (statusElement.text().contains(s: QLatin1String("200"))) {
159 propstatElement = propstatCandidate;
160 }
161 }
162 }
163
164 if (propstatElement.isNull()) {
165 emitResult();
166 return;
167 }
168
169 QDomElement propElement = Utils::firstChildElementNS(parent: propstatElement, QStringLiteral("DAV:"), QStringLiteral("prop"));
170 if (propElement.isNull()) {
171 emitResult();
172 return;
173 }
174
175 QDomElement principalCollectionSetElement = Utils::firstChildElementNS(parent: propElement, QStringLiteral("DAV:"), QStringLiteral("principal-collection-set"));
176 if (principalCollectionSetElement.isNull()) {
177 emitResult();
178 return;
179 }
180
181 QDomNodeList hrefNodes = principalCollectionSetElement.elementsByTagNameNS(QStringLiteral("DAV:"), QStringLiteral("href"));
182 for (int i = 0; i < hrefNodes.size(); ++i) {
183 QDomElement hrefElement = hrefNodes.at(index: i).toElement();
184 QString href = hrefElement.text();
185
186 QUrl url = mUrl.url();
187 if (href.startsWith(c: QLatin1Char('/'))) {
188 // href is only a path, use request url to complete
189 url.setPath(path: href, mode: QUrl::TolerantMode);
190 } else {
191 // href is a complete url
192 QUrl tmpUrl(href);
193 tmpUrl.setUserName(userName: url.userName());
194 tmpUrl.setPassword(password: url.password());
195 url = tmpUrl;
196 }
197
198 QDomDocument principalPropertySearchQuery;
199 buildReportQuery(query&: principalPropertySearchQuery);
200 KIO::DavJob *reportJob = DavManager::self()->createReportJob(url, document: principalPropertySearchQuery.toString());
201 reportJob->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
202 QObject::connect(sender: reportJob, signal: &KIO::DavJob::result, context: q_ptr, slot: [this](KJob *job) {
203 principalPropertySearchFinished(job);
204 });
205 ++mPrincipalPropertySearchSubJobCount;
206 reportJob->start();
207 }
208}
209
210void DavPrincipalSearchJobPrivate::principalPropertySearchFinished(KJob *job)
211{
212 --mPrincipalPropertySearchSubJobCount;
213
214 if (job->error() && !mPrincipalPropertySearchSubJobSuccessful) {
215 setError(job->error());
216 setErrorText(job->errorText());
217 if (mPrincipalPropertySearchSubJobCount == 0) {
218 emitResult();
219 }
220 return;
221 }
222
223 KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(object: job);
224
225 const int responseCode = davJob->queryMetaData(QStringLiteral("responsecode")).toInt();
226
227 if (responseCode > 499 && responseCode < 600 && !mPrincipalPropertySearchSubJobSuccessful) {
228 // Server-side error, unrecoverable
229 setLatestResponseCode(responseCode);
230 setError(ERR_SERVER_UNRECOVERABLE);
231 setJobErrorText(davJob->errorText());
232 setJobError(davJob->error());
233 setErrorTextFromDavError();
234 if (mPrincipalPropertySearchSubJobCount == 0) {
235 emitResult();
236 }
237 return;
238 } else if (responseCode > 399 && responseCode < 500 && !mPrincipalPropertySearchSubJobSuccessful) {
239 setLatestResponseCode(responseCode);
240 setError(ERR_PROBLEM_WITH_REQUEST);
241 setJobErrorText(davJob->errorText());
242 setJobError(davJob->error());
243 setErrorTextFromDavError();
244
245 if (mPrincipalPropertySearchSubJobCount == 0) {
246 emitResult();
247 }
248 return;
249 }
250
251 if (!mPrincipalPropertySearchSubJobSuccessful) {
252 setError(0); // nope, everything went fine
253 mPrincipalPropertySearchSubJobSuccessful = true;
254 }
255
256 /*
257 * Extract infos from a document like the following:
258 * <?xml version="1.0" encoding="utf-8" ?>
259 * <D:multistatus xmlns:D="DAV:" xmlns:B="http://BigCorp.com/ns/">
260 * <D:response>
261 * <D:href>http://www.example.com/users/jdoe</D:href>
262 * <D:propstat>
263 * <D:prop>
264 * <D:displayname>John Doe</D:displayname>
265 * </D:prop>
266 * <D:status>HTTP/1.1 200 OK</D:status>
267 * </D:propstat>
268 * </D:multistatus>
269 */
270
271 QDomDocument document;
272 document.setContent(data: davJob->responseData(), options: QDomDocument::ParseOption::UseNamespaceProcessing);
273 const QDomElement documentElement = document.documentElement();
274
275 QDomElement responseElement = Utils::firstChildElementNS(parent: documentElement, QStringLiteral("DAV:"), QStringLiteral("response"));
276 if (responseElement.isNull()) {
277 if (mPrincipalPropertySearchSubJobCount == 0) {
278 emitResult();
279 }
280 return;
281 }
282
283 // check for the valid propstat, without giving up on first error
284 QDomElement propstatElement;
285 {
286 const QDomNodeList propstats = responseElement.elementsByTagNameNS(QStringLiteral("DAV:"), QStringLiteral("propstat"));
287 const int propStatsEnd(propstats.length());
288 for (int i = 0; i < propStatsEnd; ++i) {
289 const QDomElement propstatCandidate = propstats.item(index: i).toElement();
290 const QDomElement statusElement = Utils::firstChildElementNS(parent: propstatCandidate, QStringLiteral("DAV:"), QStringLiteral("status"));
291 if (statusElement.text().contains(s: QLatin1String("200"))) {
292 propstatElement = propstatCandidate;
293 }
294 }
295 }
296
297 if (propstatElement.isNull()) {
298 if (mPrincipalPropertySearchSubJobCount == 0) {
299 emitResult();
300 }
301 return;
302 }
303
304 QDomElement propElement = Utils::firstChildElementNS(parent: propstatElement, QStringLiteral("DAV:"), QStringLiteral("prop"));
305 if (propElement.isNull()) {
306 if (mPrincipalPropertySearchSubJobCount == 0) {
307 emitResult();
308 }
309 return;
310 }
311
312 // All requested properties are now under propElement, so let's find them
313 for (const auto &[propNS, propName] : mFetchProperties) {
314 const QDomNodeList fetchNodes = propElement.elementsByTagNameNS(nsURI: propNS, localName: propName);
315 mResults.reserve(asize: mResults.size() + fetchNodes.size());
316 for (int i = 0; i < fetchNodes.size(); ++i) {
317 const QDomElement fetchElement = fetchNodes.at(index: i).toElement();
318 mResults.push_back(t: {.propertyNamespace: propNS, .property: propName, .value: fetchElement.text()});
319 }
320 }
321
322 if (mPrincipalPropertySearchSubJobCount == 0) {
323 emitResult();
324 }
325}
326
327QList<DavPrincipalSearchJob::Result> DavPrincipalSearchJob::results() const
328{
329 Q_D(const DavPrincipalSearchJob);
330 return d->mResults;
331}
332
333void DavPrincipalSearchJobPrivate::buildReportQuery(QDomDocument &query) const
334{
335 /*
336 * Build a document like the following, where XXX will
337 * be replaced by the properties the user want to fetch:
338 *
339 * <?xml version="1.0" encoding="utf-8" ?>
340 * <D:principal-property-search xmlns:D="DAV:">
341 * <D:property-search>
342 * <D:prop>
343 * <D:displayname/>
344 * </D:prop>
345 * <D:match>FILTER</D:match>
346 * </D:property-search>
347 * <D:prop>
348 * XXX
349 * </D:prop>
350 * </D:principal-property-search>
351 */
352
353 QDomElement principalPropertySearch = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("principal-property-search"));
354 query.appendChild(newChild: principalPropertySearch);
355
356 QDomElement propertySearch = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("property-search"));
357 principalPropertySearch.appendChild(newChild: propertySearch);
358
359 QDomElement prop = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
360 propertySearch.appendChild(newChild: prop);
361
362 if (mType == DavPrincipalSearchJob::DisplayName) {
363 QDomElement displayName = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("displayname"));
364 prop.appendChild(newChild: displayName);
365 } else if (mType == DavPrincipalSearchJob::EmailAddress) {
366 QDomElement calendarUserAddressSet =
367 query.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("calendar-user-address-set"));
368 prop.appendChild(newChild: calendarUserAddressSet);
369 // QDomElement hrefElement = query.createElementNS( "DAV:", "href" );
370 // prop.appendChild( hrefElement );
371 }
372
373 QDomElement match = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("match"));
374 propertySearch.appendChild(newChild: match);
375
376 QDomText propFilter = query.createTextNode(data: mFilter);
377 match.appendChild(newChild: propFilter);
378
379 prop = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
380 principalPropertySearch.appendChild(newChild: prop);
381
382 for (const auto &[propNS, propName] : mFetchProperties) {
383 QDomElement elem = query.createElementNS(nsURI: propNS, qName: propName);
384 prop.appendChild(newChild: elem);
385 }
386}
387
388#include "moc_davprincipalsearchjob.cpp"
389

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