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"