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"