KDAV

davprincipalsearchjob.cpp
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;
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;
41};
42}
43
45 : DavJobBase(new DavPrincipalSearchJobPrivate, parent)
46{
48 d->mUrl = url;
49 d->mType = type;
50 d->mFilter = filter;
51}
52
54{
56 d->mFetchProperties.push_back({!ns.isEmpty() ? ns : QStringLiteral("DAV:"), name});
57}
58
60{
62 return d->mUrl;
63}
64
66{
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(propfind);
82
83 QDomElement prop = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
84 propfind.appendChild(prop);
85
86 QDomElement principalCollectionSet = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("principal-collection-set"));
87 prop.appendChild(principalCollectionSet);
88
89 KIO::DavJob *job = DavManager::self()->createPropFindJob(d->mUrl.url(), query.toString());
90 job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
91 connect(job, &KIO::DavJob::result, this, [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 *>(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(davJob->responseData(), QDomDocument::ParseOption::UseNamespaceProcessing);
143 QDomElement documentElement = document.documentElement();
144
145 QDomElement responseElement = Utils::firstChildElementNS(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(i).toElement();
157 const QDomElement statusElement = Utils::firstChildElementNS(propstatCandidate, QStringLiteral("DAV:"), QStringLiteral("status"));
158 if (statusElement.text().contains(QLatin1String("200"))) {
159 propstatElement = propstatCandidate;
160 }
161 }
162 }
163
164 if (propstatElement.isNull()) {
165 emitResult();
166 return;
167 }
168
169 QDomElement propElement = Utils::firstChildElementNS(propstatElement, QStringLiteral("DAV:"), QStringLiteral("prop"));
170 if (propElement.isNull()) {
171 emitResult();
172 return;
173 }
174
175 QDomElement principalCollectionSetElement = Utils::firstChildElementNS(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(i).toElement();
184 QString href = hrefElement.text();
185
186 QUrl url = mUrl.url();
187 if (href.startsWith(QLatin1Char('/'))) {
188 // href is only a path, use request url to complete
189 url.setPath(href, QUrl::TolerantMode);
190 } else {
191 // href is a complete url
192 QUrl tmpUrl(href);
193 tmpUrl.setUserName(url.userName());
194 tmpUrl.setPassword(url.password());
195 url = tmpUrl;
196 }
197
198 QDomDocument principalPropertySearchQuery;
199 buildReportQuery(principalPropertySearchQuery);
200 KIO::DavJob *reportJob = DavManager::self()->createReportJob(url, principalPropertySearchQuery.toString());
201 reportJob->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
202 QObject::connect(reportJob, &KIO::DavJob::result, q_ptr, [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 *>(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(davJob->responseData(), QDomDocument::ParseOption::UseNamespaceProcessing);
273 const QDomElement documentElement = document.documentElement();
274
275 QDomElement responseElement = Utils::firstChildElementNS(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(i).toElement();
290 const QDomElement statusElement = Utils::firstChildElementNS(propstatCandidate, QStringLiteral("DAV:"), QStringLiteral("status"));
291 if (statusElement.text().contains(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(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(propNS, propName);
315 mResults.reserve(mResults.size() + fetchNodes.size());
316 for (int i = 0; i < fetchNodes.size(); ++i) {
317 const QDomElement fetchElement = fetchNodes.at(i).toElement();
318 mResults.push_back({propNS, propName, fetchElement.text()});
319 }
320 }
321
322 if (mPrincipalPropertySearchSubJobCount == 0) {
323 emitResult();
324 }
325}
326
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(principalPropertySearch);
355
356 QDomElement propertySearch = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("property-search"));
357 principalPropertySearch.appendChild(propertySearch);
358
359 QDomElement prop = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
360 propertySearch.appendChild(prop);
361
362 if (mType == DavPrincipalSearchJob::DisplayName) {
363 QDomElement displayName = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("displayname"));
364 prop.appendChild(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(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(match);
375
376 QDomText propFilter = query.createTextNode(mFilter);
377 match.appendChild(propFilter);
378
379 prop = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop"));
380 principalPropertySearch.appendChild(prop);
381
382 for (const auto &[propNS, propName] : mFetchProperties) {
383 QDomElement elem = query.createElementNS(propNS, propName);
384 prop.appendChild(elem);
385 }
386}
387
388#include "moc_davprincipalsearchjob.cpp"
base class for the jobs used by the resource.
Definition davjobbase.h:27
A job that search a DAV principal on a server.
DavPrincipalSearchJob(const DavUrl &url, FilterType type, const QString &filter, QObject *parent=nullptr)
Creates a new DAV principal search job.
DavUrl davUrl() const
Return the DavUrl used by this job.
void fetchProperty(const QString &name, const QString &ns=QString())
Add a new property to fetch from the server.
QList< Result > results() const
Get the job results.
void start() override
Starts the job.
FilterType
Types of search that are supported by this job.
A helper class to combine URL and protocol of a DAV URL.
Definition davurl.h:27
QUrl url() const
Returns the URL that identifies the DAV object.
Definition davurl.cpp:45
QByteArray responseData() const
void addMetaData(const QMap< QString, QString > &values)
QString queryMetaData(const QString &key)
int error() const
void result(KJob *job)
virtual Q_SCRIPTABLE void start()=0
QString errorText() const
std::optional< QSqlQuery > query(const QString &queryStatement)
The KDAV namespace.
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
QDomElement documentElement() const const
ParseResult setContent(QAnyStringView text, ParseOptions options)
QString toString(int indent) const const
QDomNodeList elementsByTagNameNS(const QString &nsURI, const QString &localName) const const
QString text() const const
QDomNode appendChild(const QDomNode &newChild)
bool isNull() const const
QDomElement toElement() const const
QDomNode at(int index) const const
QDomNode item(int index) const const
int length() const const
int size() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
int toInt(bool *ok, int base) const const
TolerantMode
QString password(ComponentFormattingOptions options) const const
void setPath(const QString &path, ParsingMode mode)
QString userName(ComponentFormattingOptions options) const const
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 17:02:02 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.