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

0001 /*
0002     SPDX-FileCopyrightText: 2010 Tobias Koenig <tokoe@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "davcollectionsfetchjob.h"
0008 #include "davjobbase_p.h"
0009 
0010 #include "daverror.h"
0011 #include "davmanager_p.h"
0012 #include "davprincipalhomesetsfetchjob.h"
0013 #include "davprotocolbase_p.h"
0014 #include "utils_p.h"
0015 
0016 #include "libkdav_debug.h"
0017 #include <KIO/DavJob>
0018 #include <KIO/Job>
0019 
0020 #include <QBuffer>
0021 #include <QColor>
0022 
0023 using namespace KDAV;
0024 
0025 namespace KDAV
0026 {
0027 class DavCollectionsFetchJobPrivate : public DavJobBasePrivate
0028 {
0029 public:
0030     void principalFetchFinished(KJob *job);
0031     void collectionsFetchFinished(KJob *job);
0032     void doCollectionsFetch(const QUrl &url);
0033     void subjobFinished();
0034 
0035     DavUrl mUrl;
0036     DavCollection::List mCollections;
0037     uint mSubJobCount = 0;
0038 
0039     Q_DECLARE_PUBLIC(DavCollectionsFetchJob)
0040 };
0041 }
0042 
0043 DavCollectionsFetchJob::DavCollectionsFetchJob(const DavUrl &url, QObject *parent)
0044     : DavJobBase(new DavCollectionsFetchJobPrivate, parent)
0045 {
0046     Q_D(DavCollectionsFetchJob);
0047     d->mUrl = url;
0048 }
0049 
0050 void DavCollectionsFetchJob::start()
0051 {
0052     Q_D(DavCollectionsFetchJob);
0053     if (DavManager::davProtocol(d->mUrl.protocol())->supportsPrincipals()) {
0054         DavPrincipalHomeSetsFetchJob *job = new DavPrincipalHomeSetsFetchJob(d->mUrl);
0055         connect(job, &DavPrincipalHomeSetsFetchJob::result, this, [d](KJob *job) {
0056             d->principalFetchFinished(job);
0057         });
0058         job->start();
0059     } else {
0060         d->doCollectionsFetch(d->mUrl.url());
0061     }
0062 }
0063 
0064 DavCollection::List DavCollectionsFetchJob::collections() const
0065 {
0066     Q_D(const DavCollectionsFetchJob);
0067     return d->mCollections;
0068 }
0069 
0070 DavUrl DavCollectionsFetchJob::davUrl() const
0071 {
0072     Q_D(const DavCollectionsFetchJob);
0073     return d->mUrl;
0074 }
0075 
0076 void DavCollectionsFetchJobPrivate::doCollectionsFetch(const QUrl &url)
0077 {
0078     ++mSubJobCount;
0079 
0080     const QDomDocument collectionQuery = DavManager::davProtocol(mUrl.protocol())->collectionsQuery()->buildQuery();
0081 
0082     KIO::DavJob *job = DavManager::self()->createPropFindJob(url, collectionQuery.toString());
0083     QObject::connect(job, &KIO::DavJob::result, q_ptr, [this](KJob *job) {
0084         collectionsFetchFinished(job);
0085     });
0086     job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
0087 }
0088 
0089 void DavCollectionsFetchJobPrivate::principalFetchFinished(KJob *job)
0090 {
0091     const DavPrincipalHomeSetsFetchJob *davJob = qobject_cast<DavPrincipalHomeSetsFetchJob *>(job);
0092 
0093     if (davJob->error()) {
0094         if (davJob->canRetryLater()) {
0095             // If we have a non-persistent HTTP error code then this may mean that
0096             // the URL was not a principal URL. Retry as if it were a calendar URL.
0097             qCDebug(KDAV_LOG) << job->errorText();
0098             doCollectionsFetch(mUrl.url());
0099         } else {
0100             // Just give up here.
0101             setDavError(davJob->davError());
0102             setErrorTextFromDavError();
0103             emitResult();
0104         }
0105 
0106         return;
0107     }
0108 
0109     const QStringList homeSets = davJob->homeSets();
0110     qCDebug(KDAV_LOG) << "Found" << homeSets.size() << "homesets";
0111     qCDebug(KDAV_LOG) << homeSets;
0112 
0113     if (homeSets.isEmpty()) {
0114         // Same as above, retry as if it were a calendar URL.
0115         doCollectionsFetch(mUrl.url());
0116         return;
0117     }
0118 
0119     for (const QString &homeSet : homeSets) {
0120         QUrl url = mUrl.url();
0121 
0122         if (homeSet.startsWith(QLatin1Char('/'))) {
0123             // homeSet is only a path, use request url to complete
0124             url.setPath(homeSet, QUrl::TolerantMode);
0125         } else {
0126             // homeSet is a complete url
0127             QUrl tmpUrl(homeSet);
0128             tmpUrl.setUserName(url.userName());
0129             tmpUrl.setPassword(url.password());
0130             url = tmpUrl;
0131         }
0132 
0133         doCollectionsFetch(url);
0134     }
0135 }
0136 
0137 void DavCollectionsFetchJobPrivate::collectionsFetchFinished(KJob *job)
0138 {
0139     Q_Q(DavCollectionsFetchJob);
0140     KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
0141     const QString responseCodeStr = davJob->queryMetaData(QStringLiteral("responsecode"));
0142     const int responseCode = responseCodeStr.isEmpty() ? 0 : responseCodeStr.toInt();
0143 
0144     // KIO::DavJob does not set error() even if the HTTP status code is a 4xx or a 5xx
0145     if (davJob->error() || (responseCode >= 400 && responseCode < 600)) {
0146         if (davJob->url() != mUrl.url()) {
0147             // Retry as if the initial URL was a calendar URL.
0148             // We can end up here when retrieving a homeset on
0149             // which a PROPFIND resulted in an error
0150             doCollectionsFetch(mUrl.url());
0151             --mSubJobCount;
0152             return;
0153         }
0154 
0155         setLatestResponseCode(responseCode);
0156         setError(ERR_PROBLEM_WITH_REQUEST);
0157         setJobErrorText(davJob->errorText());
0158         setJobError(davJob->error());
0159         setErrorTextFromDavError();
0160     } else {
0161         // For use in the collectionDiscovered() signal
0162         QUrl _jobUrl = mUrl.url();
0163         _jobUrl.setUserInfo(QString());
0164         const QString jobUrl = _jobUrl.toDisplayString();
0165 
0166         // Validate that we got a valid PROPFIND response
0167         QDomDocument response;
0168         response.setContent(davJob->responseData(), QDomDocument::ParseOption::UseNamespaceProcessing);
0169         QDomElement rootElement = response.documentElement();
0170         if (rootElement.tagName().compare(QLatin1String("multistatus"), Qt::CaseInsensitive) != 0) {
0171             setError(ERR_COLLECTIONFETCH);
0172             setErrorTextFromDavError();
0173             subjobFinished();
0174             return;
0175         }
0176 
0177         QByteArray resp = davJob->responseData();
0178         QDomDocument document;
0179         if (!document.setContent(resp, QDomDocument::ParseOption::UseNamespaceProcessing)) {
0180             setError(ERR_COLLECTIONFETCH);
0181             setErrorTextFromDavError();
0182             subjobFinished();
0183             return;
0184         }
0185 
0186         if (!q->error()) {
0187             /*
0188              * Extract information from a document like the following:
0189              *
0190              * <responses>
0191              *   <response xmlns="DAV:">
0192              *     <href xmlns="DAV:">/caldav.php/test1.user/home/</href>
0193              *     <propstat xmlns="DAV:">
0194              *       <prop xmlns="DAV:">
0195              *         <C:supported-calendar-component-set xmlns:C="urn:ietf:params:xml:ns:caldav">
0196              *           <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VEVENT"/>
0197              *           <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VTODO"/>
0198              *           <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VJOURNAL"/>
0199              *           <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VTIMEZONE"/>
0200              *           <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VFREEBUSY"/>
0201              *         </C:supported-calendar-component-set>
0202              *         <resourcetype xmlns="DAV:">
0203              *           <collection xmlns="DAV:"/>
0204              *           <C:calendar xmlns:C="urn:ietf:params:xml:ns:caldav"/>
0205              *           <C:schedule-calendar xmlns:C="urn:ietf:params:xml:ns:caldav"/>
0206              *         </resourcetype>
0207              *         <displayname xmlns="DAV:">Test1 User</displayname>
0208              *         <current-user-privilege-set xmlns="DAV:">
0209              *           <privilege xmlns="DAV:">
0210              *             <read xmlns="DAV:"/>
0211              *           </privilege>
0212              *         </current-user-privilege-set>
0213              *         <getctag xmlns="http://calendarserver.org/ns/">12345</getctag>
0214              *       </prop>
0215              *       <status xmlns="DAV:">HTTP/1.1 200 OK</status>
0216              *     </propstat>
0217              *   </response>
0218              * </responses>
0219              */
0220 
0221             const QDomElement responsesElement = document.documentElement();
0222 
0223             QDomElement responseElement = Utils::firstChildElementNS(responsesElement, QStringLiteral("DAV:"), QStringLiteral("response"));
0224             while (!responseElement.isNull()) {
0225                 QDomElement propstatElement;
0226 
0227                 // check for the valid propstat, without giving up on first error
0228                 {
0229                     const QDomNodeList propstats = responseElement.elementsByTagNameNS(QStringLiteral("DAV:"), QStringLiteral("propstat"));
0230                     for (int i = 0; i < propstats.length(); ++i) {
0231                         const QDomElement propstatCandidate = propstats.item(i).toElement();
0232                         const QDomElement statusElement = Utils::firstChildElementNS(propstatCandidate, QStringLiteral("DAV:"), QStringLiteral("status"));
0233                         if (statusElement.text().contains(QLatin1String("200"))) {
0234                             propstatElement = propstatCandidate;
0235                         }
0236                     }
0237                 }
0238 
0239                 if (propstatElement.isNull()) {
0240                     responseElement = Utils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
0241                     continue;
0242                 }
0243 
0244                 // extract url
0245                 const QDomElement hrefElement = Utils::firstChildElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("href"));
0246                 if (hrefElement.isNull()) {
0247                     responseElement = Utils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
0248                     continue;
0249                 }
0250 
0251                 QString href = hrefElement.text();
0252                 if (!href.endsWith(QLatin1Char('/'))) {
0253                     href.append(QLatin1Char('/'));
0254                 }
0255 
0256                 QUrl url = davJob->url();
0257                 url.setUserInfo(QString());
0258                 if (href.startsWith(QLatin1Char('/'))) {
0259                     // href is only a path, use request url to complete
0260                     url.setPath(href, QUrl::TolerantMode);
0261                 } else {
0262                     // href is a complete url
0263                     url = QUrl::fromUserInput(href);
0264                 }
0265 
0266                 // don't add this resource if it has already been detected
0267                 bool alreadySeen = false;
0268                 for (const DavCollection &seen : std::as_const(mCollections)) {
0269                     if (seen.url().toDisplayString() == url.toDisplayString()) {
0270                         alreadySeen = true;
0271                     }
0272                 }
0273                 if (alreadySeen) {
0274                     responseElement = Utils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
0275                     continue;
0276                 }
0277 
0278                 // extract display name
0279                 const QDomElement propElement = Utils::firstChildElementNS(propstatElement, QStringLiteral("DAV:"), QStringLiteral("prop"));
0280                 if (!DavManager::davProtocol(mUrl.protocol())->containsCollection(propElement)) {
0281                     responseElement = Utils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
0282                     continue;
0283                 }
0284                 const QDomElement displaynameElement = Utils::firstChildElementNS(propElement, QStringLiteral("DAV:"), QStringLiteral("displayname"));
0285                 const QString displayName = displaynameElement.text();
0286 
0287                 // Extract CTag
0288                 const QDomElement CTagElement = Utils::firstChildElementNS(propElement, //
0289                                                                            QStringLiteral("http://calendarserver.org/ns/"),
0290                                                                            QStringLiteral("getctag"));
0291                 QString CTag;
0292                 if (!CTagElement.isNull()) {
0293                     CTag = CTagElement.text();
0294                 }
0295 
0296                 // extract calendar color if provided
0297                 const QDomElement colorElement = Utils::firstChildElementNS(propElement, //
0298                                                                             QStringLiteral("http://apple.com/ns/ical/"),
0299                                                                             QStringLiteral("calendar-color"));
0300                 QColor color;
0301                 if (!colorElement.isNull()) {
0302                     QString colorValue = colorElement.text();
0303                     if (QColor::isValidColorName(colorValue)) {
0304                         // Color is either #RRGGBBAA or #RRGGBB but QColor expects #AARRGGBB
0305                         // so we put the AA in front if the string's length is 9.
0306                         if (colorValue.size() == 9) {
0307                             QString fixedColorValue = QStringLiteral("#") + colorValue.mid(7, 2) + colorValue.mid(1, 6);
0308                             color = QColor::fromString(fixedColorValue);
0309                         } else {
0310                             color = QColor::fromString(colorValue);
0311                         }
0312                     }
0313                 }
0314 
0315                 // extract allowed content types
0316                 const DavCollection::ContentTypes contentTypes = DavManager::davProtocol(mUrl.protocol())->collectionContentTypes(propstatElement);
0317 
0318                 auto _url = url;
0319                 _url.setUserInfo(mUrl.url().userInfo());
0320                 DavCollection collection(DavUrl(_url, mUrl.protocol()), displayName, contentTypes);
0321 
0322                 collection.setCTag(CTag);
0323                 if (color.isValid()) {
0324                     collection.setColor(color);
0325                 }
0326 
0327                 // extract privileges
0328                 const QDomElement currentPrivsElement = Utils::firstChildElementNS(propElement, //
0329                                                                                    QStringLiteral("DAV:"),
0330                                                                                    QStringLiteral("current-user-privilege-set"));
0331                 if (currentPrivsElement.isNull()) {
0332                     // Assume that we have all privileges
0333                     collection.setPrivileges(KDAV::All);
0334                 } else {
0335                     Privileges privileges = Utils::extractPrivileges(currentPrivsElement);
0336                     collection.setPrivileges(privileges);
0337                 }
0338 
0339                 qCDebug(KDAV_LOG) << url.toDisplayString() << "PRIVS: " << collection.privileges();
0340                 mCollections << collection;
0341                 Q_EMIT q->collectionDiscovered(mUrl.protocol(), url.toDisplayString(), jobUrl);
0342 
0343                 responseElement = Utils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
0344             }
0345         }
0346     }
0347 
0348     subjobFinished();
0349 }
0350 
0351 void DavCollectionsFetchJobPrivate::subjobFinished()
0352 {
0353     if (--mSubJobCount == 0) {
0354         emitResult();
0355     }
0356 }
0357 
0358 #include "moc_davcollectionsfetchjob.cpp"