File indexing completed on 2024-05-26 05:17:00

0001 /*
0002     Copyright (c) 2010 Tobias Koenig <tokoe@kde.org>
0003 
0004     This program is free software; you can redistribute it and/or modify
0005     it under the terms of the GNU General Public License as published by
0006     the Free Software Foundation; either version 2 of the License, or
0007     (at your option) any later version.
0008 
0009     This program is distributed in the hope that it will be useful,
0010     but WITHOUT ANY WARRANTY; without even the implied warranty of
0011     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
0012     GNU General Public License for more details.
0013 
0014     You should have received a copy of the GNU General Public License
0015     along with this program; if not, write to the Free Software
0016     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
0017 */
0018 
0019 #include "davcollectionsfetchjob.h"
0020 
0021 #include "davmanager.h"
0022 #include "davprincipalhomesetsfetchjob.h"
0023 #include "davcollectionfetchjob.h"
0024 #include "davprotocolbase.h"
0025 #include "utils.h"
0026 #include "daverror.h"
0027 #include "davjob.h"
0028 
0029 #include "libkdav2_debug.h"
0030 
0031 #include <QtCore/QBuffer>
0032 #include <QtXmlPatterns/QXmlQuery>
0033 
0034 using namespace KDAV2;
0035 
0036 DavCollectionsFetchJob::DavCollectionsFetchJob(const DavUrl &url, QObject *parent)
0037     : DavJobBase(parent), mUrl(url), mSubJobCount(0)
0038 {
0039 }
0040 
0041 void DavCollectionsFetchJob::start()
0042 {
0043     if (DavManager::self()->davProtocol(mUrl.protocol())->supportsPrincipals()) {
0044         DavPrincipalHomeSetsFetchJob *job = new DavPrincipalHomeSetsFetchJob(mUrl);
0045         connect(job, &DavPrincipalHomeSetsFetchJob::result, this, &DavCollectionsFetchJob::principalFetchFinished);
0046         job->start();
0047     } else {
0048         doCollectionsFetch(mUrl.url());
0049     }
0050 }
0051 
0052 DavCollection::List DavCollectionsFetchJob::collections() const
0053 {
0054     return mCollections;
0055 }
0056 
0057 DavUrl DavCollectionsFetchJob::davUrl() const
0058 {
0059     return mUrl;
0060 }
0061 
0062 void DavCollectionsFetchJob::doCollectionsFetch(const QUrl &url)
0063 {
0064     ++mSubJobCount;
0065 
0066     const QDomDocument collectionQuery = DavManager::self()->davProtocol(mUrl.protocol())->collectionsQuery()->buildQuery();
0067 
0068     auto job = DavManager::self()->createPropFindJob(url, collectionQuery);
0069     connect(job, &DavJob::result, this, &DavCollectionsFetchJob::collectionsFetchFinished);
0070 }
0071 
0072 void DavCollectionsFetchJob::principalFetchFinished(KJob *job)
0073 {
0074     const DavPrincipalHomeSetsFetchJob *davJob = qobject_cast<DavPrincipalHomeSetsFetchJob *>(job);
0075     if (davJob->error()) {
0076         // Just give up here.
0077         setDavError(davJob->davError());
0078         emitResult();
0079 
0080         return;
0081     }
0082 
0083     const QStringList homeSets = davJob->homeSets();
0084     qCDebug(KDAV2_LOG) << "Found " << homeSets.size() << " homesets\n " << homeSets;
0085 
0086     //Update the url in case of redirects
0087     mUrl.setUrl(davJob->url());
0088 
0089     if (homeSets.isEmpty()) {
0090         // Same as above, retry as if it were a calendar URL.
0091         doCollectionsFetch(mUrl.url());
0092         return;
0093     }
0094 
0095     foreach (const QString &homeSet, homeSets) {
0096         QUrl url = mUrl.url();
0097 
0098         if (homeSet.startsWith(QLatin1Char('/'))) {
0099             // homeSet is only a path, use request url to complete
0100             url.setPath(homeSet, QUrl::TolerantMode);
0101         } else {
0102             // homeSet is a complete url
0103             QUrl tmpUrl(homeSet);
0104             tmpUrl.setUserName(url.userName());
0105             tmpUrl.setPassword(url.password());
0106             url = tmpUrl;
0107         }
0108 
0109         doCollectionsFetch(url);
0110     }
0111 }
0112 
0113 void DavCollectionsFetchJob::collectionsFetchFinished(KJob *job)
0114 {
0115     auto davJob = static_cast<DavJob *>(job);
0116 
0117     if (davJob->error()) {
0118         setErrorFromJob(davJob);
0119     } else {
0120         // For use in the collectionDiscovered() signal
0121         QUrl _jobUrl = mUrl.url();
0122         _jobUrl.setUserInfo(QString());
0123         const QString jobUrl = _jobUrl.toDisplayString();
0124 
0125         // Validate that we got a valid PROPFIND response
0126         QDomElement rootElement = davJob->response().documentElement();
0127         if (rootElement.localName().compare(QStringLiteral("multistatus"), Qt::CaseInsensitive) != 0) {
0128             setError(ERR_COLLECTIONFETCH);
0129             setErrorTextFromDavError();
0130             subjobFinished();
0131             return;
0132         }
0133 
0134         QByteArray resp(davJob->response().toByteArray());
0135         QBuffer buffer(&resp);
0136         buffer.open(QIODevice::ReadOnly);
0137 
0138         QXmlQuery xquery;
0139         if (!xquery.setFocus(&buffer)) {
0140             setError(ERR_COLLECTIONFETCH_XQUERY_SETFOCUS);
0141             setErrorTextFromDavError();
0142             subjobFinished();
0143             return;
0144         }
0145 
0146         xquery.setQuery(DavManager::self()->davProtocol(mUrl.protocol())->collectionsXQuery());
0147         if (!xquery.isValid()) {
0148             setError(ERR_COLLECTIONFETCH_XQUERY_INVALID);
0149             setErrorTextFromDavError();
0150             subjobFinished();
0151             return;
0152         }
0153 
0154         QString responsesStr;
0155         xquery.evaluateTo(&responsesStr);
0156         responsesStr.prepend(QStringLiteral("<responses>"));
0157         responsesStr.append(QStringLiteral("</responses>"));
0158 
0159         QDomDocument document;
0160         if (!document.setContent(responsesStr, true)) {
0161             setError(ERR_COLLECTIONFETCH);
0162             setErrorTextFromDavError();
0163             subjobFinished();
0164             return;
0165         }
0166 
0167         if (!error()) {
0168             /*
0169              * Extract information from a document like the following:
0170              *
0171              * <responses>
0172              *   <response xmlns="DAV:">
0173              *     <href xmlns="DAV:">/caldav.php/test1.user/home/</href>
0174              *     <propstat xmlns="DAV:">
0175              *       <prop xmlns="DAV:">
0176              *         <C:supported-calendar-component-set xmlns:C="urn:ietf:params:xml:ns:caldav">
0177              *           <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VEVENT"/>
0178              *           <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VTODO"/>
0179              *           <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VJOURNAL"/>
0180              *           <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VTIMEZONE"/>
0181              *           <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VFREEBUSY"/>
0182              *         </C:supported-calendar-component-set>
0183              *         <resourcetype xmlns="DAV:">
0184              *           <collection xmlns="DAV:"/>
0185              *           <C:calendar xmlns:C="urn:ietf:params:xml:ns:caldav"/>
0186              *           <C:schedule-calendar xmlns:C="urn:ietf:params:xml:ns:caldav"/>
0187              *         </resourcetype>
0188              *         <displayname xmlns="DAV:">Test1 User</displayname>
0189              *         <current-user-privilege-set xmlns="DAV:">
0190              *           <privilege xmlns="DAV:">
0191              *             <read xmlns="DAV:"/>
0192              *           </privilege>
0193              *         </current-user-privilege-set>
0194              *         <getctag xmlns="http://calendarserver.org/ns/">12345</getctag>
0195              *       </prop>
0196              *       <status xmlns="DAV:">HTTP/1.1 200 OK</status>
0197              *     </propstat>
0198              *   </response>
0199              * </responses>
0200              */
0201 
0202             const QDomElement responsesElement = document.documentElement();
0203 
0204             QDomElement responseElement = Utils::firstChildElementNS(
0205                 responsesElement, QStringLiteral("DAV:"), QStringLiteral("response"));
0206             while (!responseElement.isNull()) {
0207 
0208                 DavCollection collection;
0209                 if (!Utils::extractCollection(responseElement, mUrl, collection)) {
0210                     continue;
0211                 }
0212 
0213                 QUrl url = collection.url().url();
0214 
0215                 // don't add this resource if it has already been detected
0216                 bool alreadySeen = false;
0217                 foreach (const DavCollection &seen, mCollections) {
0218                     if (seen.url().toDisplayString() == url.toDisplayString()) {
0219                         alreadySeen = true;
0220                     }
0221                 }
0222                 if (alreadySeen) {
0223                     responseElement = Utils::nextSiblingElementNS(
0224                         responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
0225                     continue;
0226                 }
0227 
0228                 bool protocolSupportsCTags = DavManager::self()->davProtocol(mUrl.protocol())->supportsCTags();
0229                 if (protocolSupportsCTags && collection.CTag() == "") {
0230                     qCDebug(KDAV2_LOG) << "No CTag found for"
0231                         << collection.url().url().toDisplayString()
0232                         << "from the home set, trying from the direct URL";
0233                     refreshIndividualCollection(collection);
0234 
0235                     responseElement = Utils::nextSiblingElementNS(
0236                         responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
0237                     continue;
0238                 }
0239 
0240                 mCollections << collection;
0241                 Q_EMIT collectionDiscovered(mUrl.protocol(), url.toDisplayString(), jobUrl);
0242 
0243                 responseElement = Utils::nextSiblingElementNS(
0244                     responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
0245             }
0246         }
0247     }
0248 
0249     subjobFinished();
0250 }
0251 
0252 // This is a workaroud for Google who doesn't support providing the CTag
0253 // directly from the home set. We refresh the collections individually from
0254 // their own URLs, but only if we haven't found a CTag with the home set
0255 // request.
0256 void DavCollectionsFetchJob::refreshIndividualCollection(const DavCollection &collection)
0257 {
0258     ++mSubJobCount;
0259     auto individualFetchJob = new DavCollectionFetchJob(collection, this);
0260     connect(individualFetchJob, &DavCollectionFetchJob::result, this, &DavCollectionsFetchJob::individualCollectionRefreshed);
0261     individualFetchJob->start();
0262 }
0263 
0264 void DavCollectionsFetchJob::individualCollectionRefreshed(KJob *job)
0265 {
0266     const auto *davJob = qobject_cast<DavCollectionFetchJob *>(job);
0267 
0268     if (davJob->error()) {
0269         setDavError(davJob->davError());
0270         emitResult();
0271         return;
0272     }
0273 
0274     qCDebug(KDAV2_LOG) << "Collection"
0275         << davJob->collection().url().url().toDisplayString() << "refreshed";
0276 
0277     if (davJob->collection().CTag() == "") {
0278         qWarning() << "Collection with an empty CTag";
0279     }
0280 
0281     mCollections << davJob->collection();
0282     subjobFinished();
0283 }
0284 
0285 void DavCollectionsFetchJob::subjobFinished()
0286 {
0287     if (--mSubJobCount == 0) {
0288         emitResult();
0289     }
0290 }
0291