File indexing completed on 2024-04-21 03:53:53

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"