| 1 | /* |
| 2 | SPDX-FileCopyrightText: 2010 Tobias Koenig <tokoe@kde.org> |
| 3 | |
| 4 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 5 | */ |
| 6 | |
| 7 | #include "davcollectionsfetchjob.h" |
| 8 | #include "davjobbase_p.h" |
| 9 | |
| 10 | #include "daverror.h" |
| 11 | #include "davmanager_p.h" |
| 12 | #include "davprincipalhomesetsfetchjob.h" |
| 13 | #include "davprotocolbase_p.h" |
| 14 | #include "utils_p.h" |
| 15 | |
| 16 | #include "libkdav_debug.h" |
| 17 | #include <KIO/DavJob> |
| 18 | #include <KIO/Job> |
| 19 | |
| 20 | #include <QBuffer> |
| 21 | #include <QColor> |
| 22 | |
| 23 | using namespace KDAV; |
| 24 | |
| 25 | namespace KDAV |
| 26 | { |
| 27 | class DavCollectionsFetchJobPrivate : public DavJobBasePrivate |
| 28 | { |
| 29 | public: |
| 30 | void principalFetchFinished(KJob *job); |
| 31 | void collectionsFetchFinished(KJob *job); |
| 32 | void doCollectionsFetch(const QUrl &url); |
| 33 | void subjobFinished(); |
| 34 | |
| 35 | DavUrl mUrl; |
| 36 | DavCollection::List mCollections; |
| 37 | uint mSubJobCount = 0; |
| 38 | |
| 39 | Q_DECLARE_PUBLIC(DavCollectionsFetchJob) |
| 40 | }; |
| 41 | } |
| 42 | |
| 43 | DavCollectionsFetchJob::DavCollectionsFetchJob(const DavUrl &url, QObject *parent) |
| 44 | : DavJobBase(new DavCollectionsFetchJobPrivate, parent) |
| 45 | { |
| 46 | Q_D(DavCollectionsFetchJob); |
| 47 | d->mUrl = url; |
| 48 | } |
| 49 | |
| 50 | void DavCollectionsFetchJob::start() |
| 51 | { |
| 52 | Q_D(DavCollectionsFetchJob); |
| 53 | if (DavManager::davProtocol(protocol: d->mUrl.protocol())->supportsPrincipals()) { |
| 54 | DavPrincipalHomeSetsFetchJob *job = new DavPrincipalHomeSetsFetchJob(d->mUrl); |
| 55 | connect(sender: job, signal: &DavPrincipalHomeSetsFetchJob::result, context: this, slot: [d](KJob *job) { |
| 56 | d->principalFetchFinished(job); |
| 57 | }); |
| 58 | job->start(); |
| 59 | } else { |
| 60 | d->doCollectionsFetch(url: d->mUrl.url()); |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | DavCollection::List DavCollectionsFetchJob::collections() const |
| 65 | { |
| 66 | Q_D(const DavCollectionsFetchJob); |
| 67 | return d->mCollections; |
| 68 | } |
| 69 | |
| 70 | DavUrl DavCollectionsFetchJob::davUrl() const |
| 71 | { |
| 72 | Q_D(const DavCollectionsFetchJob); |
| 73 | return d->mUrl; |
| 74 | } |
| 75 | |
| 76 | void DavCollectionsFetchJobPrivate::doCollectionsFetch(const QUrl &url) |
| 77 | { |
| 78 | ++mSubJobCount; |
| 79 | |
| 80 | const QDomDocument collectionQuery = DavManager::davProtocol(protocol: mUrl.protocol())->collectionsQuery()->buildQuery(); |
| 81 | |
| 82 | KIO::DavJob *job = DavManager::self()->createPropFindJob(url, document: collectionQuery.toString()); |
| 83 | QObject::connect(sender: job, signal: &KIO::DavJob::result, context: q_ptr, slot: [this](KJob *job) { |
| 84 | collectionsFetchFinished(job); |
| 85 | }); |
| 86 | job->addMetaData(QStringLiteral("PropagateHttpHeader" ), QStringLiteral("true" )); |
| 87 | } |
| 88 | |
| 89 | void DavCollectionsFetchJobPrivate::principalFetchFinished(KJob *job) |
| 90 | { |
| 91 | const DavPrincipalHomeSetsFetchJob *davJob = qobject_cast<DavPrincipalHomeSetsFetchJob *>(object: job); |
| 92 | |
| 93 | if (davJob->error()) { |
| 94 | if (davJob->canRetryLater()) { |
| 95 | // If we have a non-persistent HTTP error code then this may mean that |
| 96 | // the URL was not a principal URL. Retry as if it were a calendar URL. |
| 97 | qCDebug(KDAV_LOG) << job->errorText(); |
| 98 | doCollectionsFetch(url: mUrl.url()); |
| 99 | } else { |
| 100 | // Just give up here. |
| 101 | setDavError(davJob->davError()); |
| 102 | setErrorTextFromDavError(); |
| 103 | emitResult(); |
| 104 | } |
| 105 | |
| 106 | return; |
| 107 | } |
| 108 | |
| 109 | const QStringList homeSets = davJob->homeSets(); |
| 110 | qCDebug(KDAV_LOG) << "Found" << homeSets.size() << "homesets" ; |
| 111 | qCDebug(KDAV_LOG) << homeSets; |
| 112 | |
| 113 | if (homeSets.isEmpty()) { |
| 114 | // Same as above, retry as if it were a calendar URL. |
| 115 | doCollectionsFetch(url: mUrl.url()); |
| 116 | return; |
| 117 | } |
| 118 | |
| 119 | for (const QString &homeSet : homeSets) { |
| 120 | QUrl url = mUrl.url(); |
| 121 | |
| 122 | if (homeSet.startsWith(c: QLatin1Char('/'))) { |
| 123 | // homeSet is only a path, use request url to complete |
| 124 | url.setPath(path: homeSet, mode: QUrl::TolerantMode); |
| 125 | } else { |
| 126 | // homeSet is a complete url |
| 127 | QUrl tmpUrl(homeSet); |
| 128 | tmpUrl.setUserName(userName: url.userName()); |
| 129 | tmpUrl.setPassword(password: url.password()); |
| 130 | url = tmpUrl; |
| 131 | } |
| 132 | |
| 133 | doCollectionsFetch(url); |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | void DavCollectionsFetchJobPrivate::collectionsFetchFinished(KJob *job) |
| 138 | { |
| 139 | Q_Q(DavCollectionsFetchJob); |
| 140 | KIO::DavJob *davJob = qobject_cast<KIO::DavJob *>(object: job); |
| 141 | const QString responseCodeStr = davJob->queryMetaData(QStringLiteral("responsecode" )); |
| 142 | const int responseCode = responseCodeStr.isEmpty() ? 0 : responseCodeStr.toInt(); |
| 143 | |
| 144 | // KIO::DavJob does not set error() even if the HTTP status code is a 4xx or a 5xx |
| 145 | if (davJob->error() || (responseCode >= 400 && responseCode < 600)) { |
| 146 | if (davJob->url() != mUrl.url()) { |
| 147 | // Retry as if the initial URL was a calendar URL. |
| 148 | // We can end up here when retrieving a homeset on |
| 149 | // which a PROPFIND resulted in an error |
| 150 | doCollectionsFetch(url: mUrl.url()); |
| 151 | --mSubJobCount; |
| 152 | return; |
| 153 | } |
| 154 | |
| 155 | setLatestResponseCode(responseCode); |
| 156 | setError(ERR_PROBLEM_WITH_REQUEST); |
| 157 | setJobErrorText(davJob->errorText()); |
| 158 | setJobError(davJob->error()); |
| 159 | setErrorTextFromDavError(); |
| 160 | } else { |
| 161 | // For use in the collectionDiscovered() signal |
| 162 | QUrl _jobUrl = mUrl.url(); |
| 163 | _jobUrl.setUserInfo(userInfo: QString()); |
| 164 | const QString jobUrl = _jobUrl.toDisplayString(); |
| 165 | |
| 166 | // Validate that we got a valid PROPFIND response |
| 167 | QDomDocument response; |
| 168 | response.setContent(data: davJob->responseData(), options: QDomDocument::ParseOption::UseNamespaceProcessing); |
| 169 | QDomElement rootElement = response.documentElement(); |
| 170 | if (rootElement.tagName().compare(other: QLatin1String("multistatus" ), cs: Qt::CaseInsensitive) != 0) { |
| 171 | setError(ERR_COLLECTIONFETCH); |
| 172 | setErrorTextFromDavError(); |
| 173 | subjobFinished(); |
| 174 | return; |
| 175 | } |
| 176 | |
| 177 | QByteArray resp = davJob->responseData(); |
| 178 | QDomDocument document; |
| 179 | if (!document.setContent(data: resp, options: QDomDocument::ParseOption::UseNamespaceProcessing)) { |
| 180 | setError(ERR_COLLECTIONFETCH); |
| 181 | setErrorTextFromDavError(); |
| 182 | subjobFinished(); |
| 183 | return; |
| 184 | } |
| 185 | |
| 186 | if (!q->error()) { |
| 187 | /* |
| 188 | * Extract information from a document like the following: |
| 189 | * |
| 190 | * <responses> |
| 191 | * <response xmlns="DAV:"> |
| 192 | * <href xmlns="DAV:">/caldav.php/test1.user/home/</href> |
| 193 | * <propstat xmlns="DAV:"> |
| 194 | * <prop xmlns="DAV:"> |
| 195 | * <C:supported-calendar-component-set xmlns:C="urn:ietf:params:xml:ns:caldav"> |
| 196 | * <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VEVENT"/> |
| 197 | * <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VTODO"/> |
| 198 | * <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VJOURNAL"/> |
| 199 | * <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VTIMEZONE"/> |
| 200 | * <C:comp xmlns:C="urn:ietf:params:xml:ns:caldav" name="VFREEBUSY"/> |
| 201 | * </C:supported-calendar-component-set> |
| 202 | * <resourcetype xmlns="DAV:"> |
| 203 | * <collection xmlns="DAV:"/> |
| 204 | * <C:calendar xmlns:C="urn:ietf:params:xml:ns:caldav"/> |
| 205 | * <C:schedule-calendar xmlns:C="urn:ietf:params:xml:ns:caldav"/> |
| 206 | * </resourcetype> |
| 207 | * <displayname xmlns="DAV:">Test1 User</displayname> |
| 208 | * <current-user-privilege-set xmlns="DAV:"> |
| 209 | * <privilege xmlns="DAV:"> |
| 210 | * <read xmlns="DAV:"/> |
| 211 | * </privilege> |
| 212 | * </current-user-privilege-set> |
| 213 | * <getctag xmlns="http://calendarserver.org/ns/">12345</getctag> |
| 214 | * </prop> |
| 215 | * <status xmlns="DAV:">HTTP/1.1 200 OK</status> |
| 216 | * </propstat> |
| 217 | * </response> |
| 218 | * </responses> |
| 219 | */ |
| 220 | |
| 221 | const QDomElement responsesElement = document.documentElement(); |
| 222 | |
| 223 | QDomElement responseElement = Utils::firstChildElementNS(parent: responsesElement, QStringLiteral("DAV:" ), QStringLiteral("response" )); |
| 224 | while (!responseElement.isNull()) { |
| 225 | QDomElement propstatElement; |
| 226 | |
| 227 | // check for the valid propstat, without giving up on first error |
| 228 | { |
| 229 | const QDomNodeList propstats = responseElement.elementsByTagNameNS(QStringLiteral("DAV:" ), QStringLiteral("propstat" )); |
| 230 | for (int i = 0; i < propstats.length(); ++i) { |
| 231 | const QDomElement propstatCandidate = propstats.item(index: i).toElement(); |
| 232 | const QDomElement statusElement = Utils::firstChildElementNS(parent: propstatCandidate, QStringLiteral("DAV:" ), QStringLiteral("status" )); |
| 233 | if (statusElement.text().contains(s: QLatin1String("200" ))) { |
| 234 | propstatElement = propstatCandidate; |
| 235 | } |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | if (propstatElement.isNull()) { |
| 240 | responseElement = Utils::nextSiblingElementNS(element: responseElement, QStringLiteral("DAV:" ), QStringLiteral("response" )); |
| 241 | continue; |
| 242 | } |
| 243 | |
| 244 | // extract url |
| 245 | const QDomElement hrefElement = Utils::firstChildElementNS(parent: responseElement, QStringLiteral("DAV:" ), QStringLiteral("href" )); |
| 246 | if (hrefElement.isNull()) { |
| 247 | responseElement = Utils::nextSiblingElementNS(element: responseElement, QStringLiteral("DAV:" ), QStringLiteral("response" )); |
| 248 | continue; |
| 249 | } |
| 250 | |
| 251 | QString href = hrefElement.text(); |
| 252 | if (!href.endsWith(c: QLatin1Char('/'))) { |
| 253 | href.append(c: QLatin1Char('/')); |
| 254 | } |
| 255 | |
| 256 | QUrl url = davJob->url(); |
| 257 | url.setUserInfo(userInfo: QString()); |
| 258 | if (href.startsWith(c: QLatin1Char('/'))) { |
| 259 | // href is only a path, use request url to complete |
| 260 | url.setPath(path: href, mode: QUrl::TolerantMode); |
| 261 | } else { |
| 262 | // href is a complete url |
| 263 | url = QUrl::fromUserInput(userInput: href); |
| 264 | } |
| 265 | |
| 266 | // don't add this resource if it has already been detected |
| 267 | bool alreadySeen = false; |
| 268 | for (const DavCollection &seen : std::as_const(t&: mCollections)) { |
| 269 | if (seen.url().toDisplayString() == url.toDisplayString()) { |
| 270 | alreadySeen = true; |
| 271 | } |
| 272 | } |
| 273 | if (alreadySeen) { |
| 274 | responseElement = Utils::nextSiblingElementNS(element: responseElement, QStringLiteral("DAV:" ), QStringLiteral("response" )); |
| 275 | continue; |
| 276 | } |
| 277 | |
| 278 | // extract display name |
| 279 | const QDomElement propElement = Utils::firstChildElementNS(parent: propstatElement, QStringLiteral("DAV:" ), QStringLiteral("prop" )); |
| 280 | if (!DavManager::davProtocol(protocol: mUrl.protocol())->containsCollection(propElem: propElement)) { |
| 281 | responseElement = Utils::nextSiblingElementNS(element: responseElement, QStringLiteral("DAV:" ), QStringLiteral("response" )); |
| 282 | continue; |
| 283 | } |
| 284 | const QDomElement displaynameElement = Utils::firstChildElementNS(parent: propElement, QStringLiteral("DAV:" ), QStringLiteral("displayname" )); |
| 285 | const QString displayName = displaynameElement.text(); |
| 286 | |
| 287 | // Extract CTag |
| 288 | const QDomElement CTagElement = Utils::firstChildElementNS(parent: propElement, // |
| 289 | QStringLiteral("http://calendarserver.org/ns/" ), |
| 290 | QStringLiteral("getctag" )); |
| 291 | QString CTag; |
| 292 | if (!CTagElement.isNull()) { |
| 293 | CTag = CTagElement.text(); |
| 294 | } |
| 295 | |
| 296 | // extract calendar color if provided |
| 297 | const QDomElement colorElement = Utils::firstChildElementNS(parent: propElement, // |
| 298 | QStringLiteral("http://apple.com/ns/ical/" ), |
| 299 | QStringLiteral("calendar-color" )); |
| 300 | QColor color; |
| 301 | if (!colorElement.isNull()) { |
| 302 | QString colorValue = colorElement.text(); |
| 303 | if (QColor::isValidColorName(colorValue)) { |
| 304 | // Color is either #RRGGBBAA or #RRGGBB but QColor expects #AARRGGBB |
| 305 | // so we put the AA in front if the string's length is 9. |
| 306 | if (colorValue.size() == 9) { |
| 307 | QString fixedColorValue = QStringLiteral("#" ) + colorValue.mid(position: 7, n: 2) + colorValue.mid(position: 1, n: 6); |
| 308 | color = QColor::fromString(name: fixedColorValue); |
| 309 | } else { |
| 310 | color = QColor::fromString(name: colorValue); |
| 311 | } |
| 312 | } |
| 313 | } |
| 314 | |
| 315 | // extract allowed content types |
| 316 | const DavCollection::ContentTypes contentTypes = DavManager::davProtocol(protocol: mUrl.protocol())->collectionContentTypes(propstat: propstatElement); |
| 317 | |
| 318 | auto _url = url; |
| 319 | _url.setUserInfo(userInfo: mUrl.url().userInfo()); |
| 320 | DavCollection collection(DavUrl(_url, mUrl.protocol()), displayName, contentTypes); |
| 321 | |
| 322 | collection.setCTag(CTag); |
| 323 | if (color.isValid()) { |
| 324 | collection.setColor(color); |
| 325 | } |
| 326 | |
| 327 | // extract privileges |
| 328 | const QDomElement currentPrivsElement = Utils::firstChildElementNS(parent: propElement, // |
| 329 | QStringLiteral("DAV:" ), |
| 330 | QStringLiteral("current-user-privilege-set" )); |
| 331 | if (currentPrivsElement.isNull()) { |
| 332 | // Assume that we have all privileges |
| 333 | collection.setPrivileges(KDAV::All); |
| 334 | } else { |
| 335 | Privileges privileges = Utils::extractPrivileges(element: currentPrivsElement); |
| 336 | collection.setPrivileges(privileges); |
| 337 | } |
| 338 | |
| 339 | qCDebug(KDAV_LOG) << url.toDisplayString() << "PRIVS: " << collection.privileges(); |
| 340 | mCollections << collection; |
| 341 | Q_EMIT q->collectionDiscovered(protocol: mUrl.protocol(), collectionUrl: url.toDisplayString(), configuredUrl: jobUrl); |
| 342 | |
| 343 | responseElement = Utils::nextSiblingElementNS(element: responseElement, QStringLiteral("DAV:" ), QStringLiteral("response" )); |
| 344 | } |
| 345 | } |
| 346 | } |
| 347 | |
| 348 | subjobFinished(); |
| 349 | } |
| 350 | |
| 351 | void DavCollectionsFetchJobPrivate::subjobFinished() |
| 352 | { |
| 353 | if (--mSubJobCount == 0) { |
| 354 | emitResult(); |
| 355 | } |
| 356 | } |
| 357 | |
| 358 | #include "moc_davcollectionsfetchjob.cpp" |
| 359 | |