File indexing completed on 2025-01-05 04:49:57

0001 /*
0002     SPDX-FileCopyrightText: 2009 Grégory Oestreicher <greg@kamago.net>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "davgroupwareresource.h"
0008 
0009 #include "akonadietagcache.h"
0010 #include "configdialog.h"
0011 #include "ctagattribute.h"
0012 #include "davfreebusyhandler.h"
0013 #include "davprotocolattribute.h"
0014 #include "settings.h"
0015 #include "settingsadaptor.h"
0016 #include "setupwizard.h"
0017 #include "utils.h"
0018 
0019 #include <KDAV/DavCollection>
0020 #include <KDAV/DavCollectionDeleteJob>
0021 #include <KDAV/DavCollectionModifyJob>
0022 #include <KDAV/DavCollectionsFetchJob>
0023 #include <KDAV/DavCollectionsMultiFetchJob>
0024 #include <KDAV/DavItem>
0025 #include <KDAV/DavItemCreateJob>
0026 #include <KDAV/DavItemDeleteJob>
0027 #include <KDAV/DavItemFetchJob>
0028 #include <KDAV/DavItemModifyJob>
0029 #include <KDAV/DavItemsFetchJob>
0030 #include <KDAV/DavItemsListJob>
0031 #include <KDAV/ProtocolInfo>
0032 
0033 #include <KCalendarCore/FreeBusy>
0034 #include <KCalendarCore/ICalFormat>
0035 #include <KCalendarCore/Incidence>
0036 #include <KCalendarCore/MemoryCalendar>
0037 #include <KCalendarCore/Todo>
0038 #include <KJob>
0039 
0040 #include "davresource_debug.h"
0041 #include <Akonadi/AttributeFactory>
0042 #include <Akonadi/CachePolicy>
0043 #include <Akonadi/ChangeRecorder>
0044 #include <Akonadi/CollectionColorAttribute>
0045 #include <Akonadi/CollectionFetchScope>
0046 #include <Akonadi/CollectionModifyJob>
0047 #include <Akonadi/EntityDisplayAttribute>
0048 #include <Akonadi/ItemDeleteJob>
0049 #include <Akonadi/ItemFetchJob>
0050 #include <Akonadi/ItemFetchScope>
0051 #include <Akonadi/ItemModifyJob>
0052 #include <Akonadi/RecursiveItemFetchJob>
0053 #include <KContacts/Addressee>
0054 #include <KContacts/VCardConverter>
0055 
0056 #include <KLocalizedString>
0057 #include <kwindowsystem.h>
0058 
0059 using namespace Akonadi;
0060 
0061 using IncidencePtr = QSharedPointer<KCalendarCore::Incidence>;
0062 
0063 DavGroupwareResource::DavGroupwareResource(const QString &id)
0064     : ResourceBase(id)
0065     , FreeBusyProviderBase()
0066     , mFreeBusyHandler(new DavFreeBusyHandler(this))
0067 {
0068     AttributeFactory::registerAttribute<EntityDisplayAttribute>();
0069     AttributeFactory::registerAttribute<DavProtocolAttribute>();
0070     AttributeFactory::registerAttribute<CTagAttribute>();
0071     AttributeFactory::registerAttribute<CollectionColorAttribute>();
0072 
0073     setNeedsNetwork(true);
0074 
0075     mDavCollectionRoot.setParentCollection(Collection::root());
0076     mDavCollectionRoot.setName(identifier());
0077     mDavCollectionRoot.setRemoteId(identifier());
0078     mDavCollectionRoot.setContentMimeTypes(QStringList() << Collection::mimeType());
0079     mDavCollectionRoot.setRights(Collection::CanCreateCollection | Collection::CanDeleteCollection | Collection::CanChangeCollection);
0080 
0081     auto attribute = mDavCollectionRoot.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
0082     attribute->setIconName(QStringLiteral("folder-remote"));
0083 
0084     int refreshInterval = Settings::self()->refreshInterval();
0085     if (refreshInterval == 0) {
0086         refreshInterval = -1;
0087     }
0088 
0089     Akonadi::CachePolicy cachePolicy;
0090     cachePolicy.setInheritFromParent(false);
0091     cachePolicy.setSyncOnDemand(false);
0092     cachePolicy.setCacheTimeout(-1);
0093     cachePolicy.setIntervalCheckTime(refreshInterval);
0094     cachePolicy.setLocalParts(QStringList() << QStringLiteral("ALL"));
0095     mDavCollectionRoot.setCachePolicy(cachePolicy);
0096 
0097     changeRecorder()->fetchCollection(true);
0098     changeRecorder()->collectionFetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All);
0099     changeRecorder()->itemFetchScope().fetchFullPayload(true);
0100     changeRecorder()->itemFetchScope().setAncestorRetrieval(ItemFetchScope::All);
0101 
0102     Settings::self()->setWinId(winIdForDialogs());
0103     Settings::self()->setResourceIdentifier(identifier());
0104 
0105     connect(mFreeBusyHandler, &DavFreeBusyHandler::handlesFreeBusy, this, &DavGroupwareResource::onHandlesFreeBusy);
0106     connect(mFreeBusyHandler, &DavFreeBusyHandler::freeBusyRetrieved, this, &DavGroupwareResource::onFreeBusyRetrieved);
0107 
0108     connect(this, &DavGroupwareResource::reloadConfiguration, this, &DavGroupwareResource::onReloadConfig);
0109 
0110     scheduleCustomTask(this, "initialRetrieveCollections", QVariant(), ResourceBase::Prepend);
0111     scheduleCustomTask(this, "createInitialCache", QVariant(), ResourceBase::Prepend);
0112 }
0113 
0114 DavGroupwareResource::~DavGroupwareResource()
0115 {
0116     delete mFreeBusyHandler;
0117 }
0118 
0119 void DavGroupwareResource::collectionRemoved(const Akonadi::Collection &collection)
0120 {
0121     qCDebug(DAVRESOURCE_LOG) << "Removing collection " << collection.remoteId();
0122 
0123     if (!configurationIsValid()) {
0124         return;
0125     }
0126 
0127     const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(collection.remoteId());
0128 
0129     auto job = new KDAV::DavCollectionDeleteJob(davUrl);
0130     job->setProperty("collection", QVariant::fromValue(collection));
0131     connect(job, &KDAV::DavCollectionDeleteJob::result, this, &DavGroupwareResource::onCollectionRemovedFinished);
0132     job->start();
0133 }
0134 
0135 void DavGroupwareResource::cleanup()
0136 {
0137     Settings::self()->cleanup();
0138     ResourceBase::cleanup();
0139 }
0140 
0141 QDateTime DavGroupwareResource::lastCacheUpdate() const
0142 {
0143     return QDateTime::currentDateTime();
0144 }
0145 
0146 void DavGroupwareResource::canHandleFreeBusy(const QString &email) const
0147 {
0148     if (!isOnline()) {
0149         handlesFreeBusy(email, false);
0150     } else {
0151         mFreeBusyHandler->canHandleFreeBusy(email);
0152     }
0153 }
0154 
0155 void DavGroupwareResource::onHandlesFreeBusy(const QString &email, bool handles)
0156 {
0157     handlesFreeBusy(email, handles);
0158 }
0159 
0160 void DavGroupwareResource::retrieveFreeBusy(const QString &email, const QDateTime &start, const QDateTime &end)
0161 {
0162     if (!isOnline()) {
0163         freeBusyRetrieved(email, QString(), false, i18n("Unable to retrieve free-busy info while offline"));
0164     } else {
0165         mFreeBusyHandler->retrieveFreeBusy(email, start, end);
0166     }
0167 }
0168 
0169 void DavGroupwareResource::onFreeBusyRetrieved(const QString &email, const QString &freeBusy, bool success, const QString &errorText)
0170 {
0171     freeBusyRetrieved(email, freeBusy, success, errorText);
0172 }
0173 
0174 void DavGroupwareResource::configure(WId windowId)
0175 {
0176     Settings::self()->setWinId(windowId);
0177 
0178     // On the initial configuration we start the setup wizard
0179     if (Settings::self()->configuredDavUrls().isEmpty()) {
0180         SetupWizard wizard;
0181 
0182         if (windowId) {
0183             wizard.setAttribute(Qt::WA_NativeWindow, true);
0184             KWindowSystem::setMainWindow(wizard.windowHandle(), windowId);
0185         }
0186 
0187         const int result = wizard.exec();
0188         if (result == QDialog::Accepted) {
0189             const SetupWizard::Url::List urls = wizard.urls();
0190             for (const SetupWizard::Url &url : urls) {
0191                 auto urlConfig = new Settings::UrlConfiguration();
0192 
0193                 urlConfig->mUrl = url.url;
0194                 urlConfig->mProtocol = url.protocol;
0195                 urlConfig->mUser = url.userName;
0196                 urlConfig->mPassword = wizard.field(QStringLiteral("credentialsPassword")).toString();
0197 
0198                 Settings::self()->newUrlConfiguration(urlConfig);
0199             }
0200 
0201             if (!urls.isEmpty()) {
0202                 Settings::self()->setDisplayName(wizard.displayName());
0203             }
0204 
0205             QString defaultUser = wizard.field(QStringLiteral("credentialsUserName")).toString();
0206             if (!defaultUser.isEmpty()) {
0207                 Settings::self()->setDefaultUsername(defaultUser);
0208                 Settings::self()->setDefaultPassword(wizard.field(QStringLiteral("credentialsPassword")).toString());
0209             }
0210         }
0211     }
0212 
0213     // continue with the normal config dialog
0214     ConfigDialog dialog;
0215 
0216     if (windowId) {
0217         dialog.setAttribute(Qt::WA_NativeWindow, true);
0218         KWindowSystem::setMainWindow(dialog.windowHandle(), windowId);
0219     }
0220 
0221     if (!Settings::self()->defaultUsername().isEmpty()) {
0222         dialog.setPassword(Settings::self()->defaultPassword());
0223     }
0224 
0225     const int result = dialog.exec();
0226 
0227     if (result == QDialog::Accepted) {
0228         Settings::self()->setSettingsVersion(3);
0229         Settings::self()->save();
0230         synchronize();
0231         Q_EMIT configurationDialogAccepted();
0232     } else {
0233         Q_EMIT configurationDialogRejected();
0234     }
0235 }
0236 
0237 KJob *DavGroupwareResource::createRetrieveCollectionsJob()
0238 {
0239     qCDebug(DAVRESOURCE_LOG) << "Retrieving collections list";
0240     mSyncErrorNotified = false;
0241 
0242     if (!configurationIsValid()) {
0243         return nullptr;
0244     }
0245 
0246     Q_EMIT status(Running, i18n("Fetching collections"));
0247 
0248     auto job = new KDAV::DavCollectionsMultiFetchJob(Settings::self()->configuredDavUrls());
0249     connect(job, &KDAV::DavCollectionsMultiFetchJob::result, this, &DavGroupwareResource::onRetrieveCollectionsFinished);
0250     connect(job, &KDAV::DavCollectionsMultiFetchJob::collectionDiscovered, this, &DavGroupwareResource::onCollectionDiscovered);
0251     return job;
0252 }
0253 
0254 void DavGroupwareResource::initialRetrieveCollections()
0255 {
0256     auto job = createRetrieveCollectionsJob();
0257     if (!job) {
0258         return;
0259     }
0260     job->setProperty("initialCacheSync", QVariant::fromValue(true));
0261     job->start();
0262 }
0263 
0264 void DavGroupwareResource::retrieveCollections()
0265 {
0266     auto job = createRetrieveCollectionsJob();
0267     if (!job) {
0268         return;
0269     }
0270     job->setProperty("initialCacheSync", QVariant::fromValue(false));
0271     job->start();
0272 }
0273 
0274 void DavGroupwareResource::retrieveItems(const Akonadi::Collection &collection)
0275 {
0276     if (!collection.isValid()) {
0277         itemsRetrievalDone();
0278         return;
0279     }
0280 
0281     qCDebug(DAVRESOURCE_LOG) << "Retrieving items for collection " << collection.remoteId();
0282 
0283     if (!configurationIsValid()) {
0284         return;
0285     }
0286 
0287     // As the resource root collection contains mime types for items we must
0288     // work around the fact that Akonadi will rightfully try to retrieve items
0289     // from it. So just return an empty list
0290     if (collection.remoteId() == identifier()) {
0291         itemsRetrievalDone();
0292         return;
0293     }
0294 
0295     if (!mEtagCaches.contains(collection.remoteId())) {
0296         qCDebug(DAVRESOURCE_LOG) << "Asked to retrieve items for a collection we don't have in the cache";
0297         itemsRetrievalDone();
0298         return;
0299     }
0300 
0301     // Only continue if the collection has changed or if
0302     // it's the first time we see it
0303     const auto CTagAttr = collection.attribute<CTagAttribute>();
0304     if (CTagAttr && mCTagCache.contains(collection.remoteId()) && mCTagCache.value(collection.remoteId()) == CTagAttr->CTag()) {
0305         qCDebug(DAVRESOURCE_LOG) << "CTag for collection" << collection.remoteId() << "didn't change: " << CTagAttr->CTag();
0306         itemsRetrievalDone();
0307         return;
0308     }
0309 
0310     const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(collection.remoteId());
0311 
0312     if (!davUrl.url().isValid()) {
0313         qCCritical(DAVRESOURCE_LOG) << "Can't find a configured URL, collection.remoteId() is " << collection.remoteId();
0314         cancelTask(i18n("Asked to retrieve items for an unknown collection: %1", collection.remoteId()));
0315         // Q_ASSERT_X( false, "DavGroupwareResource::retrieveItems", "Url is invalid" );
0316         return;
0317     }
0318 
0319     auto job = new KDAV::DavItemsListJob(davUrl, mEtagCaches.value(collection.remoteId()));
0320     if (Settings::self()->limitSyncRange()) {
0321         QDateTime start = Settings::self()->getSyncRangeStart();
0322         qCDebug(DAVRESOURCE_LOG) << "Start time for list job:" << start;
0323         if (start.isValid()) {
0324             job->setTimeRange(start.toString(QStringLiteral("yyyyMMddTHHMMssZ")), QString());
0325         }
0326     }
0327     job->setProperty("collection", QVariant::fromValue(collection));
0328     job->setContentMimeTypes(collection.contentMimeTypes());
0329     connect(job, &KDAV::DavItemsListJob::result, this, &DavGroupwareResource::onRetrieveItemsFinished);
0330     job->start();
0331 }
0332 
0333 bool DavGroupwareResource::retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &)
0334 {
0335     qCDebug(DAVRESOURCE_LOG) << "Retrieving single item. Remote id = " << item.remoteId();
0336 
0337     if (!configurationIsValid()) {
0338         return false;
0339     }
0340 
0341     const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(item.parentCollection().remoteId(), item.remoteId());
0342     if (!davUrl.url().isValid()) {
0343         qCDebug(DAVRESOURCE_LOG) << "Failed to get a valid DavUrl. Parent collection remote ID is" << item.parentCollection().remoteId();
0344         cancelTask();
0345         return false;
0346     }
0347 
0348     KDAV::DavItem davItem;
0349     davItem.setContentType(QStringLiteral("text/calendar"));
0350     davItem.setEtag(item.remoteRevision());
0351 
0352     auto job = new KDAV::DavItemFetchJob(davItem);
0353     job->setProperty("item", QVariant::fromValue(item));
0354     connect(job, &KDAV::DavItemFetchJob::result, this, &DavGroupwareResource::onRetrieveItemFinished);
0355     job->start();
0356 
0357     return true;
0358 }
0359 
0360 void DavGroupwareResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection)
0361 {
0362     qCDebug(DAVRESOURCE_LOG) << "Received notification for added item. Local id = " << item.id() << ". Remote id = " << item.remoteId()
0363                              << ". Collection remote id = " << collection.remoteId();
0364 
0365     if (!configurationIsValid()) {
0366         return;
0367     }
0368 
0369     if (collection.remoteId().isEmpty()) {
0370         qCCritical(DAVRESOURCE_LOG) << "Invalid remote id for collection " << collection.id() << " = " << collection.remoteId();
0371         cancelTask(i18n("Invalid collection for item %1.", item.id()));
0372         return;
0373     }
0374 
0375     KDAV::DavItem davItem = Utils::createDavItem(item, collection);
0376     if (davItem.data().isEmpty()) {
0377         qCCritical(DAVRESOURCE_LOG) << "Item " << item.id() << " doesn't has a valid payload";
0378         cancelTask();
0379         return;
0380     }
0381 
0382     const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(collection.remoteId(), davItem.url().toDisplayString());
0383     qCDebug(DAVRESOURCE_LOG) << "Item " << item.id() << " will be put to " << davItem.url().toDisplayString();
0384     davItem.setUrl(davUrl);
0385 
0386     auto job = new KDAV::DavItemCreateJob(davItem);
0387     job->setProperty("collection", QVariant::fromValue(collection));
0388     job->setProperty("item", QVariant::fromValue(item));
0389     connect(job, &KDAV::DavItemCreateJob::result, this, &DavGroupwareResource::onItemAddedFinished);
0390     job->start();
0391 }
0392 
0393 void DavGroupwareResource::itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &)
0394 {
0395     qCDebug(DAVRESOURCE_LOG) << "Received notification for changed item. Local id = " << item.id() << ". Remote id = " << item.remoteId();
0396 
0397     if (!configurationIsValid()) {
0398         return;
0399     }
0400 
0401     const Akonadi::Collection collection = item.parentCollection();
0402     if (!mEtagCaches.contains(collection.remoteId())) {
0403         qCDebug(DAVRESOURCE_LOG) << "Changed item is in a collection we don't have in the cache";
0404         // TODO: display an error
0405         cancelTask();
0406         return;
0407     }
0408 
0409     QString ridBase = item.remoteId();
0410     if (ridBase.contains(QLatin1Char('#'))) {
0411         ridBase.truncate(ridBase.indexOf(QLatin1Char('#')));
0412     }
0413 
0414     auto cache = mEtagCaches.value(collection.remoteId());
0415     Akonadi::Item::List extraItems;
0416     const QStringList lstUrls = cache->urls();
0417     for (const QString &rid : lstUrls) {
0418         if (rid.startsWith(ridBase) && rid != item.remoteId()) {
0419             Akonadi::Item extraItem;
0420             extraItem.setRemoteId(rid);
0421             extraItems << extraItem;
0422         }
0423     }
0424 
0425     if (extraItems.isEmpty()) {
0426         doItemChange(item);
0427     } else {
0428         auto job = new Akonadi::ItemFetchJob(extraItems);
0429         job->setCollection(item.parentCollection());
0430         job->fetchScope().fetchFullPayload();
0431         job->setProperty("item", QVariant::fromValue(item));
0432         connect(job, &Akonadi::ItemFetchJob::result, this, &DavGroupwareResource::onItemChangePrepared);
0433     }
0434 }
0435 
0436 void DavGroupwareResource::onItemChangePrepared(KJob *job)
0437 {
0438     auto fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job);
0439     auto item = job->property("item").value<Akonadi::Item>();
0440     doItemChange(item, fetchJob->items());
0441 }
0442 
0443 void DavGroupwareResource::doItemChange(const Akonadi::Item &item, const Akonadi::Item::List &dependentItems)
0444 {
0445     KDAV::DavItem davItem = Utils::createDavItem(item, item.parentCollection(), dependentItems);
0446     if (davItem.data().isEmpty()) {
0447         qCCritical(DAVRESOURCE_LOG) << "Item " << item.id() << " doesn't has a valid payload";
0448         cancelTask();
0449         return;
0450     }
0451 
0452     QString url = item.remoteId();
0453     if (url.contains(QLatin1Char('#'))) {
0454         url.truncate(url.indexOf(QLatin1Char('#')));
0455     }
0456     const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(item.parentCollection().remoteId(), url);
0457 
0458     // We have to re-set the URL as it's not necessarily valid after createDavItem()
0459     davItem.setUrl(davUrl);
0460     davItem.setEtag(item.remoteRevision());
0461 
0462     auto modJob = new KDAV::DavItemModifyJob(davItem);
0463     modJob->setProperty("collection", QVariant::fromValue(item.parentCollection()));
0464     modJob->setProperty("item", QVariant::fromValue(item));
0465     modJob->setProperty("dependentItems", QVariant::fromValue(dependentItems));
0466     connect(modJob, &KDAV::DavItemModifyJob::result, this, &DavGroupwareResource::onItemChangedFinished);
0467     modJob->start();
0468 }
0469 
0470 void DavGroupwareResource::itemRemoved(const Akonadi::Item &item)
0471 {
0472     qCDebug(DAVRESOURCE_LOG) << "Received notification for removed item. Remote id = " << item.remoteId();
0473 
0474     if (!configurationIsValid()) {
0475         return;
0476     }
0477 
0478     const Akonadi::Collection collection = item.parentCollection();
0479     if (!mEtagCaches.contains(collection.remoteId())) {
0480         qCDebug(DAVRESOURCE_LOG) << "Removed item is in a collection we don't have in the cache";
0481         // TODO: display an error
0482         cancelTask();
0483         return;
0484     }
0485 
0486     QString ridBase = item.remoteId();
0487     if (ridBase.contains(QLatin1Char('#'))) {
0488         // A bit tricky: we must remove an incidence contained in a resource
0489         // containing multiple ones.
0490         ridBase.truncate(ridBase.indexOf(QLatin1Char('#')));
0491 
0492         auto cache = mEtagCaches.value(collection.remoteId());
0493         Akonadi::Item::List extraItems;
0494         const QStringList lstUrl = cache->urls();
0495         for (const QString &rid : lstUrl) {
0496             if (rid.startsWith(ridBase) && rid != item.remoteId()) {
0497                 Akonadi::Item extraItem;
0498                 extraItem.setRemoteId(rid);
0499                 extraItems << extraItem;
0500             }
0501         }
0502 
0503         if (extraItems.isEmpty()) {
0504             // Urrrr?
0505             // Well, just delete the item.
0506             doItemRemoval(item);
0507         } else {
0508             auto job = new Akonadi::ItemFetchJob(extraItems);
0509             job->setCollection(item.parentCollection());
0510             job->fetchScope().fetchFullPayload();
0511             job->setProperty("item", QVariant::fromValue(item));
0512             connect(job, &Akonadi::ItemFetchJob::result, this, &DavGroupwareResource::onItemRemovalPrepared);
0513         }
0514     } else {
0515         // easy as pie: just remove everything at the URL.
0516         doItemRemoval(item);
0517     }
0518 }
0519 
0520 void DavGroupwareResource::onItemRemovalPrepared(KJob *job)
0521 {
0522     auto fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job);
0523     auto item = job->property("item").value<Akonadi::Item>();
0524     const Akonadi::Item::List keptItems = fetchJob->items();
0525 
0526     if (keptItems.isEmpty()) {
0527         // Urrrr? Not again!
0528         doItemRemoval(item);
0529     } else {
0530         Akonadi::Item mainItem;
0531         Akonadi::Item::List extraItems;
0532         QString ridBase = item.remoteId();
0533         ridBase.truncate(ridBase.indexOf(QLatin1Char('#')));
0534 
0535         for (const Akonadi::Item &kept : keptItems) {
0536             if (kept.remoteId() == ridBase && extraItems.isEmpty()) {
0537                 mainItem = kept;
0538             } else {
0539                 extraItems << kept;
0540             }
0541         }
0542 
0543         if (!mainItem.hasPayload()) {
0544             mainItem = extraItems.takeFirst();
0545         }
0546 
0547         const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(item.parentCollection().remoteId(), ridBase);
0548 
0549         KDAV::DavItem davItem = Utils::createDavItem(mainItem, mainItem.parentCollection(), extraItems);
0550         davItem.setUrl(davUrl);
0551         davItem.setEtag(item.remoteRevision());
0552 
0553         auto modJob = new KDAV::DavItemModifyJob(davItem);
0554         modJob->setProperty("collection", QVariant::fromValue(mainItem.parentCollection()));
0555         modJob->setProperty("item", QVariant::fromValue(mainItem));
0556         modJob->setProperty("dependentItems", QVariant::fromValue(extraItems));
0557         modJob->setProperty("isRemoval", QVariant::fromValue(true));
0558         modJob->setProperty("removedItem", QVariant::fromValue(item));
0559         connect(modJob, &KDAV::DavItemModifyJob::result, this, &DavGroupwareResource::onItemChangedFinished);
0560         modJob->start();
0561     }
0562 }
0563 
0564 void DavGroupwareResource::doItemRemoval(const Akonadi::Item &item)
0565 {
0566     const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(item.parentCollection().remoteId(), item.remoteId());
0567 
0568     KDAV::DavItem davItem;
0569     davItem.setUrl(davUrl);
0570     davItem.setEtag(item.remoteRevision());
0571 
0572     auto job = new KDAV::DavItemDeleteJob(davItem);
0573     job->setProperty("item", QVariant::fromValue(item));
0574     job->setProperty("collection", QVariant::fromValue(item.parentCollection()));
0575     connect(job, &KDAV::DavItemDeleteJob::result, this, &DavGroupwareResource::onItemRemovedFinished);
0576     job->start();
0577 }
0578 
0579 void DavGroupwareResource::collectionChanged(const Collection &collection)
0580 {
0581     qCDebug(DAVRESOURCE_LOG) << "Collection changed" << collection.remoteId();
0582 
0583     const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(collection.remoteId());
0584 
0585     QColor color;
0586     if (collection.hasAttribute<Akonadi::CollectionColorAttribute>()) {
0587         const auto colorAttr = collection.attribute<Akonadi::CollectionColorAttribute>();
0588         if (colorAttr) {
0589             color = colorAttr->color();
0590         }
0591     }
0592 
0593     auto job = new KDAV::DavCollectionModifyJob(davUrl);
0594     // TODO fix renaming calendars with parent folders, right now it makes a bit of a mess
0595     // job->setProperty(QStringLiteral("displayname"), collection.displayName());
0596     if (color.isValid()) {
0597         job->setProperty(QStringLiteral("calendar-color"), color.name(), QStringLiteral("http://apple.com/ns/ical/"));
0598     }
0599     connect(job, &KDAV::DavCollectionModifyJob::result, this, [this, collection](KJob *job) {
0600         onCollectionChangedFinished(job, collection);
0601     });
0602     job->start();
0603 }
0604 
0605 void DavGroupwareResource::onCollectionChangedFinished(KJob *job, const Collection &collection)
0606 {
0607     if (job->error()) {
0608         cancelTask(i18n("Unable to modify collection: %1", job->errorText()));
0609         return;
0610     }
0611     changeCommitted(collection);
0612 }
0613 
0614 void DavGroupwareResource::doSetOnline(bool online)
0615 {
0616     qCDebug(DAVRESOURCE_LOG) << "Resource changed online status to" << online;
0617 
0618     if (online) {
0619         synchronize();
0620     }
0621 
0622     ResourceBase::doSetOnline(online);
0623 }
0624 
0625 void DavGroupwareResource::createInitialCache()
0626 {
0627     // Get all the items fetched by this resource
0628     auto job = new Akonadi::RecursiveItemFetchJob(mDavCollectionRoot, QStringList());
0629     job->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::Parent);
0630     connect(job, &Akonadi::RecursiveItemFetchJob::result, this, &DavGroupwareResource::onCreateInitialCacheReady);
0631     job->start();
0632 }
0633 
0634 void DavGroupwareResource::onCreateInitialCacheReady(KJob *job)
0635 {
0636     auto fetchJob = qobject_cast<Akonadi::RecursiveItemFetchJob *>(job);
0637 
0638     const Akonadi::Item::List itemsLst = fetchJob->items();
0639     for (const Akonadi::Item &item : itemsLst) {
0640         const QString rid = item.remoteId();
0641         if (rid.isEmpty()) {
0642             qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onCreateInitialCacheReady: Found an item without remote ID. " << item.id();
0643             continue;
0644         }
0645 
0646         const Akonadi::Collection collection = item.parentCollection();
0647         if (collection.remoteId().isEmpty()) {
0648             qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onCreateInitialCacheReady: Found an item in a collection without remote ID. " << item.remoteId();
0649             continue;
0650         }
0651 
0652         const QString etag = item.remoteRevision();
0653         if (etag.isEmpty()) {
0654             qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onCreateInitialCacheReady: Found an item without ETag. " << item.remoteId();
0655             continue;
0656         }
0657 
0658         if (!mEtagCaches.contains(collection.remoteId())) {
0659             auto cache = std::shared_ptr<KDAV::EtagCache>(new AkonadiEtagCache(collection));
0660             mEtagCaches.insert(collection.remoteId(), cache);
0661         }
0662 
0663         mEtagCaches[collection.remoteId()]->setEtag(rid, etag);
0664     }
0665     taskDone();
0666 }
0667 
0668 void DavGroupwareResource::onReloadConfig()
0669 {
0670     Settings::self()->reloadConfig();
0671     synchronize();
0672 }
0673 
0674 void DavGroupwareResource::onCollectionRemovedFinished(KJob *job)
0675 {
0676     if (job->error()) {
0677         cancelTask(i18n("Unable to remove collection: %1", job->errorText()));
0678         return;
0679     }
0680 
0681     auto collection = job->property("collection").value<Akonadi::Collection>();
0682 
0683     if (mEtagCaches.contains(collection.remoteId())) {
0684         mEtagCaches[collection.remoteId()]->deleteLater();
0685         mEtagCaches.remove(collection.remoteId());
0686     }
0687 
0688     changeProcessed();
0689 }
0690 
0691 void DavGroupwareResource::onRetrieveCollectionsFinished(KJob *job)
0692 {
0693     const KDAV::DavCollectionsMultiFetchJob *fetchJob = qobject_cast<KDAV::DavCollectionsMultiFetchJob *>(job);
0694 
0695     if (job->error()) {
0696         qCWarning(DAVRESOURCE_LOG) << "Unable to fetch collections" << job->error() << job->errorText();
0697         cancelTask(i18n("Unable to retrieve collections: %1", job->errorText()));
0698         mSyncErrorNotified = true;
0699         return;
0700     }
0701 
0702     bool initialCacheSync = job->property("initialCacheSync").toBool();
0703     Akonadi::Collection::List collections{mDavCollectionRoot};
0704     QSet<QString> seenCollectionsUrls;
0705 
0706     const KDAV::DavCollection::List davCollections = fetchJob->collections();
0707 
0708     for (const KDAV::DavCollection &davCollection : davCollections) {
0709         if (seenCollectionsUrls.contains(davCollection.url().toDisplayString())) {
0710             qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onRetrieveCollectionsFinished: Duplicate collection reported. "
0711                                      << davCollection.url().toDisplayString();
0712             continue;
0713         } else {
0714             seenCollectionsUrls.insert(davCollection.url().toDisplayString());
0715         }
0716 
0717         Akonadi::Collection collection;
0718         collection.setParentCollection(mDavCollectionRoot);
0719         collection.setRemoteId(davCollection.url().toDisplayString());
0720         collection.setName(collection.remoteId());
0721 
0722         if (davCollection.color().isValid()) {
0723             auto colorAttr = collection.attribute<CollectionColorAttribute>(Akonadi::Collection::AddIfMissing);
0724             colorAttr->setColor(davCollection.color());
0725         }
0726 
0727         if (!davCollection.displayName().isEmpty()) {
0728             auto attr = collection.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
0729             attr->setDisplayName(davCollection.displayName());
0730         }
0731 
0732         QStringList mimeTypes;
0733         mimeTypes << Collection::mimeType();
0734 
0735         const KDAV::DavCollection::ContentTypes contentTypes = davCollection.contentTypes();
0736         if (contentTypes & KDAV::DavCollection::Calendar) {
0737             mimeTypes << QStringLiteral("text/calendar");
0738         }
0739 
0740         if (contentTypes & KDAV::DavCollection::Events) {
0741             mimeTypes << KCalendarCore::Event::eventMimeType();
0742         }
0743 
0744         if (contentTypes & KDAV::DavCollection::Todos) {
0745             mimeTypes << KCalendarCore::Todo::todoMimeType();
0746         }
0747 
0748         if (contentTypes & KDAV::DavCollection::Contacts) {
0749             mimeTypes << KContacts::Addressee::mimeType();
0750         }
0751 
0752         if (contentTypes & KDAV::DavCollection::FreeBusy) {
0753             mimeTypes << KCalendarCore::FreeBusy::freeBusyMimeType();
0754         }
0755 
0756         if (contentTypes & KDAV::DavCollection::Journal) {
0757             mimeTypes << KCalendarCore::Journal::journalMimeType();
0758         }
0759 
0760         collection.setContentMimeTypes(mimeTypes);
0761         setCollectionIcon(collection /*by-ref*/);
0762 
0763         auto protoAttr = collection.attribute<DavProtocolAttribute>(Collection::AddIfMissing);
0764         protoAttr->setDavProtocol(davCollection.url().protocol());
0765 
0766         /*
0767          * We unfortunately have to update the CTag now in the cache
0768          * as this information will not be available when retrieveItems()
0769          * is called. We leave it untouched in the collection attribute
0770          * and will only update it there after successful sync.
0771          */
0772         if (!davCollection.CTag().isEmpty()) {
0773             mCTagCache.insert(davCollection.url().toDisplayString(), davCollection.CTag());
0774         }
0775 
0776         KDAV::Privileges privileges = davCollection.privileges();
0777         Akonadi::Collection::Rights rights;
0778 
0779         if (privileges & KDAV::All || privileges & KDAV::Write) {
0780             rights |= Akonadi::Collection::AllRights;
0781         }
0782 
0783         if (privileges & KDAV::WriteContent) {
0784             rights |= Akonadi::Collection::CanChangeItem;
0785         }
0786 
0787         if (privileges & KDAV::Bind) {
0788             rights |= Akonadi::Collection::CanCreateItem;
0789         }
0790 
0791         if (privileges & KDAV::Unbind) {
0792             rights |= Akonadi::Collection::CanDeleteItem;
0793         }
0794 
0795         if (privileges == KDAV::Read) {
0796             rights |= Akonadi::Collection::ReadOnly;
0797         }
0798 
0799         collection.setRights(rights);
0800         collections << collection;
0801 
0802         if (!mEtagCaches.contains(collection.remoteId())) {
0803             auto cache = std::shared_ptr<KDAV::EtagCache>(new AkonadiEtagCache(collection));
0804             mEtagCaches.insert(collection.remoteId(), cache);
0805         }
0806     }
0807 
0808     const auto keys{mEtagCaches.keys()};
0809     for (const QString &rid : keys) {
0810         if (!seenCollectionsUrls.contains(rid)) {
0811             qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onRetrieveCollectionsFinished: Collection disappeared. " << rid;
0812             mEtagCaches[rid]->deleteLater();
0813             mEtagCaches.remove(rid);
0814         }
0815     }
0816 
0817     if (!initialCacheSync) {
0818         collectionsRetrieved(collections);
0819     } else {
0820         taskDone();
0821     }
0822 }
0823 
0824 void DavGroupwareResource::onRetrieveItemsFinished(KJob *job)
0825 {
0826     if (job->error()) {
0827         if (mSyncErrorNotified) {
0828             cancelTask();
0829         } else {
0830             cancelTask(i18n("Unable to retrieve items: %1", job->errorText()));
0831             mSyncErrorNotified = true;
0832         }
0833         return;
0834     }
0835 
0836     auto collection = job->property("collection").value<Collection>();
0837     const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(collection.remoteId());
0838     const bool protocolSupportsMultiget = KDAV::ProtocolInfo::useMultiget(davUrl.protocol());
0839 
0840     const KDAV::DavItemsListJob *listJob = qobject_cast<KDAV::DavItemsListJob *>(job);
0841     auto cache = mEtagCaches.value(collection.remoteId());
0842     if (!cache) {
0843         qCDebug(DAVRESOURCE_LOG) << "Collection has disappeared during item fetch!";
0844         cancelTask();
0845         return;
0846     }
0847 
0848     Akonadi::Item::List changedItems;
0849     QSet<QString> seenRids;
0850     QStringList changedRids;
0851     changedItems.reserve(listJob->changedItems().count());
0852     const auto listJobChangedItems{listJob->changedItems()};
0853     for (const KDAV::DavItem &davItem : listJobChangedItems) {
0854         seenRids.insert(davItem.url().toDisplayString());
0855 
0856         Akonadi::Item item;
0857         item.setParentCollection(collection);
0858         item.setRemoteId(davItem.url().toDisplayString());
0859         item.setMimeType(davItem.contentType());
0860         item.setRemoteRevision(davItem.etag());
0861 
0862         cache->markAsChanged(item.remoteId());
0863         changedRids << item.remoteId();
0864         changedItems << item;
0865 
0866         // Only clear the payload (and therefore trigger a refetch from the backend) if we
0867         // do not use multiget, because in this case we fetch the complete payload
0868         // some lines below already.
0869         if (!protocolSupportsMultiget) {
0870             qCDebug(DAVRESOURCE_LOG) << "Outdated item " << item.remoteId() << " (etag = " << davItem.etag() << ")";
0871             item.clearPayload();
0872         }
0873     }
0874 
0875     const auto listJobDeleteItems{listJob->deletedItems()};
0876     for (const QString &rmd : listJobDeleteItems) {
0877         // We don't want to delete dependent items if the main item was seen
0878         if (rmd.contains(QLatin1Char('#'))) {
0879             const QString base = rmd.left(rmd.indexOf(QLatin1Char('#')));
0880             if (seenRids.contains(base)) {
0881                 continue;
0882             }
0883         }
0884 
0885         qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onRetrieveItemsFinished: Item disappeared. " << rmd;
0886         Akonadi::Item item;
0887         item.setParentCollection(collection);
0888         item.setRemoteId(rmd);
0889         cache->removeEtag(rmd);
0890 
0891         // Use a job to delete items as itemsRetrievedIncremental seem to choke
0892         // when many items are given with just their RID.
0893         auto deleteJob = new Akonadi::ItemDeleteJob(item);
0894         deleteJob->start();
0895     }
0896 
0897     // If the protocol supports multiget then deviate from the expected behavior
0898     // and fetch all items with payload now instead of waiting for Akonadi to
0899     // request it item by item in retrieveItem().
0900     // This allows the resource to use the multiget query and let it be nice
0901     // to the remote server : only one request for n items instead of n requests.
0902     if (protocolSupportsMultiget && !changedRids.isEmpty()) {
0903         auto fetchJob = new KDAV::DavItemsFetchJob(davUrl, changedRids);
0904         connect(fetchJob, &KDAV::DavItemsFetchJob::result, this, &DavGroupwareResource::onMultigetFinished);
0905         fetchJob->setProperty("collection", QVariant::fromValue(collection));
0906         fetchJob->setProperty("items", QVariant::fromValue(changedItems));
0907         fetchJob->start();
0908         // delay the call of itemsRetrieved() to onMultigetFinished()
0909     } else {
0910         // Update the collection CTag attribute now as sync is done.
0911         if (mCTagCache.contains(collection.remoteId())) {
0912             auto CTagAttr = collection.attribute<CTagAttribute>(Collection::AddIfMissing);
0913             qCDebug(DAVRESOURCE_LOG) << "Updating collection CTag from" << CTagAttr->CTag() << "to" << mCTagCache.value(collection.remoteId());
0914             CTagAttr->setCTag(mCTagCache.value(collection.remoteId()));
0915             auto modifyJob = new Akonadi::CollectionModifyJob(collection);
0916             modifyJob->start();
0917         }
0918 
0919         itemsRetrievedIncremental(changedItems, Akonadi::Item::List());
0920     }
0921 }
0922 
0923 void DavGroupwareResource::onMultigetFinished(KJob *job)
0924 {
0925     if (job->error()) {
0926         if (mSyncErrorNotified) {
0927             cancelTask();
0928         } else {
0929             cancelTask(i18n("Unable to retrieve items: %1", job->errorText()));
0930             mSyncErrorNotified = true;
0931         }
0932         return;
0933     }
0934 
0935     auto collection = job->property("collection").value<Akonadi::Collection>();
0936     auto cache = mEtagCaches.value(collection.remoteId());
0937     if (!cache) {
0938         qCDebug(DAVRESOURCE_LOG) << "Collection has disappeared during item fetch!";
0939         cancelTask();
0940         return;
0941     }
0942 
0943     const auto origItems = job->property("items").value<Akonadi::Item::List>();
0944     const KDAV::DavItemsFetchJob *davJob = qobject_cast<KDAV::DavItemsFetchJob *>(job);
0945 
0946     Akonadi::Item::List items;
0947     for (Akonadi::Item item : std::as_const(origItems)) {
0948         const KDAV::DavItem davItem = davJob->item(item.remoteId());
0949 
0950         // No data was retrieved for this item, maybe because it is not out of date
0951         if (davItem.data().isEmpty()) {
0952             qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onMultigetFinished: Empty item returned. " << item.remoteId();
0953             if (!cache->isOutOfDate(item.remoteId())) {
0954                 qCDebug(DAVRESOURCE_LOG) << "DavGroupwareResource::onMultigetFinished: Item is not changed, including it. " << item.remoteId();
0955                 items << item;
0956             }
0957             continue;
0958         }
0959 
0960         Akonadi::Item::List extraItems;
0961         if (!Utils::parseDavData(davItem, item, extraItems)) {
0962             qCWarning(DAVRESOURCE_LOG) << "DavGroupwareResource::onMultigetFinished: Failed to parse item data. " << item.remoteId();
0963             continue;
0964         }
0965 
0966         // update etag
0967         item.setRemoteRevision(davItem.etag());
0968         cache->setEtag(item.remoteId(), davItem.etag());
0969         items << item;
0970         for (const Akonadi::Item &extraItem : std::as_const(extraItems)) {
0971             cache->setEtag(extraItem.remoteId(), davItem.etag());
0972             items << extraItem;
0973         }
0974     }
0975 
0976     // Update the collection CTag attribute now as sync is done.
0977     if (mCTagCache.contains(collection.remoteId())) {
0978         auto CTagAttr = collection.attribute<CTagAttribute>(Collection::AddIfMissing);
0979         qCDebug(DAVRESOURCE_LOG) << "Updating collection CTag from" << CTagAttr->CTag() << "to" << mCTagCache.value(collection.remoteId());
0980         CTagAttr->setCTag(mCTagCache.value(collection.remoteId()));
0981         auto modifyJob = new Akonadi::CollectionModifyJob(collection);
0982         modifyJob->start();
0983     }
0984 
0985     itemsRetrievedIncremental(items, Akonadi::Item::List());
0986 }
0987 
0988 void DavGroupwareResource::onRetrieveItemFinished(KJob *job)
0989 {
0990     onItemFetched(job, ItemUpdateAdd);
0991 }
0992 
0993 void DavGroupwareResource::onItemRefreshed(KJob *job)
0994 {
0995     ItemFetchUpdateType update = ItemUpdateChange;
0996     if (job->property("isRemoval").isValid() && job->property("isRemoval").toBool()) {
0997         update = ItemUpdateNone;
0998     }
0999 
1000     onItemFetched(job, update);
1001 }
1002 
1003 void DavGroupwareResource::onItemFetched(KJob *job, ItemFetchUpdateType updateType)
1004 {
1005     if (job->error()) {
1006         if (mSyncErrorNotified) {
1007             cancelTask();
1008         } else {
1009             cancelTask(i18n("Unable to retrieve item: %1", job->errorText()));
1010             mSyncErrorNotified = true;
1011         }
1012         return;
1013     }
1014 
1015     const KDAV::DavItemFetchJob *fetchJob = qobject_cast<KDAV::DavItemFetchJob *>(job);
1016     const KDAV::DavItem davItem = fetchJob->item();
1017     auto item = fetchJob->property("item").value<Akonadi::Item>();
1018     auto collection = fetchJob->property("collection").value<Akonadi::Collection>();
1019 
1020     Akonadi::Item::List extraItems;
1021     if (!Utils::parseDavData(davItem, item, extraItems)) {
1022         qCWarning(DAVRESOURCE_LOG) << "DavGroupwareResource::onItemFetched: Failed to parse item data. " << item.remoteId();
1023         // We get some XML error when the item doesn't exist:
1024         // <d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns">
1025         //  <s:sabredav-version>2.1.11</s:sabredav-version>
1026         //  <s:exception>Sabre\DAV\Exception\NotFound</s:exception>
1027         //  <s:message>Calendar object not found</s:message>
1028         //</d:error>
1029         cancelTask(i18n("Unable to retrieve item: failed to parse item data. Maybe it was deleted already."));
1030         return;
1031     }
1032 
1033     // update etag
1034     item.setRemoteRevision(davItem.etag());
1035     auto etag = mEtagCaches[collection.remoteId()];
1036     etag->setEtag(item.remoteId(), davItem.etag());
1037 
1038     if (!extraItems.isEmpty()) {
1039         for (int i = 0, total = extraItems.size(); i < total; ++i) {
1040             etag->setEtag(extraItems.at(i).remoteId(), davItem.etag());
1041         }
1042 
1043         auto j = new Akonadi::ItemModifyJob(extraItems);
1044         j->setIgnorePayload(true);
1045     }
1046 
1047     if (updateType == ItemUpdateChange) {
1048         changeCommitted(item);
1049     } else if (updateType == ItemUpdateAdd) {
1050         itemRetrieved(item);
1051     }
1052 }
1053 
1054 void DavGroupwareResource::onItemAddedFinished(KJob *job)
1055 {
1056     const KDAV::DavItemCreateJob *createJob = qobject_cast<KDAV::DavItemCreateJob *>(job);
1057     KDAV::DavItem davItem = createJob->item();
1058     auto item = createJob->property("item").value<Akonadi::Item>();
1059     item.setRemoteId(davItem.url().toDisplayString());
1060 
1061     if (createJob->error()) {
1062         qCCritical(DAVRESOURCE_LOG) << "Error when uploading item:" << createJob->error() << createJob->errorString();
1063         if (createJob->canRetryLater()) {
1064             retryAfterFailure(createJob->errorString());
1065         } else {
1066             cancelTask(i18n("Unable to add item: %1", createJob->errorString()));
1067         }
1068         return;
1069     }
1070 
1071     auto collection = createJob->property("collection").value<Akonadi::Collection>();
1072 
1073     if (davItem.etag().isEmpty()) {
1074         const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(collection.remoteId(), item.remoteId());
1075         davItem.setUrl(davUrl);
1076         auto fetchJob = new KDAV::DavItemFetchJob(davItem);
1077         fetchJob->setProperty("item", QVariant::fromValue(item));
1078         fetchJob->setProperty("collection", QVariant::fromValue(collection));
1079         connect(fetchJob, &KDAV::DavItemFetchJob::result, this, &DavGroupwareResource::onItemRefreshed);
1080         fetchJob->start();
1081     } else {
1082         item.setRemoteRevision(davItem.etag());
1083         mEtagCaches[collection.remoteId()]->setEtag(davItem.url().toDisplayString(), davItem.etag());
1084         changeCommitted(item);
1085     }
1086 }
1087 
1088 void DavGroupwareResource::onItemChangedFinished(KJob *job)
1089 {
1090     const KDAV::DavItemModifyJob *modifyJob = qobject_cast<KDAV::DavItemModifyJob *>(job);
1091     KDAV::DavItem davItem = modifyJob->item();
1092     auto collection = modifyJob->property("collection").value<Akonadi::Collection>();
1093     auto item = modifyJob->property("item").value<Akonadi::Item>();
1094     auto dependentItems = modifyJob->property("dependentItems").value<Akonadi::Item::List>();
1095     bool isRemoval = modifyJob->property("isRemoval").isValid() && modifyJob->property("isRemoval").toBool();
1096     auto cache = mEtagCaches.value(collection.remoteId());
1097     if (!cache) {
1098         qCDebug(DAVRESOURCE_LOG) << "Collection has disappeared during item fetch!";
1099         cancelTask();
1100         return;
1101     }
1102 
1103     if (modifyJob->error()) {
1104         qCCritical(DAVRESOURCE_LOG) << "Error when uploading item:" << modifyJob->error() << modifyJob->errorString();
1105         if (modifyJob->hasConflict()) {
1106             handleConflict(item, dependentItems, modifyJob->freshItem(), isRemoval, modifyJob->freshResponseCode());
1107         } else if (modifyJob->canRetryLater()) {
1108             retryAfterFailure(modifyJob->errorString());
1109         } else {
1110             cancelTask(i18n("Unable to change item: %1", modifyJob->errorString()));
1111         }
1112         return;
1113     }
1114 
1115     if (isRemoval) {
1116         auto removedItem = job->property("removedItem").value<Akonadi::Item>();
1117         if (removedItem.isValid()) {
1118             cache->removeEtag(removedItem.remoteId());
1119             changeProcessed();
1120         }
1121     }
1122 
1123     if (davItem.etag().isEmpty()) {
1124         const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(item.parentCollection().remoteId(), item.remoteId());
1125         davItem.setUrl(davUrl);
1126         auto fetchJob = new KDAV::DavItemFetchJob(davItem);
1127         fetchJob->setProperty("item", QVariant::fromValue(item));
1128         fetchJob->setProperty("collection", QVariant::fromValue(collection));
1129         fetchJob->setProperty("dependentItems", QVariant::fromValue(dependentItems));
1130         fetchJob->setProperty("isRemoval", QVariant::fromValue(isRemoval));
1131         connect(fetchJob, &KDAV::DavItemsFetchJob::result, this, &DavGroupwareResource::onItemRefreshed);
1132         fetchJob->start();
1133     } else {
1134         if (!isRemoval) {
1135             item.setRemoteRevision(davItem.etag());
1136             cache->setEtag(davItem.url().toDisplayString(), davItem.etag());
1137             changeCommitted(item);
1138         }
1139 
1140         if (!dependentItems.isEmpty()) {
1141             for (int i = 0, total = dependentItems.size(); i < total; ++i) {
1142                 dependentItems[i].setRemoteRevision(davItem.etag());
1143                 cache->setEtag(dependentItems.at(i).remoteId(), davItem.etag());
1144             }
1145 
1146             auto j = new Akonadi::ItemModifyJob(dependentItems);
1147             j->setIgnorePayload(true);
1148         }
1149     }
1150 }
1151 
1152 void DavGroupwareResource::onDeletedItemRecreated(KJob *job)
1153 {
1154     const KDAV::DavItemCreateJob *createJob = qobject_cast<KDAV::DavItemCreateJob *>(job);
1155     KDAV::DavItem davItem = createJob->item();
1156     auto item = createJob->property("item").value<Akonadi::Item>();
1157     Akonadi::Collection collection = item.parentCollection();
1158     auto dependentItems = createJob->property("dependentItems").value<Akonadi::Item::List>();
1159 
1160     if (davItem.etag().isEmpty()) {
1161         const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(item.parentCollection().remoteId(), item.remoteId());
1162         davItem.setUrl(davUrl);
1163         auto fetchJob = new KDAV::DavItemFetchJob(davItem);
1164         fetchJob->setProperty("item", QVariant::fromValue(item));
1165         fetchJob->setProperty("dependentItems", QVariant::fromValue(dependentItems));
1166         connect(fetchJob, &KDAV::DavItemFetchJob::result, this, &DavGroupwareResource::onItemRefreshed);
1167         fetchJob->start();
1168     } else {
1169         item.setRemoteRevision(davItem.etag());
1170         auto etag = mEtagCaches[collection.remoteId()];
1171         etag->setEtag(davItem.url().toDisplayString(), davItem.etag());
1172         changeCommitted(item);
1173 
1174         if (!dependentItems.isEmpty()) {
1175             for (int i = 0, total = dependentItems.size(); i < total; ++i) {
1176                 dependentItems[i].setRemoteRevision(davItem.etag());
1177                 etag->setEtag(dependentItems.at(i).remoteId(), davItem.etag());
1178             }
1179 
1180             auto j = new Akonadi::ItemModifyJob(dependentItems);
1181             j->setIgnorePayload(true);
1182         }
1183     }
1184 }
1185 
1186 void DavGroupwareResource::onItemRemovedFinished(KJob *job)
1187 {
1188     if (job->error()) {
1189         const KDAV::DavItemDeleteJob *deleteJob = qobject_cast<KDAV::DavItemDeleteJob *>(job);
1190 
1191         if (deleteJob->hasConflict()) {
1192             // Use a shortcut here as we don't show a conflict dialog to the user.
1193             handleConflict(Akonadi::Item(), Akonadi::Item::List(), deleteJob->freshItem(), true, 0);
1194         } else if (deleteJob->canRetryLater()) {
1195             retryAfterFailure(job->errorString());
1196         } else {
1197             cancelTask(i18n("Unable to remove item: %1", job->errorString()));
1198         }
1199     } else {
1200         auto item = job->property("item").value<Akonadi::Item>();
1201         auto collection = job->property("collection").value<Akonadi::Collection>();
1202         mEtagCaches[collection.remoteId()]->removeEtag(item.remoteId());
1203         changeProcessed();
1204     }
1205 }
1206 
1207 void DavGroupwareResource::onCollectionDiscovered(KDAV::Protocol protocol, const QString &collection, const QString &config)
1208 {
1209     Settings::self()->addCollectionUrlMapping(protocol, collection, config);
1210 }
1211 
1212 void DavGroupwareResource::handleConflict(const Item &lI, const Item::List &localDependentItems, const KDAV::DavItem &rI, bool isLocalRemoval, int responseCode)
1213 {
1214     Akonadi::Item localItem(lI);
1215     Akonadi::Item remoteItem, tmpRemoteItem; // The tmp* vars are here to store the result of the parseDavData() call
1216     Akonadi::Item::List remoteDependentItems, tmpRemoteDependentItems; // as we have no idea which item triggered the conflict.
1217     qCDebug(DAVRESOURCE_LOG) << "Fresh response code is" << responseCode;
1218     bool isRemoteRemoval = (responseCode == 404 || responseCode == 410);
1219 
1220     if (!isRemoteRemoval) {
1221         if (!Utils::parseDavData(rI, tmpRemoteItem, tmpRemoteDependentItems)) {
1222             // TODO: set a more correct error message here
1223             cancelTask(i18n("Unable to change item: %1", QStringLiteral("conflict resolution failed")));
1224             return;
1225             // TODO: we can end up here if the remote item was deleted
1226         }
1227 
1228         // Now try to find the item that really triggered the conflict
1229         const Akonadi::Item::List allRemoteItems = Akonadi::Item::List() << tmpRemoteItem << tmpRemoteDependentItems;
1230         for (const Akonadi::Item &tmpItem : allRemoteItems) {
1231             if (tmpItem.payloadData() != localItem.payloadData()) {
1232                 if (remoteItem.isValid()) {
1233                     // Oops, we can only manage one changed item at this stage, sorry...
1234                     // TODO: make this translatable
1235                     cancelTask(i18n("Unable to change item: %1", QStringLiteral("more than one item was changed in the backend")));
1236                     return;
1237                 }
1238                 remoteItem = tmpItem;
1239             } else {
1240                 remoteDependentItems << tmpItem;
1241             }
1242         }
1243     }
1244 
1245     if (isLocalRemoval) {
1246         // TODO: implement with the configurable strategy
1247         /*
1248          * Here by default we don't delete an event that was modified in the backend, and
1249          * instead we just abort the current task.
1250          * Also, trigger an immediate sync to refresh the item.
1251          */
1252         qCDebug(DAVRESOURCE_LOG) << "Local removal conflict";
1253         // TODO: make this translatable
1254         cancelTask(i18n("Unable to remove item: %1", QStringLiteral("it was changed in the backend in the meantime")));
1255         synchronize();
1256     } else if (isRemoteRemoval) {
1257         // TODO: implement with the configurable strategy
1258         /*
1259          * Here also it is a bit tricky to clear the item in the local cache as the resource
1260          * will not get notified if the user chooses to delete the item and abandon the local
1261          * modification. For the time being let's just re-upload the changed item.
1262          */
1263         qCDebug(DAVRESOURCE_LOG) << "Remote removal conflict";
1264         Akonadi::Collection collection = localItem.parentCollection();
1265         KDAV::DavItem davItem = Utils::createDavItem(localItem, collection, localDependentItems);
1266 
1267         QString urlStr = localItem.remoteId();
1268         if (urlStr.contains(QLatin1Char('#'))) {
1269             urlStr.truncate(urlStr.indexOf(QLatin1Char('#')));
1270         }
1271         const KDAV::DavUrl davUrl = Settings::self()->davUrlFromCollectionUrl(collection.remoteId(), urlStr);
1272         davItem.setUrl(davUrl);
1273 
1274         auto job = new KDAV::DavItemCreateJob(davItem);
1275         job->setProperty("item", QVariant::fromValue(localItem));
1276         job->setProperty("dependentItems", QVariant::fromValue(localDependentItems));
1277         connect(job, &KJob::result, this, &DavGroupwareResource::onDeletedItemRecreated);
1278         job->start();
1279     } else {
1280         const QString remoteEtag = rI.etag();
1281         Akonadi::Collection collection = localItem.parentCollection();
1282 
1283         localItem.setRemoteRevision(remoteEtag);
1284         changeCommitted(localItem);
1285 
1286         // Update the ETag cache in all cases as the new ETag will have to be used
1287         // later for any update or deletion
1288         mEtagCaches[collection.remoteId()]->setEtag(rI.url().toDisplayString(), remoteEtag);
1289 
1290         // The first step is to fire a first modify job that will replace the item currently
1291         // in the local cache with the one that was found in the backend.
1292         Akonadi::Item updatedItem(localItem);
1293         updatedItem.setPayloadFromData(remoteItem.payloadData());
1294         updatedItem.setRemoteRevision(remoteEtag);
1295         auto j = new Akonadi::ItemModifyJob(updatedItem);
1296         j->setIgnorePayload(false);
1297         j->start();
1298 
1299         // So now we have in the cache what's in the backend but the user is not aware
1300         // that behind the scenes something terrible is happening. Well, nearly...
1301         // To notify him of this, and due to the way the conflict handler works, we have
1302         // to re-attempt a modification to revert the modify job that was just fired.
1303         // So yes, we are effectively re-submitting the client-provided content, but
1304         // with a revision that will trigger the conflict dialog.
1305         // The only problem is that the user will see that we update the item before
1306         // the conflict dialog has time to display (if it's not behind the application
1307         // window).
1308         localItem.setRevision(0);
1309         j = new Akonadi::ItemModifyJob(localItem);
1310         j->setIgnorePayload(false);
1311         connect(j, &KJob::result, this, &DavGroupwareResource::onConflictModifyJobFinished);
1312         j->start();
1313 
1314         // Hopefully for the dependent items everything will be fine. Right?
1315         // Not so sure in fact.
1316         if (!remoteDependentItems.isEmpty()) {
1317             auto etag = mEtagCaches[collection.remoteId()];
1318             for (int i = 0; i < remoteDependentItems.size(); ++i) {
1319                 remoteDependentItems[i].setRemoteRevision(remoteEtag);
1320                 etag->setEtag(remoteDependentItems.at(i).remoteId(), remoteEtag);
1321             }
1322 
1323             auto j = new Akonadi::ItemModifyJob(remoteDependentItems);
1324             j->setIgnorePayload(true);
1325         }
1326     }
1327 }
1328 
1329 void DavGroupwareResource::onConflictModifyJobFinished(KJob *job)
1330 {
1331     auto j = qobject_cast<Akonadi::ItemModifyJob *>(job);
1332     if (j->error()) {
1333         qCCritical(DAVRESOURCE_LOG) << "Conflict update failed: " << job->errorText();
1334         // TODO: what do we do now? We just committed an item that's in a weird state...
1335     }
1336 }
1337 
1338 bool DavGroupwareResource::configurationIsValid()
1339 {
1340     if (Settings::self()->configuredDavUrls().empty()) {
1341         Q_EMIT status(NotConfigured, i18n("The resource is not configured yet"));
1342         cancelTask(i18n("The resource is not configured yet"));
1343         return false;
1344     }
1345 
1346     int newICT = Settings::self()->refreshInterval();
1347     if (newICT == 0) {
1348         newICT = -1;
1349     }
1350 
1351     if (newICT != mDavCollectionRoot.cachePolicy().intervalCheckTime()) {
1352         Akonadi::CachePolicy cachePolicy = mDavCollectionRoot.cachePolicy();
1353         cachePolicy.setIntervalCheckTime(newICT);
1354 
1355         mDavCollectionRoot.setCachePolicy(cachePolicy);
1356     }
1357 
1358     if (!Settings::self()->displayName().isEmpty()) {
1359         auto attribute = mDavCollectionRoot.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
1360         attribute->setDisplayName(Settings::self()->displayName());
1361         setName(Settings::self()->displayName());
1362     }
1363 
1364     return true;
1365 }
1366 
1367 void DavGroupwareResource::retryAfterFailure(const QString &errorMessage)
1368 {
1369     Q_EMIT status(Broken, errorMessage);
1370     deferTask();
1371     setTemporaryOffline(Settings::self()->refreshInterval() <= 0 ? 300 : Settings::self()->refreshInterval() * 60);
1372 }
1373 
1374 /*static*/
1375 void DavGroupwareResource::setCollectionIcon(Akonadi::Collection &collection)
1376 {
1377     const QStringList mimeTypes = collection.contentMimeTypes();
1378     if (mimeTypes.count() == 1) {
1379         QHash<QString, QString> mapping;
1380         mapping.insert(KCalendarCore::Event::eventMimeType(), QStringLiteral("view-calendar"));
1381         mapping.insert(KCalendarCore::Todo::todoMimeType(), QStringLiteral("view-calendar-tasks"));
1382         mapping.insert(KCalendarCore::Journal::journalMimeType(), QStringLiteral("view-pim-journal"));
1383         mapping.insert(KContacts::Addressee::mimeType(), QStringLiteral("view-pim-contacts"));
1384 
1385         const QString mimetypeFirst = mimeTypes.first();
1386         if (!mimetypeFirst.isEmpty()) {
1387             auto attribute = collection.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
1388             attribute->setIconName(mimetypeFirst);
1389         }
1390     }
1391 }
1392 
1393 AKONADI_RESOURCE_MAIN(DavGroupwareResource)
1394 
1395 #include "moc_davgroupwareresource.cpp"