File indexing completed on 2024-05-05 07:55:01
0001 /* 0002 SPDX-FileCopyrightText: 2011 Grégory Oestreicher <greg@kamago.net> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "davprincipalsearchjob.h" 0008 #include "davjobbase_p.h" 0009 0010 #include "daverror.h" 0011 #include "davmanager_p.h" 0012 #include "utils_p.h" 0013 0014 #include <KIO/DavJob> 0015 #include <KIO/Job> 0016 0017 #include <QUrl> 0018 0019 using namespace KDAV; 0020 0021 namespace KDAV 0022 { 0023 class DavPrincipalSearchJobPrivate : public DavJobBasePrivate 0024 { 0025 public: 0026 void buildReportQuery(QDomDocument &query) const; 0027 void principalCollectionSetSearchFinished(KJob *job); 0028 void principalPropertySearchFinished(KJob *job); 0029 0030 DavUrl mUrl; 0031 DavPrincipalSearchJob::FilterType mType; 0032 QString mFilter; 0033 int mPrincipalPropertySearchSubJobCount = 0; 0034 bool mPrincipalPropertySearchSubJobSuccessful = false; 0035 struct PropertyInfo { 0036 QString propNS; 0037 QString propName; 0038 }; 0039 std::vector<PropertyInfo> mFetchProperties; 0040 QList<DavPrincipalSearchJob::Result> mResults; 0041 }; 0042 } 0043 0044 DavPrincipalSearchJob::DavPrincipalSearchJob(const DavUrl &url, DavPrincipalSearchJob::FilterType type, const QString &filter, QObject *parent) 0045 : DavJobBase(new DavPrincipalSearchJobPrivate, parent) 0046 { 0047 Q_D(DavPrincipalSearchJob); 0048 d->mUrl = url; 0049 d->mType = type; 0050 d->mFilter = filter; 0051 } 0052 0053 void DavPrincipalSearchJob::fetchProperty(const QString &name, const QString &ns) 0054 { 0055 Q_D(DavPrincipalSearchJob); 0056 d->mFetchProperties.push_back({!ns.isEmpty() ? ns : QStringLiteral("DAV:"), name}); 0057 } 0058 0059 DavUrl DavPrincipalSearchJob::davUrl() const 0060 { 0061 Q_D(const DavPrincipalSearchJob); 0062 return d->mUrl; 0063 } 0064 0065 void DavPrincipalSearchJob::start() 0066 { 0067 Q_D(DavPrincipalSearchJob); 0068 /* 0069 * The first step is to try to locate the URL that contains the principals. 0070 * This is done with a PROPFIND request and a XML like this: 0071 * <?xml version="1.0" encoding="utf-8" ?> 0072 * <D:propfind xmlns:D="DAV:"> 0073 * <D:prop> 0074 * <D:principal-collection-set/> 0075 * </D:prop> 0076 * </D:propfind> 0077 */ 0078 QDomDocument query; 0079 0080 QDomElement propfind = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("propfind")); 0081 query.appendChild(propfind); 0082 0083 QDomElement prop = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop")); 0084 propfind.appendChild(prop); 0085 0086 QDomElement principalCollectionSet = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("principal-collection-set")); 0087 prop.appendChild(principalCollectionSet); 0088 0089 KIO::DavJob *job = DavManager::self()->createPropFindJob(d->mUrl.url(), query.toString()); 0090 job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true")); 0091 connect(job, &KIO::DavJob::result, this, [d](KJob *job) { 0092 d->principalCollectionSetSearchFinished(job); 0093 }); 0094 job->start(); 0095 } 0096 0097 void DavPrincipalSearchJobPrivate::principalCollectionSetSearchFinished(KJob *job) 0098 { 0099 KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job); 0100 const QString responseCodeStr = davJob->queryMetaData(QStringLiteral("responsecode")); 0101 const int responseCode = responseCodeStr.isEmpty() ? 0 : responseCodeStr.toInt(); 0102 // KIO::DavJob does not set error() even if the HTTP status code is a 4xx or a 5xx 0103 if (davJob->error() || (responseCode >= 400 && responseCode < 600)) { 0104 setLatestResponseCode(responseCode); 0105 setError(ERR_PROBLEM_WITH_REQUEST); 0106 setJobErrorText(davJob->errorText()); 0107 setJobError(davJob->error()); 0108 setErrorTextFromDavError(); 0109 0110 emitResult(); 0111 return; 0112 } 0113 0114 if (job->error()) { 0115 setError(job->error()); 0116 setErrorText(job->errorText()); 0117 emitResult(); 0118 return; 0119 } 0120 0121 /* 0122 * Extract information from a document like the following: 0123 * 0124 * <?xml version="1.0" encoding="utf-8" ?> 0125 * <D:multistatus xmlns:D="DAV:"> 0126 * <D:response> 0127 * <D:href>http://www.example.com/papers/</D:href> 0128 * <D:propstat> 0129 * <D:prop> 0130 * <D:principal-collection-set> 0131 * <D:href>http://www.example.com/acl/users/</D:href> 0132 * <D:href>http://www.example.com/acl/groups/</D:href> 0133 * </D:principal-collection-set> 0134 * </D:prop> 0135 * <D:status>HTTP/1.1 200 OK</D:status> 0136 * </D:propstat> 0137 * </D:response> 0138 * </D:multistatus> 0139 */ 0140 0141 QDomDocument document; 0142 document.setContent(davJob->responseData(), QDomDocument::ParseOption::UseNamespaceProcessing); 0143 QDomElement documentElement = document.documentElement(); 0144 0145 QDomElement responseElement = Utils::firstChildElementNS(documentElement, QStringLiteral("DAV:"), QStringLiteral("response")); 0146 if (responseElement.isNull()) { 0147 emitResult(); 0148 return; 0149 } 0150 0151 // check for the valid propstat, without giving up on first error 0152 QDomElement propstatElement; 0153 { 0154 const QDomNodeList propstats = responseElement.elementsByTagNameNS(QStringLiteral("DAV:"), QStringLiteral("propstat")); 0155 for (int i = 0; i < propstats.length(); ++i) { 0156 const QDomElement propstatCandidate = propstats.item(i).toElement(); 0157 const QDomElement statusElement = Utils::firstChildElementNS(propstatCandidate, QStringLiteral("DAV:"), QStringLiteral("status")); 0158 if (statusElement.text().contains(QLatin1String("200"))) { 0159 propstatElement = propstatCandidate; 0160 } 0161 } 0162 } 0163 0164 if (propstatElement.isNull()) { 0165 emitResult(); 0166 return; 0167 } 0168 0169 QDomElement propElement = Utils::firstChildElementNS(propstatElement, QStringLiteral("DAV:"), QStringLiteral("prop")); 0170 if (propElement.isNull()) { 0171 emitResult(); 0172 return; 0173 } 0174 0175 QDomElement principalCollectionSetElement = Utils::firstChildElementNS(propElement, QStringLiteral("DAV:"), QStringLiteral("principal-collection-set")); 0176 if (principalCollectionSetElement.isNull()) { 0177 emitResult(); 0178 return; 0179 } 0180 0181 QDomNodeList hrefNodes = principalCollectionSetElement.elementsByTagNameNS(QStringLiteral("DAV:"), QStringLiteral("href")); 0182 for (int i = 0; i < hrefNodes.size(); ++i) { 0183 QDomElement hrefElement = hrefNodes.at(i).toElement(); 0184 QString href = hrefElement.text(); 0185 0186 QUrl url = mUrl.url(); 0187 if (href.startsWith(QLatin1Char('/'))) { 0188 // href is only a path, use request url to complete 0189 url.setPath(href, QUrl::TolerantMode); 0190 } else { 0191 // href is a complete url 0192 QUrl tmpUrl(href); 0193 tmpUrl.setUserName(url.userName()); 0194 tmpUrl.setPassword(url.password()); 0195 url = tmpUrl; 0196 } 0197 0198 QDomDocument principalPropertySearchQuery; 0199 buildReportQuery(principalPropertySearchQuery); 0200 KIO::DavJob *reportJob = DavManager::self()->createReportJob(url, principalPropertySearchQuery.toString()); 0201 reportJob->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true")); 0202 QObject::connect(reportJob, &KIO::DavJob::result, q_ptr, [this](KJob *job) { 0203 principalPropertySearchFinished(job); 0204 }); 0205 ++mPrincipalPropertySearchSubJobCount; 0206 reportJob->start(); 0207 } 0208 } 0209 0210 void DavPrincipalSearchJobPrivate::principalPropertySearchFinished(KJob *job) 0211 { 0212 --mPrincipalPropertySearchSubJobCount; 0213 0214 if (job->error() && !mPrincipalPropertySearchSubJobSuccessful) { 0215 setError(job->error()); 0216 setErrorText(job->errorText()); 0217 if (mPrincipalPropertySearchSubJobCount == 0) { 0218 emitResult(); 0219 } 0220 return; 0221 } 0222 0223 KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job); 0224 0225 const int responseCode = davJob->queryMetaData(QStringLiteral("responsecode")).toInt(); 0226 0227 if (responseCode > 499 && responseCode < 600 && !mPrincipalPropertySearchSubJobSuccessful) { 0228 // Server-side error, unrecoverable 0229 setLatestResponseCode(responseCode); 0230 setError(ERR_SERVER_UNRECOVERABLE); 0231 setJobErrorText(davJob->errorText()); 0232 setJobError(davJob->error()); 0233 setErrorTextFromDavError(); 0234 if (mPrincipalPropertySearchSubJobCount == 0) { 0235 emitResult(); 0236 } 0237 return; 0238 } else if (responseCode > 399 && responseCode < 500 && !mPrincipalPropertySearchSubJobSuccessful) { 0239 setLatestResponseCode(responseCode); 0240 setError(ERR_PROBLEM_WITH_REQUEST); 0241 setJobErrorText(davJob->errorText()); 0242 setJobError(davJob->error()); 0243 setErrorTextFromDavError(); 0244 0245 if (mPrincipalPropertySearchSubJobCount == 0) { 0246 emitResult(); 0247 } 0248 return; 0249 } 0250 0251 if (!mPrincipalPropertySearchSubJobSuccessful) { 0252 setError(0); // nope, everything went fine 0253 mPrincipalPropertySearchSubJobSuccessful = true; 0254 } 0255 0256 /* 0257 * Extract infos from a document like the following: 0258 * <?xml version="1.0" encoding="utf-8" ?> 0259 * <D:multistatus xmlns:D="DAV:" xmlns:B="http://BigCorp.com/ns/"> 0260 * <D:response> 0261 * <D:href>http://www.example.com/users/jdoe</D:href> 0262 * <D:propstat> 0263 * <D:prop> 0264 * <D:displayname>John Doe</D:displayname> 0265 * </D:prop> 0266 * <D:status>HTTP/1.1 200 OK</D:status> 0267 * </D:propstat> 0268 * </D:multistatus> 0269 */ 0270 0271 QDomDocument document; 0272 document.setContent(davJob->responseData(), QDomDocument::ParseOption::UseNamespaceProcessing); 0273 const QDomElement documentElement = document.documentElement(); 0274 0275 QDomElement responseElement = Utils::firstChildElementNS(documentElement, QStringLiteral("DAV:"), QStringLiteral("response")); 0276 if (responseElement.isNull()) { 0277 if (mPrincipalPropertySearchSubJobCount == 0) { 0278 emitResult(); 0279 } 0280 return; 0281 } 0282 0283 // check for the valid propstat, without giving up on first error 0284 QDomElement propstatElement; 0285 { 0286 const QDomNodeList propstats = responseElement.elementsByTagNameNS(QStringLiteral("DAV:"), QStringLiteral("propstat")); 0287 const int propStatsEnd(propstats.length()); 0288 for (int i = 0; i < propStatsEnd; ++i) { 0289 const QDomElement propstatCandidate = propstats.item(i).toElement(); 0290 const QDomElement statusElement = Utils::firstChildElementNS(propstatCandidate, QStringLiteral("DAV:"), QStringLiteral("status")); 0291 if (statusElement.text().contains(QLatin1String("200"))) { 0292 propstatElement = propstatCandidate; 0293 } 0294 } 0295 } 0296 0297 if (propstatElement.isNull()) { 0298 if (mPrincipalPropertySearchSubJobCount == 0) { 0299 emitResult(); 0300 } 0301 return; 0302 } 0303 0304 QDomElement propElement = Utils::firstChildElementNS(propstatElement, QStringLiteral("DAV:"), QStringLiteral("prop")); 0305 if (propElement.isNull()) { 0306 if (mPrincipalPropertySearchSubJobCount == 0) { 0307 emitResult(); 0308 } 0309 return; 0310 } 0311 0312 // All requested properties are now under propElement, so let's find them 0313 for (const auto &[propNS, propName] : mFetchProperties) { 0314 const QDomNodeList fetchNodes = propElement.elementsByTagNameNS(propNS, propName); 0315 mResults.reserve(mResults.size() + fetchNodes.size()); 0316 for (int i = 0; i < fetchNodes.size(); ++i) { 0317 const QDomElement fetchElement = fetchNodes.at(i).toElement(); 0318 mResults.push_back({propNS, propName, fetchElement.text()}); 0319 } 0320 } 0321 0322 if (mPrincipalPropertySearchSubJobCount == 0) { 0323 emitResult(); 0324 } 0325 } 0326 0327 QList<DavPrincipalSearchJob::Result> DavPrincipalSearchJob::results() const 0328 { 0329 Q_D(const DavPrincipalSearchJob); 0330 return d->mResults; 0331 } 0332 0333 void DavPrincipalSearchJobPrivate::buildReportQuery(QDomDocument &query) const 0334 { 0335 /* 0336 * Build a document like the following, where XXX will 0337 * be replaced by the properties the user want to fetch: 0338 * 0339 * <?xml version="1.0" encoding="utf-8" ?> 0340 * <D:principal-property-search xmlns:D="DAV:"> 0341 * <D:property-search> 0342 * <D:prop> 0343 * <D:displayname/> 0344 * </D:prop> 0345 * <D:match>FILTER</D:match> 0346 * </D:property-search> 0347 * <D:prop> 0348 * XXX 0349 * </D:prop> 0350 * </D:principal-property-search> 0351 */ 0352 0353 QDomElement principalPropertySearch = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("principal-property-search")); 0354 query.appendChild(principalPropertySearch); 0355 0356 QDomElement propertySearch = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("property-search")); 0357 principalPropertySearch.appendChild(propertySearch); 0358 0359 QDomElement prop = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop")); 0360 propertySearch.appendChild(prop); 0361 0362 if (mType == DavPrincipalSearchJob::DisplayName) { 0363 QDomElement displayName = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("displayname")); 0364 prop.appendChild(displayName); 0365 } else if (mType == DavPrincipalSearchJob::EmailAddress) { 0366 QDomElement calendarUserAddressSet = 0367 query.createElementNS(QStringLiteral("urn:ietf:params:xml:ns:caldav"), QStringLiteral("calendar-user-address-set")); 0368 prop.appendChild(calendarUserAddressSet); 0369 // QDomElement hrefElement = query.createElementNS( "DAV:", "href" ); 0370 // prop.appendChild( hrefElement ); 0371 } 0372 0373 QDomElement match = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("match")); 0374 propertySearch.appendChild(match); 0375 0376 QDomText propFilter = query.createTextNode(mFilter); 0377 match.appendChild(propFilter); 0378 0379 prop = query.createElementNS(QStringLiteral("DAV:"), QStringLiteral("prop")); 0380 principalPropertySearch.appendChild(prop); 0381 0382 for (const auto &[propNS, propName] : mFetchProperties) { 0383 QDomElement elem = query.createElementNS(propNS, propName); 0384 prop.appendChild(elem); 0385 } 0386 } 0387 0388 #include "moc_davprincipalsearchjob.cpp"