File indexing completed on 2024-05-19 05:11:18

0001 /*
0002   SPDX-FileCopyrightText: 2004 Reinhold Kainhofer <reinhold@kainhofer.com>
0003   SPDX-FileCopyrightText: 2010-2012 Sérgio Martins <iamsergio@gmail.com>
0004 
0005   SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 #include "incidencechanger.h"
0008 #include "akonadicalendar_debug.h"
0009 #include "calendarutils.h"
0010 #include "incidencechanger_p.h"
0011 #include "mailscheduler_p.h"
0012 #include "utils_p.h"
0013 #include <Akonadi/ItemCreateJob>
0014 #include <Akonadi/ItemDeleteJob>
0015 #include <Akonadi/ItemModifyJob>
0016 #include <Akonadi/TransactionSequence>
0017 
0018 #include <KGuiItem>
0019 #include <KJob>
0020 #include <KLocalizedString>
0021 #include <KMessageBox>
0022 
0023 #include <QBitArray>
0024 
0025 using namespace Akonadi;
0026 using namespace KCalendarCore;
0027 
0028 AKONADI_CALENDAR_TESTS_EXPORT bool akonadi_calendar_running_unittests = false;
0029 
0030 static ITIPHandlerDialogDelegate::Action actionFromStatus(ITIPHandlerHelper::SendResult result)
0031 {
0032     // enum SendResult {
0033     //      Canceled,        /**< Sending was canceled by the user, meaning there are
0034     //                          local changes of which other attendees are not aware. */
0035     //      FailKeepUpdate,  /**< Sending failed, the changes to the incidence must be kept. */
0036     //      FailAbortUpdate, /**< Sending failed, the changes to the incidence must be undone. */
0037     //      NoSendingNeeded, /**< In some cases it is not needed to send an invitation
0038     //                          (e.g. when we are the only attendee) */
0039     //      Success
0040     switch (result) {
0041     case ITIPHandlerHelper::ResultCanceled:
0042         return ITIPHandlerDialogDelegate::ActionDontSendMessage;
0043     case ITIPHandlerHelper::ResultSuccess:
0044         return ITIPHandlerDialogDelegate::ActionSendMessage;
0045     default:
0046         return ITIPHandlerDialogDelegate::ActionAsk;
0047     }
0048 }
0049 
0050 static bool weAreOrganizer(const Incidence::Ptr &incidence)
0051 {
0052     const QString email = incidence->organizer().email();
0053     return Akonadi::CalendarUtils::thatIsMe(email);
0054 }
0055 
0056 static bool allowedModificationsWithoutRevisionUpdate(const Incidence::Ptr &incidence)
0057 {
0058     // Modifications that are per user allowed without getting outofsync with organisator
0059     // * if only alarm settings are modified.
0060     const QSet<KCalendarCore::IncidenceBase::Field> dirtyFields = incidence->dirtyFields();
0061     QSet<KCalendarCore::IncidenceBase::Field> alarmOnlyModify;
0062     alarmOnlyModify << IncidenceBase::FieldAlarms << IncidenceBase::FieldLastModified;
0063     return dirtyFields == alarmOnlyModify;
0064 }
0065 
0066 static void updateHandlerPrivacyPolicy(ITIPHandlerHelper *helper, IncidenceChanger::InvitationPrivacyFlags flags)
0067 {
0068     ITIPHandlerHelper::MessagePrivacyFlags helperFlags;
0069     helperFlags.setFlag(ITIPHandlerHelper::MessagePrivacySign, (flags & IncidenceChanger::InvitationPrivacySign) == IncidenceChanger::InvitationPrivacySign);
0070     helperFlags.setFlag(ITIPHandlerHelper::MessagePrivacyEncrypt,
0071                         (flags & IncidenceChanger::InvitationPrivacyEncrypt) == IncidenceChanger::InvitationPrivacyEncrypt);
0072     helper->setMessagePrivacy(helperFlags);
0073 }
0074 
0075 namespace Akonadi
0076 {
0077 // Does a queued emit, with QMetaObject::invokeMethod
0078 static void emitCreateFinished(IncidenceChanger *changer,
0079                                int changeId,
0080                                const Akonadi::Item &item,
0081                                Akonadi::IncidenceChanger::ResultCode resultCode,
0082                                const QString &errorString)
0083 {
0084     QMetaObject::invokeMethod(changer,
0085                               "createFinished",
0086                               Qt::QueuedConnection,
0087                               Q_ARG(int, changeId),
0088                               Q_ARG(Akonadi::Item, item),
0089                               Q_ARG(Akonadi::IncidenceChanger::ResultCode, resultCode),
0090                               Q_ARG(QString, errorString));
0091 }
0092 
0093 // Does a queued emit, with QMetaObject::invokeMethod
0094 static void
0095 emitModifyFinished(IncidenceChanger *changer, int changeId, const Akonadi::Item &item, IncidenceChanger::ResultCode resultCode, const QString &errorString)
0096 {
0097     QMetaObject::invokeMethod(changer,
0098                               "modifyFinished",
0099                               Qt::QueuedConnection,
0100                               Q_ARG(int, changeId),
0101                               Q_ARG(Akonadi::Item, item),
0102                               Q_ARG(Akonadi::IncidenceChanger::ResultCode, resultCode),
0103                               Q_ARG(QString, errorString));
0104 }
0105 
0106 // Does a queued emit, with QMetaObject::invokeMethod
0107 static void emitDeleteFinished(IncidenceChanger *changer,
0108                                int changeId,
0109                                const QList<Akonadi::Item::Id> &itemIdList,
0110                                IncidenceChanger::ResultCode resultCode,
0111                                const QString &errorString)
0112 {
0113     QMetaObject::invokeMethod(changer,
0114                               "deleteFinished",
0115                               Qt::QueuedConnection,
0116                               Q_ARG(int, changeId),
0117                               Q_ARG(QList<Akonadi::Item::Id>, itemIdList),
0118                               Q_ARG(Akonadi::IncidenceChanger::ResultCode, resultCode),
0119                               Q_ARG(QString, errorString));
0120 }
0121 }
0122 
0123 using IdToRevisionHash = QHash<Akonadi::Item::Id, int>;
0124 Q_GLOBAL_STATIC(IdToRevisionHash, s_latestRevisionByItemId)
0125 
0126 IncidenceChangerPrivate::IncidenceChangerPrivate(bool enableHistory, ITIPHandlerComponentFactory *factory, IncidenceChanger *qq)
0127     : q(qq)
0128 {
0129     mLatestChangeId = 0;
0130     mShowDialogsOnError = true;
0131     mFactory = factory ? factory : new ITIPHandlerComponentFactory(this);
0132     mHistory = enableHistory ? new History(this) : nullptr;
0133     mUseHistory = enableHistory;
0134     mDestinationPolicy = IncidenceChanger::DestinationPolicyDefault;
0135     mRespectsCollectionRights = false;
0136     mGroupwareCommunication = false;
0137     mLatestAtomicOperationId = 0;
0138     mBatchOperationInProgress = false;
0139     mAutoAdjustRecurrence = true;
0140     m_collectionFetchJob = nullptr;
0141     m_invitationPolicy = IncidenceChanger::InvitationPolicyAsk;
0142 
0143     qRegisterMetaType<QList<Akonadi::Item::Id>>("QList<Akonadi::Item::Id>");
0144     qRegisterMetaType<Akonadi::Item::Id>("Akonadi::Item::Id");
0145     qRegisterMetaType<Akonadi::Item>("Akonadi::Item");
0146     qRegisterMetaType<Akonadi::IncidenceChanger::ResultCode>("Akonadi::IncidenceChanger::ResultCode");
0147     qRegisterMetaType<ITIPHandlerHelper::SendResult>("ITIPHandlerHelper::SendResult");
0148 }
0149 
0150 IncidenceChangerPrivate::~IncidenceChangerPrivate()
0151 {
0152     if (!mAtomicOperations.isEmpty() || !mQueuedModifications.isEmpty() || !mModificationsInProgress.isEmpty()) {
0153         qCDebug(AKONADICALENDAR_LOG) << "Normal if the application was being used. "
0154                                         "But might indicate a memory leak if it wasn't";
0155     }
0156 }
0157 
0158 bool IncidenceChangerPrivate::atomicOperationIsValid(uint atomicOperationId) const
0159 {
0160     // Changes must be done between startAtomicOperation() and endAtomicOperation()
0161     return mAtomicOperations.contains(atomicOperationId) && !mAtomicOperations[atomicOperationId]->m_endCalled;
0162 }
0163 
0164 bool IncidenceChangerPrivate::hasRights(const Collection &collection, IncidenceChanger::ChangeType changeType) const
0165 {
0166     bool result = false;
0167     switch (changeType) {
0168     case IncidenceChanger::ChangeTypeCreate:
0169         result = collection.rights() & Akonadi::Collection::CanCreateItem;
0170         break;
0171     case IncidenceChanger::ChangeTypeModify:
0172         result = collection.rights() & Akonadi::Collection::CanChangeItem;
0173         break;
0174     case IncidenceChanger::ChangeTypeDelete:
0175         result = collection.rights() & Akonadi::Collection::CanDeleteItem;
0176         break;
0177     default:
0178         Q_ASSERT_X(false, "hasRights", "invalid type");
0179     }
0180 
0181     return !collection.isValid() || !mRespectsCollectionRights || result;
0182 }
0183 
0184 Akonadi::Job *IncidenceChangerPrivate::parentJob(const Change::Ptr &change) const
0185 {
0186     return (mBatchOperationInProgress && !change->queuedModification) ? mAtomicOperations[mLatestAtomicOperationId]->transaction() : nullptr;
0187 }
0188 
0189 void IncidenceChangerPrivate::queueModification(const Change::Ptr &change)
0190 {
0191     // If there's already a change queued we just discard it
0192     // and send the newer change, which already includes
0193     // previous modifications
0194     const Akonadi::Item::Id id = change->newItem.id();
0195     if (mQueuedModifications.contains(id)) {
0196         Change::Ptr toBeDiscarded = mQueuedModifications.take(id);
0197         toBeDiscarded->resultCode = IncidenceChanger::ResultCodeModificationDiscarded;
0198         toBeDiscarded->completed = true;
0199         mChangeById.remove(toBeDiscarded->id);
0200     }
0201 
0202     change->queuedModification = true;
0203     mQueuedModifications[id] = change;
0204 }
0205 
0206 void IncidenceChangerPrivate::performNextModification(Akonadi::Item::Id id)
0207 {
0208     mModificationsInProgress.remove(id);
0209 
0210     if (mQueuedModifications.contains(id)) {
0211         const Change::Ptr change = mQueuedModifications.take(id);
0212         performModification(change);
0213     }
0214 }
0215 
0216 void IncidenceChangerPrivate::handleTransactionJobResult(KJob *job)
0217 {
0218     auto transaction = qobject_cast<TransactionSequence *>(job);
0219     Q_ASSERT(transaction);
0220     Q_ASSERT(mAtomicOperationByTransaction.contains(transaction));
0221 
0222     const uint atomicOperationId = mAtomicOperationByTransaction.take(transaction);
0223 
0224     Q_ASSERT(mAtomicOperations.contains(atomicOperationId));
0225     AtomicOperation *operation = mAtomicOperations[atomicOperationId];
0226     Q_ASSERT(operation);
0227     Q_ASSERT(operation->m_id == atomicOperationId);
0228     if (job->error()) {
0229         if (!operation->rolledback()) {
0230             operation->setRolledback();
0231         }
0232         qCritical() << "Transaction failed, everything was rolledback. " << job->errorString();
0233     } else {
0234         Q_ASSERT(operation->m_endCalled);
0235         Q_ASSERT(!operation->pendingJobs());
0236     }
0237 
0238     if (!operation->pendingJobs() && operation->m_endCalled) {
0239         delete mAtomicOperations.take(atomicOperationId);
0240         mBatchOperationInProgress = false;
0241     } else {
0242         operation->m_transactionCompleted = true;
0243     }
0244 }
0245 
0246 void IncidenceChangerPrivate::handleCreateJobResult(KJob *job)
0247 {
0248     Change::Ptr change = mChangeForJob.take(job);
0249 
0250     const auto j = qobject_cast<const ItemCreateJob *>(job);
0251     Q_ASSERT(j);
0252     Akonadi::Item item = j->item();
0253 
0254     if (j->error()) {
0255         const QString errorString = j->errorString();
0256         IncidenceChanger::ResultCode resultCode = IncidenceChanger::ResultCodeJobError;
0257         item = change->newItem;
0258         qCritical() << errorString;
0259         if (mShowDialogsOnError) {
0260             KMessageBox::error(change->parentWidget, i18n("Error while trying to create calendar item. Error was: %1", errorString));
0261         }
0262         mChangeById.remove(change->id);
0263         change->errorString = errorString;
0264         change->resultCode = resultCode;
0265         // puff, change finally goes out of scope, and emits the incidenceCreated signal.
0266     } else {
0267         Q_ASSERT(item.isValid());
0268         Q_ASSERT(item.hasPayload<KCalendarCore::Incidence::Ptr>());
0269         change->newItem = item;
0270 
0271         if (change->useGroupwareCommunication) {
0272             connect(change.data(), &Change::dialogClosedAfterChange, this, &IncidenceChangerPrivate::handleCreateJobResult2);
0273             handleInvitationsAfterChange(change);
0274         } else {
0275             handleCreateJobResult2(change->id, ITIPHandlerHelper::ResultSuccess);
0276         }
0277     }
0278 }
0279 
0280 void IncidenceChangerPrivate::handleCreateJobResult2(int changeId, ITIPHandlerHelper::SendResult status)
0281 {
0282     Change::Ptr change = mChangeById[changeId];
0283     Akonadi::Item item = change->newItem;
0284 
0285     mChangeById.remove(changeId);
0286 
0287     if (status == ITIPHandlerHelper::ResultFailAbortUpdate) {
0288         qCritical() << "Sending invitations failed, but did not delete the incidence";
0289     }
0290 
0291     const uint atomicOperationId = change->atomicOperationId;
0292     if (atomicOperationId != 0) {
0293         mInvitationStatusByAtomicOperation.insert(atomicOperationId, status);
0294     }
0295 
0296     QString description;
0297     if (change->atomicOperationId != 0) {
0298         AtomicOperation *a = mAtomicOperations[change->atomicOperationId];
0299         ++a->m_numCompletedChanges;
0300         change->completed = true;
0301         description = a->m_description;
0302     }
0303 
0304     // for user undo/redo
0305     if (change->recordToHistory) {
0306         mHistory->recordCreation(item, description, change->atomicOperationId);
0307     }
0308 
0309     change->errorString = QString();
0310     change->resultCode = IncidenceChanger::ResultCodeSuccess;
0311     // puff, change finally goes out of scope, and emits the incidenceCreated signal.
0312 }
0313 
0314 void IncidenceChangerPrivate::handleDeleteJobResult(KJob *job)
0315 {
0316     Change::Ptr change = mChangeForJob.take(job);
0317 
0318     const auto j = qobject_cast<const ItemDeleteJob *>(job);
0319     const Item::List items = j->deletedItems();
0320 
0321     QSharedPointer<DeletionChange> deletionChange = change.staticCast<DeletionChange>();
0322 
0323     deletionChange->mItemIds.reserve(deletionChange->mItemIds.count() + items.count());
0324     for (const Akonadi::Item &item : items) {
0325         deletionChange->mItemIds.append(item.id());
0326     }
0327     QString description;
0328     if (change->atomicOperationId != 0) {
0329         AtomicOperation *a = mAtomicOperations[change->atomicOperationId];
0330         a->m_numCompletedChanges++;
0331         change->completed = true;
0332         description = a->m_description;
0333     }
0334     if (j->error()) {
0335         const QString errorString = j->errorString();
0336         qCritical() << errorString;
0337 
0338         if (mShowDialogsOnError) {
0339             KMessageBox::error(change->parentWidget, i18n("Error while trying to delete calendar item. Error was: %1", errorString));
0340         }
0341 
0342         for (const Item &item : items) {
0343             // Weren't deleted due to error
0344             mDeletedItemIds.remove(mDeletedItemIds.indexOf(item.id()));
0345         }
0346         mChangeById.remove(change->id);
0347         change->resultCode = IncidenceChanger::ResultCodeJobError;
0348         change->errorString = errorString;
0349         change->emitCompletionSignal();
0350     } else { // success
0351         if (change->recordToHistory) {
0352             Q_ASSERT(mHistory);
0353             mHistory->recordDeletions(items, description, change->atomicOperationId);
0354         }
0355 
0356         if (change->useGroupwareCommunication) {
0357             connect(change.data(), &Change::dialogClosedAfterChange, this, &IncidenceChangerPrivate::handleDeleteJobResult2);
0358             handleInvitationsAfterChange(change);
0359         } else {
0360             handleDeleteJobResult2(change->id, ITIPHandlerHelper::ResultSuccess);
0361         }
0362     }
0363 }
0364 
0365 void IncidenceChangerPrivate::handleDeleteJobResult2(int changeId, ITIPHandlerHelper::SendResult status)
0366 {
0367     Change::Ptr change = mChangeById[changeId];
0368     mChangeById.remove(change->id);
0369 
0370     if (status == ITIPHandlerHelper::ResultSuccess) {
0371         change->errorString = QString();
0372         change->resultCode = IncidenceChanger::ResultCodeSuccess;
0373     } else {
0374         change->errorString = i18nc("errormessage for a job ended with an unexpected result", "An unknown error occurred");
0375         change->resultCode = IncidenceChanger::ResultCodeJobError;
0376     }
0377 
0378     // puff, change finally goes out of scope, and emits the incidenceDeleted signal.
0379 }
0380 
0381 void IncidenceChangerPrivate::handleModifyJobResult(KJob *job)
0382 {
0383     Change::Ptr change = mChangeForJob.take(job);
0384 
0385     const auto j = qobject_cast<const ItemModifyJob *>(job);
0386     const Item item = j->item();
0387     Q_ASSERT(mDirtyFieldsByJob.contains(job));
0388     Q_ASSERT(item.hasPayload<KCalendarCore::Incidence::Ptr>());
0389     const QSet<KCalendarCore::IncidenceBase::Field> dirtyFields = mDirtyFieldsByJob.value(job);
0390     item.payload<KCalendarCore::Incidence::Ptr>()->setDirtyFields(dirtyFields);
0391     QString description;
0392     if (change->atomicOperationId != 0) {
0393         AtomicOperation *a = mAtomicOperations[change->atomicOperationId];
0394         a->m_numCompletedChanges++;
0395         change->completed = true;
0396         description = a->m_description;
0397     }
0398     if (j->error()) {
0399         const QString errorString = j->errorString();
0400         IncidenceChanger::ResultCode resultCode = IncidenceChanger::ResultCodeJobError;
0401         if (deleteAlreadyCalled(item.id())) {
0402             // User deleted the item almost at the same time he changed it. We could just return success
0403             // but the delete is probably already recorded to History, and that would make undo not work
0404             // in the proper order.
0405             resultCode = IncidenceChanger::ResultCodeAlreadyDeleted;
0406             qCWarning(AKONADICALENDAR_LOG) << "Trying to change item " << item.id() << " while deletion is in progress.";
0407         } else {
0408             qCritical() << errorString;
0409         }
0410         if (mShowDialogsOnError) {
0411             KMessageBox::error(change->parentWidget, i18n("Error while trying to modify calendar item. Error was: %1", errorString));
0412         }
0413         mChangeById.remove(change->id);
0414         change->errorString = errorString;
0415         change->resultCode = resultCode;
0416         // puff, change finally goes out of scope, and emits the incidenceModified signal.
0417 
0418         QMetaObject::invokeMethod(this, "performNextModification", Qt::QueuedConnection, Q_ARG(Akonadi::Item::Id, item.id()));
0419     } else { // success
0420         (*(s_latestRevisionByItemId()))[item.id()] = item.revision();
0421         change->newItem = item;
0422         if (change->recordToHistory && !change->originalItems.isEmpty()) {
0423             Q_ASSERT(change->originalItems.count() == 1);
0424             mHistory->recordModification(change->originalItems.constFirst(), item, description, change->atomicOperationId);
0425         }
0426 
0427         if (change->useGroupwareCommunication) {
0428             connect(change.data(), &Change::dialogClosedAfterChange, this, &IncidenceChangerPrivate::handleModifyJobResult2);
0429             handleInvitationsAfterChange(change);
0430         } else {
0431             handleModifyJobResult2(change->id, ITIPHandlerHelper::ResultSuccess);
0432         }
0433     }
0434 }
0435 
0436 void IncidenceChangerPrivate::handleModifyJobResult2(int changeId, ITIPHandlerHelper::SendResult status)
0437 {
0438     Change::Ptr change = mChangeById[changeId];
0439 
0440     mChangeById.remove(changeId);
0441     if (change->atomicOperationId != 0) {
0442         mInvitationStatusByAtomicOperation.insert(change->atomicOperationId, status);
0443     }
0444     change->errorString = QString();
0445     change->resultCode = IncidenceChanger::ResultCodeSuccess;
0446     // puff, change finally goes out of scope, and emits the incidenceModified signal.
0447 
0448     QMetaObject::invokeMethod(this, "performNextModification", Qt::QueuedConnection, Q_ARG(Akonadi::Item::Id, change->newItem.id()));
0449 }
0450 
0451 bool IncidenceChangerPrivate::deleteAlreadyCalled(Akonadi::Item::Id id) const
0452 {
0453     return mDeletedItemIds.contains(id);
0454 }
0455 
0456 void IncidenceChangerPrivate::handleInvitationsBeforeChange(const Change::Ptr &change)
0457 {
0458     if (mGroupwareCommunication) {
0459         ITIPHandlerHelper::SendResult result = ITIPHandlerHelper::ResultSuccess;
0460         switch (change->type) {
0461         case IncidenceChanger::ChangeTypeCreate:
0462             // nothing needs to be done
0463             break;
0464         case IncidenceChanger::ChangeTypeDelete: {
0465             ITIPHandlerHelper::SendResult status;
0466             bool sendOk = true;
0467             Q_ASSERT(!change->originalItems.isEmpty());
0468 
0469             auto handler = new ITIPHandlerHelper(mFactory, change->parentWidget);
0470             handler->setParent(this);
0471             updateHandlerPrivacyPolicy(handler, m_invitationPrivacy);
0472 
0473             if (m_invitationPolicy == IncidenceChanger::InvitationPolicySend) {
0474                 handler->setDefaultAction(ITIPHandlerDialogDelegate::ActionSendMessage);
0475             } else if (m_invitationPolicy == IncidenceChanger::InvitationPolicyDontSend) {
0476                 handler->setDefaultAction(ITIPHandlerDialogDelegate::ActionDontSendMessage);
0477             } else if (mInvitationStatusByAtomicOperation.contains(change->atomicOperationId)) {
0478                 handler->setDefaultAction(actionFromStatus(mInvitationStatusByAtomicOperation.value(change->atomicOperationId)));
0479             }
0480 
0481             connect(handler, &ITIPHandlerHelper::finished, change.data(), &Change::emitUserDialogClosedBeforeChange);
0482 
0483             for (const Akonadi::Item &item : std::as_const(change->originalItems)) {
0484                 Q_ASSERT(item.hasPayload<KCalendarCore::Incidence::Ptr>());
0485                 Incidence::Ptr incidence = CalendarUtils::incidence(item);
0486                 if (!incidence->supportsGroupwareCommunication()) {
0487                     continue;
0488                 }
0489                 // We only send CANCEL if we're the organizer.
0490                 // If we're not, then we send REPLY with PartStat=Declined in handleInvitationsAfterChange()
0491                 if (Akonadi::CalendarUtils::thatIsMe(incidence->organizer().email())) {
0492                     // TODO: not to popup all delete message dialogs at once :(
0493                     sendOk = false;
0494                     handler->sendIncidenceDeletedMessage(KCalendarCore::iTIPCancel, incidence);
0495                     if (change->atomicOperationId) {
0496                         mInvitationStatusByAtomicOperation.insert(change->atomicOperationId, status);
0497                     }
0498                     // TODO: with some status we want to break immediately
0499                 }
0500             }
0501 
0502             if (sendOk) {
0503                 change->emitUserDialogClosedBeforeChange(result);
0504             }
0505             return;
0506         }
0507         case IncidenceChanger::ChangeTypeModify: {
0508             if (change->originalItems.isEmpty()) {
0509                 break;
0510             }
0511 
0512             Q_ASSERT(change->originalItems.count() == 1);
0513             Incidence::Ptr oldIncidence = CalendarUtils::incidence(change->originalItems.first());
0514             Incidence::Ptr newIncidence = CalendarUtils::incidence(change->newItem);
0515 
0516             if (!oldIncidence->supportsGroupwareCommunication()) {
0517                 break;
0518             }
0519 
0520             if (allowedModificationsWithoutRevisionUpdate(newIncidence)) {
0521                 change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultSuccess);
0522                 return;
0523             }
0524 
0525             if (akonadi_calendar_running_unittests && !weAreOrganizer(newIncidence)) {
0526                 // This is a bit of a workaround when running tests. I don't want to show the
0527                 // "You're not organizer, do you want to modify event?" dialog in unit-tests, but want
0528                 // to emulate a "yes" and a "no" press.
0529                 if (m_invitationPolicy == IncidenceChanger::InvitationPolicySend) {
0530                     change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultSuccess);
0531                     return;
0532                 } else if (m_invitationPolicy == IncidenceChanger::InvitationPolicyDontSend) {
0533                     change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultCanceled);
0534                     return;
0535                 }
0536             }
0537 
0538             ITIPHandlerHelper handler(mFactory, change->parentWidget);
0539             const bool modify = handler.handleIncidenceAboutToBeModified(newIncidence);
0540             if (modify) {
0541                 break;
0542             } else {
0543                 result = ITIPHandlerHelper::ResultCanceled;
0544             }
0545 
0546             if (newIncidence->type() == oldIncidence->type()) {
0547                 IncidenceBase *i1 = newIncidence.data();
0548                 IncidenceBase *i2 = oldIncidence.data();
0549                 *i1 = *i2;
0550             }
0551             break;
0552         }
0553         default:
0554             Q_ASSERT(false);
0555             result = ITIPHandlerHelper::ResultCanceled;
0556         }
0557         change->emitUserDialogClosedBeforeChange(result);
0558     } else {
0559         change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultSuccess);
0560     }
0561 }
0562 
0563 void IncidenceChangerPrivate::handleInvitationsAfterChange(const Change::Ptr &change)
0564 {
0565     if (change->useGroupwareCommunication) {
0566         auto handler = new ITIPHandlerHelper(mFactory, change->parentWidget);
0567         connect(handler, &ITIPHandlerHelper::finished, change.data(), &Change::emitUserDialogClosedAfterChange);
0568         handler->setParent(this);
0569         updateHandlerPrivacyPolicy(handler, m_invitationPrivacy);
0570 
0571         const bool alwaysSend = (m_invitationPolicy == IncidenceChanger::InvitationPolicySend);
0572         const bool neverSend = (m_invitationPolicy == IncidenceChanger::InvitationPolicyDontSend);
0573         if (alwaysSend) {
0574             handler->setDefaultAction(ITIPHandlerDialogDelegate::ActionSendMessage);
0575         }
0576 
0577         if (neverSend) {
0578             handler->setDefaultAction(ITIPHandlerDialogDelegate::ActionDontSendMessage);
0579         }
0580 
0581         switch (change->type) {
0582         case IncidenceChanger::ChangeTypeCreate: {
0583             Incidence::Ptr incidence = CalendarUtils::incidence(change->newItem);
0584             if (incidence->supportsGroupwareCommunication()) {
0585                 handler->sendIncidenceCreatedMessage(KCalendarCore::iTIPRequest, incidence);
0586                 return;
0587             }
0588             break;
0589         }
0590         case IncidenceChanger::ChangeTypeDelete:
0591             handler->deleteLater();
0592             handler = nullptr;
0593             Q_ASSERT(!change->originalItems.isEmpty());
0594             for (const Akonadi::Item &item : std::as_const(change->originalItems)) {
0595                 Q_ASSERT(item.hasPayload());
0596                 Incidence::Ptr incidence = CalendarUtils::incidence(item);
0597                 Q_ASSERT(incidence);
0598                 if (!incidence->supportsGroupwareCommunication()) {
0599                     continue;
0600                 }
0601 
0602                 if (!Akonadi::CalendarUtils::thatIsMe(incidence->organizer().email())) {
0603                     const QStringList myEmails = Akonadi::CalendarUtils::allEmails();
0604                     bool notifyOrganizer = false;
0605                     const KCalendarCore::Attendee me(incidence->attendeeByMails(myEmails));
0606                     if (!me.isNull()) {
0607                         if (me.status() == KCalendarCore::Attendee::Accepted || me.status() == KCalendarCore::Attendee::Delegated) {
0608                             notifyOrganizer = true;
0609                         }
0610                         KCalendarCore::Attendee newMe(me);
0611                         newMe.setStatus(KCalendarCore::Attendee::Declined);
0612                         incidence->clearAttendees();
0613                         incidence->addAttendee(newMe);
0614                         // break;
0615                     }
0616 
0617                     if (notifyOrganizer) {
0618                         MailScheduler scheduler(mFactory, change->parentWidget); // TODO make async
0619                         scheduler.performTransaction(incidence, KCalendarCore::iTIPReply);
0620                     }
0621                 }
0622             }
0623             break;
0624         case IncidenceChanger::ChangeTypeModify: {
0625             if (change->originalItems.isEmpty()) {
0626                 break;
0627             }
0628 
0629             Q_ASSERT(change->originalItems.count() == 1);
0630             Incidence::Ptr oldIncidence = CalendarUtils::incidence(change->originalItems.first());
0631             Incidence::Ptr newIncidence = CalendarUtils::incidence(change->newItem);
0632 
0633             if (!newIncidence->supportsGroupwareCommunication() || !Akonadi::CalendarUtils::thatIsMe(newIncidence->organizer().email())) {
0634                 // If we're not the organizer, the user already saw the "Do you really want to do this, incidence will become out of sync"
0635                 break;
0636             }
0637 
0638             if (allowedModificationsWithoutRevisionUpdate(newIncidence)) {
0639                 break;
0640             }
0641 
0642             if (!neverSend && !alwaysSend && mInvitationStatusByAtomicOperation.contains(change->atomicOperationId)) {
0643                 handler->setDefaultAction(actionFromStatus(mInvitationStatusByAtomicOperation.value(change->atomicOperationId)));
0644             }
0645 
0646             const bool attendeeStatusChanged = myAttendeeStatusChanged(newIncidence, oldIncidence, Akonadi::CalendarUtils::allEmails());
0647 
0648             handler->sendIncidenceModifiedMessage(KCalendarCore::iTIPRequest, newIncidence, attendeeStatusChanged);
0649             return;
0650         }
0651         default:
0652             handler->deleteLater();
0653             handler = nullptr;
0654             Q_ASSERT(false);
0655             change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultCanceled);
0656             return;
0657         }
0658         handler->deleteLater();
0659         handler = nullptr;
0660         change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultSuccess);
0661     } else {
0662         change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultSuccess);
0663     }
0664 }
0665 
0666 /** static */
0667 bool IncidenceChangerPrivate::myAttendeeStatusChanged(const Incidence::Ptr &newInc, const Incidence::Ptr &oldInc, const QStringList &myEmails)
0668 {
0669     Q_ASSERT(newInc);
0670     Q_ASSERT(oldInc);
0671     const Attendee oldMe = oldInc->attendeeByMails(myEmails);
0672     const Attendee newMe = newInc->attendeeByMails(myEmails);
0673 
0674     return !oldMe.isNull() && !newMe.isNull() && oldMe.status() != newMe.status();
0675 }
0676 
0677 IncidenceChanger::IncidenceChanger(QObject *parent)
0678     : QObject(parent)
0679     , d(new IncidenceChangerPrivate(/**history=*/true, /*factory=*/nullptr, this))
0680 {
0681 }
0682 
0683 IncidenceChanger::IncidenceChanger(ITIPHandlerComponentFactory *factory, QObject *parent)
0684     : QObject(parent)
0685     , d(new IncidenceChangerPrivate(/**history=*/true, factory, this))
0686 {
0687 }
0688 
0689 IncidenceChanger::IncidenceChanger(bool enableHistory, QObject *parent)
0690     : QObject(parent)
0691     , d(new IncidenceChangerPrivate(enableHistory, /*factory=*/nullptr, this))
0692 {
0693 }
0694 
0695 IncidenceChanger::~IncidenceChanger() = default;
0696 
0697 int IncidenceChanger::createFromItem(const Item &item, const Collection &collection, QWidget *parent)
0698 {
0699     const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0;
0700 
0701     const Change::Ptr change(new CreationChange(this, ++d->mLatestChangeId, atomicOperationId, parent));
0702     const int changeId = change->id;
0703     Q_ASSERT(!(d->mBatchOperationInProgress && !d->mAtomicOperations.contains(atomicOperationId)));
0704     if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) {
0705         const QString errorMessage = d->showErrorDialog(ResultCodeRolledback, parent);
0706         qCWarning(AKONADICALENDAR_LOG) << errorMessage;
0707 
0708         change->resultCode = ResultCodeRolledback;
0709         change->errorString = errorMessage;
0710         d->cleanupTransaction();
0711         return changeId;
0712     }
0713 
0714     change->newItem = item;
0715 
0716     d->step1DetermineDestinationCollection(change, collection);
0717 
0718     return change->id;
0719 }
0720 
0721 int IncidenceChanger::createIncidence(const Incidence::Ptr &incidence, const Collection &collection, QWidget *parent)
0722 {
0723     if (!incidence) {
0724         qCWarning(AKONADICALENDAR_LOG) << "An invalid payload is not allowed.";
0725         d->cancelTransaction();
0726         return -1;
0727     }
0728 
0729     Item item;
0730     item.setPayload<KCalendarCore::Incidence::Ptr>(incidence);
0731     item.setMimeType(incidence->mimeType());
0732 
0733     return createFromItem(item, collection, parent);
0734 }
0735 
0736 int IncidenceChanger::deleteIncidence(const Item &item, QWidget *parent)
0737 {
0738     Item::List list;
0739     list.append(item);
0740 
0741     return deleteIncidences(list, parent);
0742 }
0743 
0744 int IncidenceChanger::deleteIncidences(const Item::List &items, QWidget *parent)
0745 {
0746     if (items.isEmpty()) {
0747         qCritical() << "Delete what?";
0748         d->cancelTransaction();
0749         return -1;
0750     }
0751 
0752     for (const Item &item : items) {
0753         if (!item.isValid()) {
0754             qCritical() << "Items must be valid!";
0755             d->cancelTransaction();
0756             return -1;
0757         }
0758     }
0759 
0760     const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0;
0761     const int changeId = ++d->mLatestChangeId;
0762     const Change::Ptr change(new DeletionChange(this, changeId, atomicOperationId, parent));
0763 
0764     for (const Item &item : items) {
0765         if (!d->hasRights(item.parentCollection(), ChangeTypeDelete)) {
0766             qCWarning(AKONADICALENDAR_LOG) << "Item " << item.id() << " can't be deleted due to ACL restrictions";
0767             const QString errorString = d->showErrorDialog(ResultCodePermissions, parent);
0768             change->resultCode = ResultCodePermissions;
0769             change->errorString = errorString;
0770             d->cancelTransaction();
0771             return changeId;
0772         }
0773     }
0774 
0775     if (!d->allowAtomicOperation(atomicOperationId, change)) {
0776         const QString errorString = d->showErrorDialog(ResultCodeDuplicateId, parent);
0777         change->resultCode = ResultCodeDuplicateId;
0778         change->errorString = errorString;
0779         qCWarning(AKONADICALENDAR_LOG) << errorString;
0780         d->cancelTransaction();
0781         return changeId;
0782     }
0783 
0784     Item::List itemsToDelete;
0785     for (const Item &item : items) {
0786         if (d->deleteAlreadyCalled(item.id())) {
0787             // IncidenceChanger::deleteIncidence() called twice, ignore this one.
0788             qCDebug(AKONADICALENDAR_LOG) << "Item " << item.id() << " already deleted or being deleted, skipping";
0789         } else {
0790             itemsToDelete.append(item);
0791         }
0792     }
0793 
0794     if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) {
0795         const QString errorMessage = d->showErrorDialog(ResultCodeRolledback, parent);
0796         change->resultCode = ResultCodeRolledback;
0797         change->errorString = errorMessage;
0798         qCritical() << errorMessage;
0799         d->cleanupTransaction();
0800         return changeId;
0801     }
0802 
0803     if (itemsToDelete.isEmpty()) {
0804         QList<Akonadi::Item::Id> itemIdList;
0805         itemIdList.append(Item().id());
0806         qCDebug(AKONADICALENDAR_LOG) << "Items already deleted or being deleted, skipping";
0807         const QString errorMessage = i18n("That calendar item was already deleted, or currently being deleted.");
0808         // Queued emit because return must be executed first, otherwise caller won't know this workId
0809         change->resultCode = ResultCodeAlreadyDeleted;
0810         change->errorString = errorMessage;
0811         d->cancelTransaction();
0812         qCWarning(AKONADICALENDAR_LOG) << errorMessage;
0813         return changeId;
0814     }
0815     change->originalItems = itemsToDelete;
0816 
0817     d->mChangeById.insert(changeId, change);
0818 
0819     if (d->mGroupwareCommunication) {
0820         connect(change.data(), &Change::dialogClosedBeforeChange, d.get(), &IncidenceChangerPrivate::deleteIncidences2);
0821         d->handleInvitationsBeforeChange(change);
0822     } else {
0823         d->deleteIncidences2(changeId, ITIPHandlerHelper::ResultSuccess);
0824     }
0825     return changeId;
0826 }
0827 
0828 void IncidenceChangerPrivate::deleteIncidences2(int changeId, ITIPHandlerHelper::SendResult status)
0829 {
0830     Q_UNUSED(status)
0831     Change::Ptr change = mChangeById[changeId];
0832     const uint atomicOperationId = change->atomicOperationId;
0833     auto deleteJob = new ItemDeleteJob(change->originalItems, parentJob(change));
0834     mChangeForJob.insert(deleteJob, change);
0835 
0836     if (mBatchOperationInProgress) {
0837         AtomicOperation *atomic = mAtomicOperations[atomicOperationId];
0838         Q_ASSERT(atomic);
0839         atomic->addChange(change);
0840     }
0841 
0842     mDeletedItemIds.reserve(mDeletedItemIds.count() + change->originalItems.count());
0843     for (const Item &item : std::as_const(change->originalItems)) {
0844         mDeletedItemIds << item.id();
0845     }
0846 
0847     // Do some cleanup
0848     if (mDeletedItemIds.count() > 100) {
0849         mDeletedItemIds.remove(0, 50);
0850     }
0851 
0852     // QueuedConnection because of possible sync exec calls.
0853     connect(deleteJob, &KJob::result, this, &IncidenceChangerPrivate::handleDeleteJobResult, Qt::QueuedConnection);
0854 }
0855 
0856 int IncidenceChanger::modifyIncidence(const Item &changedItem, const KCalendarCore::Incidence::Ptr &originalPayload, QWidget *parent)
0857 {
0858     if (!changedItem.isValid() || !changedItem.hasPayload<Incidence::Ptr>()) {
0859         qCWarning(AKONADICALENDAR_LOG) << "An invalid item or payload is not allowed.";
0860         d->cancelTransaction();
0861         return -1;
0862     }
0863 
0864     if (!d->hasRights(changedItem.parentCollection(), ChangeTypeModify)) {
0865         qCWarning(AKONADICALENDAR_LOG) << "Item " << changedItem.id() << " can't be deleted due to ACL restrictions";
0866         const int changeId = ++d->mLatestChangeId;
0867         const QString errorString = d->showErrorDialog(ResultCodePermissions, parent);
0868         emitModifyFinished(this, changeId, changedItem, ResultCodePermissions, errorString);
0869         d->cancelTransaction();
0870         return changeId;
0871     }
0872 
0873     // TODO also update revision here instead of in the editor
0874     changedItem.payload<Incidence::Ptr>()->setLastModified(QDateTime::currentDateTimeUtc());
0875 
0876     const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0;
0877     const int changeId = ++d->mLatestChangeId;
0878     auto modificationChange = new ModificationChange(this, changeId, atomicOperationId, parent);
0879     Change::Ptr change(modificationChange);
0880 
0881     if (originalPayload) {
0882         Item originalItem(changedItem);
0883         originalItem.setPayload<KCalendarCore::Incidence::Ptr>(originalPayload);
0884         modificationChange->originalItems << originalItem;
0885     }
0886 
0887     modificationChange->newItem = changedItem;
0888     d->mChangeById.insert(changeId, change);
0889 
0890     if (!d->allowAtomicOperation(atomicOperationId, change)) {
0891         const QString errorString = d->showErrorDialog(ResultCodeDuplicateId, parent);
0892 
0893         change->resultCode = ResultCodeDuplicateId;
0894         change->errorString = errorString;
0895         d->cancelTransaction();
0896         qCWarning(AKONADICALENDAR_LOG) << "Atomic operation now allowed";
0897         return changeId;
0898     }
0899 
0900     if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) {
0901         const QString errorMessage = d->showErrorDialog(ResultCodeRolledback, parent);
0902         qCritical() << errorMessage;
0903         d->cleanupTransaction();
0904         emitModifyFinished(this, changeId, changedItem, ResultCodeRolledback, errorMessage);
0905     } else {
0906         d->adjustRecurrence(originalPayload, CalendarUtils::incidence(modificationChange->newItem));
0907         d->performModification(change);
0908     }
0909 
0910     return changeId;
0911 }
0912 
0913 void IncidenceChangerPrivate::performModification(const Change::Ptr &change)
0914 {
0915     const Item::Id id = change->newItem.id();
0916     Akonadi::Item &newItem = change->newItem;
0917     Q_ASSERT(newItem.isValid());
0918     Q_ASSERT(newItem.hasPayload<Incidence::Ptr>());
0919 
0920     const int changeId = change->id;
0921 
0922     if (deleteAlreadyCalled(id)) {
0923         // IncidenceChanger::deleteIncidence() called twice, ignore this one.
0924         qCDebug(AKONADICALENDAR_LOG) << "Item " << id << " already deleted or being deleted, skipping";
0925 
0926         // Queued emit because return must be executed first, otherwise caller won't know this workId
0927         emitModifyFinished(q,
0928                            change->id,
0929                            newItem,
0930                            IncidenceChanger::ResultCodeAlreadyDeleted,
0931                            i18n("That calendar item was already deleted, or currently being deleted."));
0932         return;
0933     }
0934 
0935     const uint atomicOperationId = change->atomicOperationId;
0936     const bool hasAtomicOperationId = atomicOperationId != 0;
0937     if (hasAtomicOperationId && mAtomicOperations[atomicOperationId]->rolledback()) {
0938         const QString errorMessage = showErrorDialog(IncidenceChanger::ResultCodeRolledback, nullptr);
0939         qCritical() << errorMessage;
0940         emitModifyFinished(q, changeId, newItem, IncidenceChanger::ResultCodeRolledback, errorMessage);
0941         return;
0942     }
0943     if (mGroupwareCommunication) {
0944         connect(change.data(), &Change::dialogClosedBeforeChange, this, &IncidenceChangerPrivate::performModification2);
0945         handleInvitationsBeforeChange(change);
0946     } else {
0947         performModification2(change->id, ITIPHandlerHelper::ResultSuccess);
0948     }
0949 }
0950 
0951 void IncidenceChangerPrivate::performModification2(int changeId, ITIPHandlerHelper::SendResult status)
0952 {
0953     Change::Ptr change = mChangeById[changeId];
0954     const Item::Id id = change->newItem.id();
0955     Akonadi::Item &newItem = change->newItem;
0956     Q_ASSERT(newItem.isValid());
0957     Q_ASSERT(newItem.hasPayload<Incidence::Ptr>());
0958     if (status == ITIPHandlerHelper::ResultCanceled) { // TODO:fireout what is right here:)
0959         // User got a "You're not the organizer, do you really want to send" dialog, and said "no"
0960         qCDebug(AKONADICALENDAR_LOG) << "User cancelled, giving up";
0961         emitModifyFinished(q, change->id, newItem, IncidenceChanger::ResultCodeUserCanceled, QString());
0962         return;
0963     }
0964 
0965     const uint atomicOperationId = change->atomicOperationId;
0966     const bool hasAtomicOperationId = atomicOperationId != 0;
0967 
0968     QHash<Akonadi::Item::Id, int> &latestRevisionByItemId = *(s_latestRevisionByItemId());
0969     if (latestRevisionByItemId.contains(id) && latestRevisionByItemId[id] > newItem.revision()) {
0970         /* When a ItemModifyJob ends, the application can still modify the old items if the user
0971          * is quick because the ETM wasn't updated yet, and we'll get a STORE error, because
0972          * we are not modifying the latest revision.
0973          *
0974          * When a job ends, we keep the new revision in s_latestRevisionByItemId
0975          * so we can update the item's revision
0976          */
0977         newItem.setRevision(latestRevisionByItemId[id]);
0978     }
0979 
0980     Incidence::Ptr incidence = CalendarUtils::incidence(newItem);
0981     {
0982         if (!allowedModificationsWithoutRevisionUpdate(incidence)) { // increment revision ( KCalendarCore revision, not akonadi )
0983             const int revision = incidence->revision();
0984             incidence->setRevision(revision + 1);
0985         }
0986 
0987         // Reset attendee status, when resceduling
0988         QSet<IncidenceBase::Field> resetPartStatus;
0989         resetPartStatus << IncidenceBase::FieldDtStart << IncidenceBase::FieldDtEnd << IncidenceBase::FieldDtStart << IncidenceBase::FieldLocation
0990                         << IncidenceBase::FieldDtDue << IncidenceBase::FieldDuration << IncidenceBase::FieldRecurrence;
0991         if (!(incidence->dirtyFields() & resetPartStatus).isEmpty() && weAreOrganizer(incidence)) {
0992             auto attendees = incidence->attendees();
0993             for (auto &attendee : attendees) {
0994                 if (attendee.role() != Attendee::NonParticipant && attendee.status() != Attendee::Delegated && !Akonadi::CalendarUtils::thatIsMe(attendee)) {
0995                     attendee.setStatus(Attendee::NeedsAction);
0996                     attendee.setRSVP(true);
0997                 }
0998             }
0999             incidence->setAttendees(attendees);
1000         }
1001     }
1002 
1003     // Dav Fix
1004     // Don't write back remote revision since we can't make sure it is the current one
1005     newItem.setRemoteRevision(QString());
1006 
1007     if (mModificationsInProgress.contains(newItem.id())) {
1008         // There's already a ItemModifyJob running for this item ID
1009         // Let's wait for it to end.
1010         queueModification(change);
1011     } else {
1012         auto modifyJob = new ItemModifyJob(newItem, parentJob(change));
1013         mChangeForJob.insert(modifyJob, change);
1014         mDirtyFieldsByJob.insert(modifyJob, incidence->dirtyFields());
1015 
1016         if (hasAtomicOperationId) {
1017             AtomicOperation *atomic = mAtomicOperations[atomicOperationId];
1018             Q_ASSERT(atomic);
1019             atomic->addChange(change);
1020         }
1021 
1022         mModificationsInProgress[newItem.id()] = change;
1023         // QueuedConnection because of possible sync exec calls.
1024         connect(modifyJob, &KJob::result, this, &IncidenceChangerPrivate::handleModifyJobResult, Qt::QueuedConnection);
1025     }
1026 }
1027 
1028 void IncidenceChanger::startAtomicOperation(const QString &operationDescription)
1029 {
1030     if (d->mBatchOperationInProgress) {
1031         qCDebug(AKONADICALENDAR_LOG) << "An atomic operation is already in progress.";
1032         return;
1033     }
1034 
1035     ++d->mLatestAtomicOperationId;
1036     d->mBatchOperationInProgress = true;
1037 
1038     auto atomicOperation = new AtomicOperation(d.get(), d->mLatestAtomicOperationId);
1039     atomicOperation->m_description = operationDescription;
1040     d->mAtomicOperations.insert(d->mLatestAtomicOperationId, atomicOperation);
1041 }
1042 
1043 void IncidenceChanger::endAtomicOperation()
1044 {
1045     if (!d->mBatchOperationInProgress) {
1046         qCDebug(AKONADICALENDAR_LOG) << "No atomic operation is in progress.";
1047         return;
1048     }
1049 
1050     Q_ASSERT_X(d->mLatestAtomicOperationId != 0, "IncidenceChanger::endAtomicOperation()", "Call startAtomicOperation() first.");
1051 
1052     Q_ASSERT(d->mAtomicOperations.contains(d->mLatestAtomicOperationId));
1053     AtomicOperation *atomicOperation = d->mAtomicOperations[d->mLatestAtomicOperationId];
1054     Q_ASSERT(atomicOperation);
1055     atomicOperation->m_endCalled = true;
1056 
1057     const bool allJobsCompleted = !atomicOperation->pendingJobs();
1058 
1059     if (allJobsCompleted && atomicOperation->rolledback() && atomicOperation->m_transactionCompleted) {
1060         // The transaction job already completed, we can cleanup:
1061         delete d->mAtomicOperations.take(d->mLatestAtomicOperationId);
1062         d->mBatchOperationInProgress = false;
1063     } /* else if ( allJobsCompleted ) {
1064      Q_ASSERT( atomicOperation->transaction );
1065      atomicOperation->transaction->commit(); we using autocommit now
1066    }*/
1067 }
1068 
1069 void IncidenceChanger::setShowDialogsOnError(bool enable)
1070 {
1071     d->mShowDialogsOnError = enable;
1072     if (d->mHistory) {
1073         d->mHistory->incidenceChanger()->setShowDialogsOnError(enable);
1074     }
1075 }
1076 
1077 bool IncidenceChanger::showDialogsOnError() const
1078 {
1079     return d->mShowDialogsOnError;
1080 }
1081 
1082 void IncidenceChanger::setRespectsCollectionRights(bool respects)
1083 {
1084     d->mRespectsCollectionRights = respects;
1085 }
1086 
1087 bool IncidenceChanger::respectsCollectionRights() const
1088 {
1089     return d->mRespectsCollectionRights;
1090 }
1091 
1092 void IncidenceChanger::setDestinationPolicy(IncidenceChanger::DestinationPolicy destinationPolicy)
1093 {
1094     d->mDestinationPolicy = destinationPolicy;
1095 }
1096 
1097 IncidenceChanger::DestinationPolicy IncidenceChanger::destinationPolicy() const
1098 {
1099     return d->mDestinationPolicy;
1100 }
1101 
1102 void IncidenceChanger::setEntityTreeModel(Akonadi::EntityTreeModel *entityTreeModel)
1103 {
1104     d->mEntityTreeModel = entityTreeModel;
1105 }
1106 
1107 Akonadi::EntityTreeModel *IncidenceChanger::entityTreeModel() const
1108 {
1109     return d->mEntityTreeModel;
1110 }
1111 
1112 void IncidenceChanger::setDefaultCollection(const Akonadi::Collection &collection)
1113 {
1114     d->mDefaultCollection = collection;
1115 }
1116 
1117 Collection IncidenceChanger::defaultCollection() const
1118 {
1119     return d->mDefaultCollection;
1120 }
1121 
1122 bool IncidenceChanger::historyEnabled() const
1123 {
1124     return d->mUseHistory;
1125 }
1126 
1127 void IncidenceChanger::setHistoryEnabled(bool enable)
1128 {
1129     if (d->mUseHistory != enable) {
1130         d->mUseHistory = enable;
1131         if (enable && !d->mHistory) {
1132             d->mHistory = new History(d.get());
1133         }
1134     }
1135 }
1136 
1137 History *IncidenceChanger::history() const
1138 {
1139     return d->mHistory;
1140 }
1141 
1142 bool IncidenceChanger::deletedRecently(Akonadi::Item::Id id) const
1143 {
1144     return d->deleteAlreadyCalled(id);
1145 }
1146 
1147 void IncidenceChanger::setGroupwareCommunication(bool enabled)
1148 {
1149     d->mGroupwareCommunication = enabled;
1150 }
1151 
1152 bool IncidenceChanger::groupwareCommunication() const
1153 {
1154     return d->mGroupwareCommunication;
1155 }
1156 
1157 void IncidenceChanger::setAutoAdjustRecurrence(bool enable)
1158 {
1159     d->mAutoAdjustRecurrence = enable;
1160 }
1161 
1162 bool IncidenceChanger::autoAdjustRecurrence() const
1163 {
1164     return d->mAutoAdjustRecurrence;
1165 }
1166 
1167 void IncidenceChanger::setInvitationPolicy(IncidenceChanger::InvitationPolicy policy)
1168 {
1169     d->m_invitationPolicy = policy;
1170 }
1171 
1172 IncidenceChanger::InvitationPolicy IncidenceChanger::invitationPolicy() const
1173 {
1174     return d->m_invitationPolicy;
1175 }
1176 
1177 Akonadi::Collection IncidenceChanger::lastCollectionUsed() const
1178 {
1179     return d->mLastCollectionUsed;
1180 }
1181 
1182 void IncidenceChanger::setInvitationPrivacy(IncidenceChanger::InvitationPrivacyFlags invitationPrivacy)
1183 {
1184     d->m_invitationPrivacy = invitationPrivacy;
1185 }
1186 
1187 IncidenceChanger::InvitationPrivacyFlags IncidenceChanger::invitationPrivacy() const
1188 {
1189     return d->m_invitationPrivacy;
1190 }
1191 
1192 QString IncidenceChangerPrivate::showErrorDialog(IncidenceChanger::ResultCode resultCode, QWidget *parent)
1193 {
1194     QString errorString;
1195     switch (resultCode) {
1196     case IncidenceChanger::ResultCodePermissions:
1197         errorString = i18n("Operation can not be performed due to ACL restrictions");
1198         break;
1199     case IncidenceChanger::ResultCodeInvalidUserCollection:
1200         errorString = i18n("The chosen collection is invalid");
1201         break;
1202     case IncidenceChanger::ResultCodeInvalidDefaultCollection:
1203         errorString = i18n(
1204             "Default collection is invalid or doesn't have proper ACLs"
1205             " and DestinationPolicyNeverAsk was used");
1206         break;
1207     case IncidenceChanger::ResultCodeDuplicateId:
1208         errorString = i18n("Duplicate item id in a group operation");
1209         break;
1210     case IncidenceChanger::ResultCodeRolledback:
1211         errorString = i18n(
1212             "One change belonging to a group of changes failed. "
1213             "All changes are being rolled back.");
1214         break;
1215     default:
1216         Q_ASSERT(false);
1217         return QString(i18n("Unknown error"));
1218     }
1219 
1220     if (mShowDialogsOnError) {
1221         KMessageBox::error(parent, errorString);
1222     }
1223 
1224     return errorString;
1225 }
1226 
1227 void IncidenceChangerPrivate::adjustRecurrence(const KCalendarCore::Incidence::Ptr &originalIncidence, const KCalendarCore::Incidence::Ptr &incidence)
1228 {
1229     if (!originalIncidence || !incidence->recurs() || incidence->hasRecurrenceId() || !mAutoAdjustRecurrence
1230         || !incidence->dirtyFields().contains(KCalendarCore::Incidence::FieldDtStart)) {
1231         return;
1232     }
1233 
1234     const QDate originalDate = originalIncidence->dtStart().date();
1235     const QDate newStartDate = incidence->dtStart().date();
1236 
1237     if (!originalDate.isValid() || !newStartDate.isValid() || originalDate == newStartDate) {
1238         return;
1239     }
1240 
1241     KCalendarCore::Recurrence *recurrence = incidence->recurrence();
1242     switch (recurrence->recurrenceType()) {
1243     case KCalendarCore::Recurrence::rWeekly: {
1244         QBitArray days = recurrence->days();
1245         const int oldIndex = originalDate.dayOfWeek() - 1; // QDate returns [1-7];
1246         const int newIndex = newStartDate.dayOfWeek() - 1;
1247         if (oldIndex != newIndex) {
1248             days.clearBit(oldIndex);
1249             days.setBit(newIndex);
1250             recurrence->setWeekly(recurrence->frequency(), days);
1251         }
1252     }
1253     default:
1254         break; // Other types not implemented
1255     }
1256 
1257     // Now fix cases where dtstart would be bigger than the recurrence end rendering it impossible for a view to show it:
1258     // To retrieve the recurrence end don't trust Recurrence::endDt() since it returns dtStart if the rrule's end is < than dtstart,
1259     // it seems someone made Recurrence::endDt() more robust, but getNextOccurrences() still craps out. So lets fix it here
1260     // there's no reason to write bogus ical to disk.
1261     const QDate recurrenceEndDate = recurrence->defaultRRule() ? recurrence->defaultRRule()->endDt().date() : QDate();
1262     if (recurrenceEndDate.isValid() && recurrenceEndDate < newStartDate) {
1263         recurrence->setEndDate(newStartDate);
1264     }
1265 }
1266 
1267 void IncidenceChangerPrivate::cancelTransaction()
1268 {
1269     if (mBatchOperationInProgress) {
1270         mAtomicOperations[mLatestAtomicOperationId]->setRolledback();
1271     }
1272 }
1273 
1274 void IncidenceChangerPrivate::cleanupTransaction()
1275 {
1276     Q_ASSERT(mAtomicOperations.contains(mLatestAtomicOperationId));
1277     AtomicOperation *operation = mAtomicOperations[mLatestAtomicOperationId];
1278     Q_ASSERT(operation);
1279     Q_ASSERT(operation->rolledback());
1280     if (!operation->pendingJobs() && operation->m_endCalled && operation->m_transactionCompleted) {
1281         delete mAtomicOperations.take(mLatestAtomicOperationId);
1282         mBatchOperationInProgress = false;
1283     }
1284 }
1285 
1286 bool IncidenceChangerPrivate::allowAtomicOperation(int atomicOperationId, const Change::Ptr &change) const
1287 {
1288     bool allow = true;
1289     if (atomicOperationId > 0) {
1290         Q_ASSERT(mAtomicOperations.contains(atomicOperationId));
1291         AtomicOperation *operation = mAtomicOperations.value(atomicOperationId);
1292 
1293         if (change->type == IncidenceChanger::ChangeTypeCreate) {
1294             allow = true;
1295         } else if (change->type == IncidenceChanger::ChangeTypeModify) {
1296             allow = !operation->m_itemIdsInOperation.contains(change->newItem.id());
1297         } else if (change->type == IncidenceChanger::ChangeTypeDelete) {
1298             DeletionChange::Ptr deletion = change.staticCast<DeletionChange>();
1299             for (Akonadi::Item::Id id : std::as_const(deletion->mItemIds)) {
1300                 if (operation->m_itemIdsInOperation.contains(id)) {
1301                     allow = false;
1302                     break;
1303                 }
1304             }
1305         }
1306     }
1307 
1308     if (!allow) {
1309         qCWarning(AKONADICALENDAR_LOG) << "Each change belonging to a group operation"
1310                                        << "must have a different Akonadi::Item::Id";
1311     }
1312 
1313     return allow;
1314 }
1315 
1316 /**reimp*/
1317 void ModificationChange::emitCompletionSignal()
1318 {
1319     emitModifyFinished(changer, id, newItem, resultCode, errorString);
1320 }
1321 
1322 /**reimp*/
1323 void CreationChange::emitCompletionSignal()
1324 {
1325     // Does a queued emit, with QMetaObject::invokeMethod
1326     emitCreateFinished(changer, id, newItem, resultCode, errorString);
1327 }
1328 
1329 /**reimp*/
1330 void DeletionChange::emitCompletionSignal()
1331 {
1332     emitDeleteFinished(changer, id, mItemIds, resultCode, errorString);
1333 }
1334 
1335 /**
1336 Lost code from KDE 4.4 that was never called/used with incidenceeditors-ng.
1337 
1338       Attendees were removed from this incidence. Only the removed attendees
1339       are present in the incidence, so we just need to send a cancel messages
1340       to all attendees groupware messages are enabled at all.
1341 
1342 void IncidenceChanger::cancelAttendees( const Akonadi::Item &aitem )
1343 {
1344   const KCalendarCore::Incidence::Ptr incidence = CalendarSupport::incidence( aitem );
1345   Q_ASSERT( incidence );
1346   if ( KCalPrefs::instance()->mUseGroupwareCommunication ) {
1347     if ( KMessageBox::questionYesNo(
1348            0,
1349            i18n( "Some attendees were removed from the incidence. "
1350                  "Shall cancel messages be sent to these attendees?" ),
1351            i18nc("@title:window", "Attendees Removed" ), KGuiItem( i18n( "Send Messages" ) ),
1352            KGuiItem( i18n( "Do Not Send" ) ) ) == KMessageBox::ButtonCode::PrimaryAction) {
1353       // don't use Akonadi::Groupware::sendICalMessage here, because that asks just
1354       // a very general question "Other people are involved, send message to
1355       // them?", which isn't helpful at all in this situation. Afterwards, it
1356       // would only call the Akonadi::MailScheduler::performTransaction, so do this
1357       // manually.
1358       CalendarSupport::MailScheduler scheduler(
1359         static_cast<CalendarSupport::Calendar*>(d->mCalendar) );
1360       scheduler.performTransaction( incidence, KCalendarCore::iTIPCancel );
1361     }
1362   }
1363 }
1364 
1365 */
1366 
1367 AtomicOperation::AtomicOperation(IncidenceChangerPrivate *icp, uint ident)
1368     : m_id(ident)
1369     , m_endCalled(false)
1370     , m_numCompletedChanges(0)
1371     , m_transactionCompleted(false)
1372     , m_wasRolledback(false)
1373     , m_transaction(nullptr)
1374     , m_incidenceChangerPrivate(icp)
1375 {
1376     Q_ASSERT(m_id != 0);
1377 }
1378 
1379 Akonadi::TransactionSequence *AtomicOperation::transaction()
1380 {
1381     if (!m_transaction) {
1382         m_transaction = new Akonadi::TransactionSequence;
1383         m_transaction->setAutomaticCommittingEnabled(true);
1384 
1385         m_incidenceChangerPrivate->mAtomicOperationByTransaction.insert(m_transaction, m_id);
1386 
1387         QObject::connect(m_transaction, &KJob::result, m_incidenceChangerPrivate, &IncidenceChangerPrivate::handleTransactionJobResult);
1388     }
1389 
1390     return m_transaction;
1391 }
1392 
1393 #include "moc_incidencechanger.cpp"