File indexing completed on 2024-04-28 03:53:55

0001 /*
0002     SPDX-FileCopyrightText: 2010 Tobias Koenig <tokoe@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "davitemslistjob.h"
0008 #include "davjobbase_p.h"
0009 
0010 #include "daverror.h"
0011 #include "davmanager_p.h"
0012 #include "davprotocolbase_p.h"
0013 #include "davurl.h"
0014 #include "etagcache.h"
0015 #include "utils_p.h"
0016 
0017 #include <KIO/DavJob>
0018 #include <KIO/Job>
0019 
0020 #include <set>
0021 
0022 using namespace KDAV;
0023 
0024 namespace KDAV
0025 {
0026 class DavItemsListJobPrivate : public DavJobBasePrivate
0027 {
0028 public:
0029     void davJobFinished(KJob *job);
0030 
0031     DavUrl mUrl;
0032     std::shared_ptr<EtagCache> mEtagCache;
0033     QStringList mMimeTypes;
0034     QString mRangeStart;
0035     QString mRangeEnd;
0036     DavItem::List mItems;
0037     std::set<QString> mSeenUrls; // to prevent events duplication with some servers
0038     DavItem::List mChangedItems;
0039     QStringList mDeletedItems;
0040     uint mSubJobCount = 0;
0041 };
0042 }
0043 
0044 DavItemsListJob::DavItemsListJob(const DavUrl &url, const std::shared_ptr<EtagCache> &cache, QObject *parent)
0045     : DavJobBase(new DavItemsListJobPrivate, parent)
0046 {
0047     Q_D(DavItemsListJob);
0048     d->mUrl = url;
0049     d->mEtagCache = cache;
0050 }
0051 
0052 DavItemsListJob::~DavItemsListJob() = default;
0053 
0054 void DavItemsListJob::setContentMimeTypes(const QStringList &types)
0055 {
0056     Q_D(DavItemsListJob);
0057     d->mMimeTypes = types;
0058 }
0059 
0060 void DavItemsListJob::setTimeRange(const QString &start, const QString &end)
0061 {
0062     Q_D(DavItemsListJob);
0063     d->mRangeStart = start;
0064     d->mRangeEnd = end;
0065 }
0066 
0067 void DavItemsListJob::start()
0068 {
0069     Q_D(DavItemsListJob);
0070     const DavProtocolBase *protocol = DavManager::davProtocol(d->mUrl.protocol());
0071     Q_ASSERT(protocol);
0072 
0073     const auto queries = protocol->itemsQueries();
0074     for (XMLQueryBuilder::Ptr builder : queries) {
0075         if (!d->mRangeStart.isEmpty()) {
0076             builder->setParameter(QStringLiteral("start"), d->mRangeStart);
0077         }
0078         if (!d->mRangeEnd.isEmpty()) {
0079             builder->setParameter(QStringLiteral("end"), d->mRangeEnd);
0080         }
0081 
0082         const QDomDocument props = builder->buildQuery();
0083         const QString mimeType = builder->mimeType();
0084 
0085         if (d->mMimeTypes.isEmpty() || d->mMimeTypes.contains(mimeType)) {
0086             ++d->mSubJobCount;
0087             if (protocol->useReport()) {
0088                 KIO::DavJob *job = DavManager::self()->createReportJob(d->mUrl.url(), props.toString());
0089                 job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
0090                 job->setProperty("davType", QStringLiteral("report"));
0091                 job->setProperty("itemsMimeType", mimeType);
0092                 connect(job, &KIO::DavJob::result, this, [d](KJob *job) {
0093                     d->davJobFinished(job);
0094                 });
0095             } else {
0096                 KIO::DavJob *job = DavManager::self()->createPropFindJob(d->mUrl.url(), props.toString());
0097                 job->addMetaData(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
0098                 job->setProperty("davType", QStringLiteral("propFind"));
0099                 job->setProperty("itemsMimeType", mimeType);
0100                 connect(job, &KIO::DavJob::result, this, [d](KJob *job) {
0101                     d->davJobFinished(job);
0102                 });
0103             }
0104         }
0105     }
0106 
0107     if (d->mSubJobCount == 0) {
0108         setError(ERR_ITEMLIST_NOMIMETYPE);
0109         d->setErrorTextFromDavError();
0110         emitResult();
0111     }
0112 }
0113 
0114 DavItem::List DavItemsListJob::items() const
0115 {
0116     Q_D(const DavItemsListJob);
0117     return d->mItems;
0118 }
0119 
0120 DavItem::List DavItemsListJob::changedItems() const
0121 {
0122     Q_D(const DavItemsListJob);
0123     return d->mChangedItems;
0124 }
0125 
0126 QStringList DavItemsListJob::deletedItems() const
0127 {
0128     Q_D(const DavItemsListJob);
0129     return d->mDeletedItems;
0130 }
0131 
0132 void DavItemsListJobPrivate::davJobFinished(KJob *job)
0133 {
0134     KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(job);
0135     const int responseCode = davJob->queryMetaData(QStringLiteral("responsecode")).isEmpty() //
0136         ? 0
0137         : davJob->queryMetaData(QStringLiteral("responsecode")).toInt();
0138 
0139     // KIO::DavJob does not set error() even if the HTTP status code is a 4xx or a 5xx
0140     if (davJob->error() || (responseCode >= 400 && responseCode < 600)) {
0141         setLatestResponseCode(responseCode);
0142         setError(ERR_PROBLEM_WITH_REQUEST);
0143         setJobErrorText(davJob->errorText());
0144         setJobError(davJob->error());
0145         setErrorTextFromDavError();
0146     } else {
0147         /*
0148          * Extract data from a document like the following:
0149          *
0150          * <multistatus xmlns="DAV:">
0151          *   <response xmlns="DAV:">
0152          *     <href xmlns="DAV:">/caldav.php/test1.user/home/KOrganizer-166749289.780.ics</href>
0153          *     <propstat xmlns="DAV:">
0154          *       <prop xmlns="DAV:">
0155          *         <getetag xmlns="DAV:">"b4bbea0278f4f63854c4167a7656024a"</getetag>
0156          *       </prop>
0157          *       <status xmlns="DAV:">HTTP/1.1 200 OK</status>
0158          *     </propstat>
0159          *   </response>
0160          *   <response xmlns="DAV:">
0161          *     <href xmlns="DAV:">/caldav.php/test1.user/home/KOrganizer-399416366.464.ics</href>
0162          *     <propstat xmlns="DAV:">
0163          *       <prop xmlns="DAV:">
0164          *         <getetag xmlns="DAV:">"52eb129018398a7da4f435b2bc4c6cd5"</getetag>
0165          *       </prop>
0166          *       <status xmlns="DAV:">HTTP/1.1 200 OK</status>
0167          *     </propstat>
0168          *   </response>
0169          * </multistatus>
0170          */
0171 
0172         const QString itemsMimeType = job->property("itemsMimeType").toString();
0173         QDomDocument document;
0174         document.setContent(davJob->responseData(), QDomDocument::ParseOption::UseNamespaceProcessing);
0175         const QDomElement documentElement = document.documentElement();
0176 
0177         QDomElement responseElement = Utils::firstChildElementNS(documentElement, QStringLiteral("DAV:"), QStringLiteral("response"));
0178         while (!responseElement.isNull()) {
0179             QDomElement propstatElement;
0180 
0181             // check for the valid propstat, without giving up on first error
0182             {
0183                 const QDomNodeList propstats = responseElement.elementsByTagNameNS(QStringLiteral("DAV:"), QStringLiteral("propstat"));
0184                 for (int i = 0; i < propstats.length(); ++i) {
0185                     const QDomElement propstatCandidate = propstats.item(i).toElement();
0186                     const QDomElement statusElement = Utils::firstChildElementNS(propstatCandidate, QStringLiteral("DAV:"), QStringLiteral("status"));
0187                     if (statusElement.text().contains(QLatin1String("200"))) {
0188                         propstatElement = propstatCandidate;
0189                     }
0190                 }
0191             }
0192 
0193             if (propstatElement.isNull()) {
0194                 responseElement = Utils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
0195                 continue;
0196             }
0197 
0198             const QDomElement propElement = Utils::firstChildElementNS(propstatElement, QStringLiteral("DAV:"), QStringLiteral("prop"));
0199 
0200             // check whether it is a DAV collection ...
0201             const QDomElement resourcetypeElement = Utils::firstChildElementNS(propElement, QStringLiteral("DAV:"), QStringLiteral("resourcetype"));
0202             if (!resourcetypeElement.isNull()) {
0203                 const QDomElement collectionElement = Utils::firstChildElementNS(resourcetypeElement, QStringLiteral("DAV:"), QStringLiteral("collection"));
0204                 if (!collectionElement.isNull()) {
0205                     responseElement = Utils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
0206                     continue;
0207                 }
0208             }
0209 
0210             // ... if not it is an item
0211             DavItem item;
0212             item.setContentType(itemsMimeType);
0213 
0214             // extract path
0215             const QDomElement hrefElement = Utils::firstChildElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("href"));
0216             const QString href = hrefElement.text();
0217 
0218             QUrl url = davJob->url();
0219             url.setUserInfo(QString());
0220             if (href.startsWith(QLatin1Char('/'))) {
0221                 // href is only a path, use request url to complete
0222                 url.setPath(href, QUrl::TolerantMode);
0223             } else {
0224                 // href is a complete url
0225                 url = QUrl::fromUserInput(href);
0226             }
0227 
0228             const QString itemUrl = url.toDisplayString();
0229             const auto [it, isInserted] = mSeenUrls.insert(itemUrl);
0230             if (!isInserted) {
0231                 responseElement = Utils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
0232                 continue;
0233             }
0234 
0235             auto _url = url;
0236             _url.setUserInfo(mUrl.url().userInfo());
0237             item.setUrl(DavUrl(_url, mUrl.protocol()));
0238 
0239             // extract ETag
0240             const QDomElement getetagElement = Utils::firstChildElementNS(propElement, QStringLiteral("DAV:"), QStringLiteral("getetag"));
0241 
0242             item.setEtag(getetagElement.text());
0243 
0244             mItems << item;
0245 
0246             if (mEtagCache->etagChanged(itemUrl, item.etag())) {
0247                 mChangedItems << item;
0248             }
0249 
0250             responseElement = Utils::nextSiblingElementNS(responseElement, QStringLiteral("DAV:"), QStringLiteral("response"));
0251         }
0252     }
0253 
0254     mDeletedItems.clear();
0255 
0256     const auto map = mEtagCache->etagCacheMap();
0257     for (auto it = map.cbegin(); it != map.cend(); ++it) {
0258         const QString remoteId = it.key();
0259         if (mSeenUrls.find(remoteId) == mSeenUrls.cend()) {
0260             mDeletedItems.append(remoteId);
0261         }
0262     }
0263 
0264     if (--mSubJobCount == 0) {
0265         emitResult();
0266     }
0267 }
0268 
0269 #include "moc_davitemslistjob.cpp"