1 /*
2   SPDX-FileCopyrightText: 2004 Reinhold Kainhofer <reinhold@kainhofer.com>
3   SPDX-FileCopyrightText: 2010-2012 Sérgio Martins <iamsergio@gmail.com>
4 
5   SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7 #include "incidencechanger.h"
8 #include "akonadicalendar_debug.h"
9 #include "incidencechanger_p.h"
10 #include "mailscheduler_p.h"
11 #include "utils_p.h"
12 #include <Akonadi/ItemCreateJob>
13 #include <Akonadi/ItemDeleteJob>
14 #include <Akonadi/ItemModifyJob>
15 #include <Akonadi/TransactionSequence>
16 
17 #include <KGuiItem>
18 #include <KJob>
19 #include <KLocalizedString>
20 #include <KMessageBox>
21 
22 #include <QBitArray>
23 
24 using namespace Akonadi;
25 using namespace KCalendarCore;
26 
27 AKONADI_CALENDAR_TESTS_EXPORT bool akonadi_calendar_running_unittests = false;
28 
actionFromStatus(ITIPHandlerHelper::SendResult result)29 static ITIPHandlerDialogDelegate::Action actionFromStatus(ITIPHandlerHelper::SendResult result)
30 {
31     // enum SendResult {
32     //      Canceled,        /**< Sending was canceled by the user, meaning there are
33     //                          local changes of which other attendees are not aware. */
34     //      FailKeepUpdate,  /**< Sending failed, the changes to the incidence must be kept. */
35     //      FailAbortUpdate, /**< Sending failed, the changes to the incidence must be undone. */
36     //      NoSendingNeeded, /**< In some cases it is not needed to send an invitation
37     //                          (e.g. when we are the only attendee) */
38     //      Success
39     switch (result) {
40     case ITIPHandlerHelper::ResultCanceled:
41         return ITIPHandlerDialogDelegate::ActionDontSendMessage;
42     case ITIPHandlerHelper::ResultSuccess:
43         return ITIPHandlerDialogDelegate::ActionSendMessage;
44     default:
45         return ITIPHandlerDialogDelegate::ActionAsk;
46     }
47 }
48 
weAreOrganizer(const Incidence::Ptr & incidence)49 static bool weAreOrganizer(const Incidence::Ptr &incidence)
50 {
51     const QString email = incidence->organizer().email();
52     return Akonadi::CalendarUtils::thatIsMe(email);
53 }
54 
allowedModificationsWithoutRevisionUpdate(const Incidence::Ptr & incidence)55 static bool allowedModificationsWithoutRevisionUpdate(const Incidence::Ptr &incidence)
56 {
57     // Modifications that are per user allowed without getting outofsync with organisator
58     // * if only alarm settings are modified.
59     const QSet<KCalendarCore::IncidenceBase::Field> dirtyFields = incidence->dirtyFields();
60     QSet<KCalendarCore::IncidenceBase::Field> alarmOnlyModify;
61     alarmOnlyModify << IncidenceBase::FieldAlarms << IncidenceBase::FieldLastModified;
62     return dirtyFields == alarmOnlyModify;
63 }
64 
65 namespace Akonadi
66 {
67 // Does a queued emit, with QMetaObject::invokeMethod
emitCreateFinished(IncidenceChanger * changer,int changeId,const Akonadi::Item & item,Akonadi::IncidenceChanger::ResultCode resultCode,const QString & errorString)68 static void emitCreateFinished(IncidenceChanger *changer,
69                                int changeId,
70                                const Akonadi::Item &item,
71                                Akonadi::IncidenceChanger::ResultCode resultCode,
72                                const QString &errorString)
73 {
74     QMetaObject::invokeMethod(changer,
75                               "createFinished",
76                               Qt::QueuedConnection,
77                               Q_ARG(int, changeId),
78                               Q_ARG(Akonadi::Item, item),
79                               Q_ARG(Akonadi::IncidenceChanger::ResultCode, resultCode),
80                               Q_ARG(QString, errorString));
81 }
82 
83 // Does a queued emit, with QMetaObject::invokeMethod
84 static void
emitModifyFinished(IncidenceChanger * changer,int changeId,const Akonadi::Item & item,IncidenceChanger::ResultCode resultCode,const QString & errorString)85 emitModifyFinished(IncidenceChanger *changer, int changeId, const Akonadi::Item &item, IncidenceChanger::ResultCode resultCode, const QString &errorString)
86 {
87     QMetaObject::invokeMethod(changer,
88                               "modifyFinished",
89                               Qt::QueuedConnection,
90                               Q_ARG(int, changeId),
91                               Q_ARG(Akonadi::Item, item),
92                               Q_ARG(Akonadi::IncidenceChanger::ResultCode, resultCode),
93                               Q_ARG(QString, errorString));
94 }
95 
96 // Does a queued emit, with QMetaObject::invokeMethod
emitDeleteFinished(IncidenceChanger * changer,int changeId,const QVector<Akonadi::Item::Id> & itemIdList,IncidenceChanger::ResultCode resultCode,const QString & errorString)97 static void emitDeleteFinished(IncidenceChanger *changer,
98                                int changeId,
99                                const QVector<Akonadi::Item::Id> &itemIdList,
100                                IncidenceChanger::ResultCode resultCode,
101                                const QString &errorString)
102 {
103     QMetaObject::invokeMethod(changer,
104                               "deleteFinished",
105                               Qt::QueuedConnection,
106                               Q_ARG(int, changeId),
107                               Q_ARG(QVector<Akonadi::Item::Id>, itemIdList),
108                               Q_ARG(Akonadi::IncidenceChanger::ResultCode, resultCode),
109                               Q_ARG(QString, errorString));
110 }
111 }
112 
113 using IdToRevisionHash = QHash<Akonadi::Item::Id, int>;
Q_GLOBAL_STATIC(IdToRevisionHash,s_latestRevisionByItemId)114 Q_GLOBAL_STATIC(IdToRevisionHash, s_latestRevisionByItemId)
115 
116 IncidenceChangerPrivate::IncidenceChangerPrivate(bool enableHistory, ITIPHandlerComponentFactory *factory, IncidenceChanger *qq)
117     : q(qq)
118 {
119     mLatestChangeId = 0;
120     mShowDialogsOnError = true;
121     mFactory = factory ? factory : new ITIPHandlerComponentFactory(this);
122     mHistory = enableHistory ? new History(this) : nullptr;
123     mUseHistory = enableHistory;
124     mDestinationPolicy = IncidenceChanger::DestinationPolicyDefault;
125     mRespectsCollectionRights = false;
126     mGroupwareCommunication = false;
127     mLatestAtomicOperationId = 0;
128     mBatchOperationInProgress = false;
129     mAutoAdjustRecurrence = true;
130     m_collectionFetchJob = nullptr;
131     m_invitationPolicy = IncidenceChanger::InvitationPolicyAsk;
132 
133     qRegisterMetaType<QVector<Akonadi::Item::Id>>("QVector<Akonadi::Item::Id>");
134     qRegisterMetaType<Akonadi::Item::Id>("Akonadi::Item::Id");
135     qRegisterMetaType<Akonadi::Item>("Akonadi::Item");
136     qRegisterMetaType<Akonadi::IncidenceChanger::ResultCode>("Akonadi::IncidenceChanger::ResultCode");
137     qRegisterMetaType<ITIPHandlerHelper::SendResult>("ITIPHandlerHelper::SendResult");
138 }
139 
~IncidenceChangerPrivate()140 IncidenceChangerPrivate::~IncidenceChangerPrivate()
141 {
142     if (!mAtomicOperations.isEmpty() || !mQueuedModifications.isEmpty() || !mModificationsInProgress.isEmpty()) {
143         qCDebug(AKONADICALENDAR_LOG) << "Normal if the application was being used. "
144                                         "But might indicate a memory leak if it wasn't";
145     }
146 }
147 
atomicOperationIsValid(uint atomicOperationId) const148 bool IncidenceChangerPrivate::atomicOperationIsValid(uint atomicOperationId) const
149 {
150     // Changes must be done between startAtomicOperation() and endAtomicOperation()
151     return mAtomicOperations.contains(atomicOperationId) && !mAtomicOperations[atomicOperationId]->m_endCalled;
152 }
153 
hasRights(const Collection & collection,IncidenceChanger::ChangeType changeType) const154 bool IncidenceChangerPrivate::hasRights(const Collection &collection, IncidenceChanger::ChangeType changeType) const
155 {
156     bool result = false;
157     switch (changeType) {
158     case IncidenceChanger::ChangeTypeCreate:
159         result = collection.rights() & Akonadi::Collection::CanCreateItem;
160         break;
161     case IncidenceChanger::ChangeTypeModify:
162         result = collection.rights() & Akonadi::Collection::CanChangeItem;
163         break;
164     case IncidenceChanger::ChangeTypeDelete:
165         result = collection.rights() & Akonadi::Collection::CanDeleteItem;
166         break;
167     default:
168         Q_ASSERT_X(false, "hasRights", "invalid type");
169     }
170 
171     return !collection.isValid() || !mRespectsCollectionRights || result;
172 }
173 
parentJob(const Change::Ptr & change) const174 Akonadi::Job *IncidenceChangerPrivate::parentJob(const Change::Ptr &change) const
175 {
176     return (mBatchOperationInProgress && !change->queuedModification) ? mAtomicOperations[mLatestAtomicOperationId]->transaction() : nullptr;
177 }
178 
queueModification(const Change::Ptr & change)179 void IncidenceChangerPrivate::queueModification(const Change::Ptr &change)
180 {
181     // If there's already a change queued we just discard it
182     // and send the newer change, which already includes
183     // previous modifications
184     const Akonadi::Item::Id id = change->newItem.id();
185     if (mQueuedModifications.contains(id)) {
186         Change::Ptr toBeDiscarded = mQueuedModifications.take(id);
187         toBeDiscarded->resultCode = IncidenceChanger::ResultCodeModificationDiscarded;
188         toBeDiscarded->completed = true;
189         mChangeById.remove(toBeDiscarded->id);
190     }
191 
192     change->queuedModification = true;
193     mQueuedModifications[id] = change;
194 }
195 
performNextModification(Akonadi::Item::Id id)196 void IncidenceChangerPrivate::performNextModification(Akonadi::Item::Id id)
197 {
198     mModificationsInProgress.remove(id);
199 
200     if (mQueuedModifications.contains(id)) {
201         const Change::Ptr change = mQueuedModifications.take(id);
202         performModification(change);
203     }
204 }
205 
handleTransactionJobResult(KJob * job)206 void IncidenceChangerPrivate::handleTransactionJobResult(KJob *job)
207 {
208     auto transaction = qobject_cast<TransactionSequence *>(job);
209     Q_ASSERT(transaction);
210     Q_ASSERT(mAtomicOperationByTransaction.contains(transaction));
211 
212     const uint atomicOperationId = mAtomicOperationByTransaction.take(transaction);
213 
214     Q_ASSERT(mAtomicOperations.contains(atomicOperationId));
215     AtomicOperation *operation = mAtomicOperations[atomicOperationId];
216     Q_ASSERT(operation);
217     Q_ASSERT(operation->m_id == atomicOperationId);
218     if (job->error()) {
219         if (!operation->rolledback()) {
220             operation->setRolledback();
221         }
222         qCritical() << "Transaction failed, everything was rolledback. " << job->errorString();
223     } else {
224         Q_ASSERT(operation->m_endCalled);
225         Q_ASSERT(!operation->pendingJobs());
226     }
227 
228     if (!operation->pendingJobs() && operation->m_endCalled) {
229         delete mAtomicOperations.take(atomicOperationId);
230         mBatchOperationInProgress = false;
231     } else {
232         operation->m_transactionCompleted = true;
233     }
234 }
235 
handleCreateJobResult(KJob * job)236 void IncidenceChangerPrivate::handleCreateJobResult(KJob *job)
237 {
238     Change::Ptr change = mChangeForJob.take(job);
239 
240     const auto j = qobject_cast<const ItemCreateJob *>(job);
241     Q_ASSERT(j);
242     Akonadi::Item item = j->item();
243 
244     if (j->error()) {
245         const QString errorString = j->errorString();
246         IncidenceChanger::ResultCode resultCode = IncidenceChanger::ResultCodeJobError;
247         item = change->newItem;
248         qCritical() << errorString;
249         if (mShowDialogsOnError) {
250             KMessageBox::sorry(change->parentWidget, i18n("Error while trying to create calendar item. Error was: %1", errorString));
251         }
252         mChangeById.remove(change->id);
253         change->errorString = errorString;
254         change->resultCode = resultCode;
255         // puff, change finally goes out of scope, and emits the incidenceCreated signal.
256     } else {
257         Q_ASSERT(item.isValid());
258         Q_ASSERT(item.hasPayload<KCalendarCore::Incidence::Ptr>());
259         change->newItem = item;
260 
261         if (change->useGroupwareCommunication) {
262             connect(change.data(), &Change::dialogClosedAfterChange, this, &IncidenceChangerPrivate::handleCreateJobResult2);
263             handleInvitationsAfterChange(change);
264         } else {
265             handleCreateJobResult2(change->id, ITIPHandlerHelper::ResultSuccess);
266         }
267     }
268 }
269 
handleCreateJobResult2(int changeId,ITIPHandlerHelper::SendResult status)270 void IncidenceChangerPrivate::handleCreateJobResult2(int changeId, ITIPHandlerHelper::SendResult status)
271 {
272     Change::Ptr change = mChangeById[changeId];
273     Akonadi::Item item = change->newItem;
274 
275     mChangeById.remove(changeId);
276 
277     if (status == ITIPHandlerHelper::ResultFailAbortUpdate) {
278         qCritical() << "Sending invitations failed, but did not delete the incidence";
279     }
280 
281     const uint atomicOperationId = change->atomicOperationId;
282     if (atomicOperationId != 0) {
283         mInvitationStatusByAtomicOperation.insert(atomicOperationId, status);
284     }
285 
286     QString description;
287     if (change->atomicOperationId != 0) {
288         AtomicOperation *a = mAtomicOperations[change->atomicOperationId];
289         ++a->m_numCompletedChanges;
290         change->completed = true;
291         description = a->m_description;
292     }
293 
294     // for user undo/redo
295     if (change->recordToHistory) {
296         mHistory->recordCreation(item, description, change->atomicOperationId);
297     }
298 
299     change->errorString = QString();
300     change->resultCode = IncidenceChanger::ResultCodeSuccess;
301     // puff, change finally goes out of scope, and emits the incidenceCreated signal.
302 }
303 
handleDeleteJobResult(KJob * job)304 void IncidenceChangerPrivate::handleDeleteJobResult(KJob *job)
305 {
306     Change::Ptr change = mChangeForJob.take(job);
307 
308     const auto j = qobject_cast<const ItemDeleteJob *>(job);
309     const Item::List items = j->deletedItems();
310 
311     QSharedPointer<DeletionChange> deletionChange = change.staticCast<DeletionChange>();
312 
313     deletionChange->mItemIds.reserve(deletionChange->mItemIds.count() + items.count());
314     for (const Akonadi::Item &item : items) {
315         deletionChange->mItemIds.append(item.id());
316     }
317     QString description;
318     if (change->atomicOperationId != 0) {
319         AtomicOperation *a = mAtomicOperations[change->atomicOperationId];
320         a->m_numCompletedChanges++;
321         change->completed = true;
322         description = a->m_description;
323     }
324     if (j->error()) {
325         const QString errorString = j->errorString();
326         qCritical() << errorString;
327 
328         if (mShowDialogsOnError) {
329             KMessageBox::sorry(change->parentWidget, i18n("Error while trying to delete calendar item. Error was: %1", errorString));
330         }
331 
332         for (const Item &item : items) {
333             // Weren't deleted due to error
334             mDeletedItemIds.remove(mDeletedItemIds.indexOf(item.id()));
335         }
336         mChangeById.remove(change->id);
337         change->resultCode = IncidenceChanger::ResultCodeJobError;
338         change->errorString = errorString;
339         change->emitCompletionSignal();
340     } else { // success
341         if (change->recordToHistory) {
342             Q_ASSERT(mHistory);
343             mHistory->recordDeletions(items, description, change->atomicOperationId);
344         }
345 
346         if (change->useGroupwareCommunication) {
347             connect(change.data(), &Change::dialogClosedAfterChange, this, &IncidenceChangerPrivate::handleDeleteJobResult2);
348             handleInvitationsAfterChange(change);
349         } else {
350             handleDeleteJobResult2(change->id, ITIPHandlerHelper::ResultSuccess);
351         }
352     }
353 }
354 
handleDeleteJobResult2(int changeId,ITIPHandlerHelper::SendResult status)355 void IncidenceChangerPrivate::handleDeleteJobResult2(int changeId, ITIPHandlerHelper::SendResult status)
356 {
357     Change::Ptr change = mChangeById[changeId];
358     mChangeById.remove(change->id);
359 
360     if (status == ITIPHandlerHelper::ResultSuccess) {
361         change->errorString = QString();
362         change->resultCode = IncidenceChanger::ResultCodeSuccess;
363     } else {
364         change->errorString = i18nc("errormessage for a job ended with an unexpected result", "An unknown error occurred");
365         change->resultCode = IncidenceChanger::ResultCodeJobError;
366     }
367 
368     // puff, change finally goes out of scope, and emits the incidenceDeleted signal.
369 }
370 
handleModifyJobResult(KJob * job)371 void IncidenceChangerPrivate::handleModifyJobResult(KJob *job)
372 {
373     Change::Ptr change = mChangeForJob.take(job);
374 
375     const auto j = qobject_cast<const ItemModifyJob *>(job);
376     const Item item = j->item();
377     Q_ASSERT(mDirtyFieldsByJob.contains(job));
378     Q_ASSERT(item.hasPayload<KCalendarCore::Incidence::Ptr>());
379     const QSet<KCalendarCore::IncidenceBase::Field> dirtyFields = mDirtyFieldsByJob.value(job);
380     item.payload<KCalendarCore::Incidence::Ptr>()->setDirtyFields(dirtyFields);
381     QString description;
382     if (change->atomicOperationId != 0) {
383         AtomicOperation *a = mAtomicOperations[change->atomicOperationId];
384         a->m_numCompletedChanges++;
385         change->completed = true;
386         description = a->m_description;
387     }
388     if (j->error()) {
389         const QString errorString = j->errorString();
390         IncidenceChanger::ResultCode resultCode = IncidenceChanger::ResultCodeJobError;
391         if (deleteAlreadyCalled(item.id())) {
392             // User deleted the item almost at the same time he changed it. We could just return success
393             // but the delete is probably already recorded to History, and that would make undo not work
394             // in the proper order.
395             resultCode = IncidenceChanger::ResultCodeAlreadyDeleted;
396             qCWarning(AKONADICALENDAR_LOG) << "Trying to change item " << item.id() << " while deletion is in progress.";
397         } else {
398             qCritical() << errorString;
399         }
400         if (mShowDialogsOnError) {
401             KMessageBox::sorry(change->parentWidget, i18n("Error while trying to modify calendar item. Error was: %1", errorString));
402         }
403         mChangeById.remove(change->id);
404         change->errorString = errorString;
405         change->resultCode = resultCode;
406         // puff, change finally goes out of scope, and emits the incidenceModified signal.
407 
408         QMetaObject::invokeMethod(this, "performNextModification", Qt::QueuedConnection, Q_ARG(Akonadi::Item::Id, item.id()));
409     } else { // success
410         (*(s_latestRevisionByItemId()))[item.id()] = item.revision();
411         change->newItem = item;
412         if (change->recordToHistory && !change->originalItems.isEmpty()) {
413             Q_ASSERT(change->originalItems.count() == 1);
414             mHistory->recordModification(change->originalItems.constFirst(), item, description, change->atomicOperationId);
415         }
416 
417         if (change->useGroupwareCommunication) {
418             connect(change.data(), &Change::dialogClosedAfterChange, this, &IncidenceChangerPrivate::handleModifyJobResult2);
419             handleInvitationsAfterChange(change);
420         } else {
421             handleModifyJobResult2(change->id, ITIPHandlerHelper::ResultSuccess);
422         }
423     }
424 }
425 
handleModifyJobResult2(int changeId,ITIPHandlerHelper::SendResult status)426 void IncidenceChangerPrivate::handleModifyJobResult2(int changeId, ITIPHandlerHelper::SendResult status)
427 {
428     Change::Ptr change = mChangeById[changeId];
429 
430     mChangeById.remove(changeId);
431     if (change->atomicOperationId != 0) {
432         mInvitationStatusByAtomicOperation.insert(change->atomicOperationId, status);
433     }
434     change->errorString = QString();
435     change->resultCode = IncidenceChanger::ResultCodeSuccess;
436     // puff, change finally goes out of scope, and emits the incidenceModified signal.
437 
438     QMetaObject::invokeMethod(this, "performNextModification", Qt::QueuedConnection, Q_ARG(Akonadi::Item::Id, change->newItem.id()));
439 }
440 
deleteAlreadyCalled(Akonadi::Item::Id id) const441 bool IncidenceChangerPrivate::deleteAlreadyCalled(Akonadi::Item::Id id) const
442 {
443     return mDeletedItemIds.contains(id);
444 }
445 
handleInvitationsBeforeChange(const Change::Ptr & change)446 void IncidenceChangerPrivate::handleInvitationsBeforeChange(const Change::Ptr &change)
447 {
448     if (mGroupwareCommunication) {
449         ITIPHandlerHelper::SendResult result = ITIPHandlerHelper::ResultSuccess;
450         switch (change->type) {
451         case IncidenceChanger::ChangeTypeCreate:
452             // nothing needs to be done
453             break;
454         case IncidenceChanger::ChangeTypeDelete: {
455             ITIPHandlerHelper::SendResult status;
456             bool sendOk = true;
457             Q_ASSERT(!change->originalItems.isEmpty());
458 
459             auto handler = new ITIPHandlerHelper(mFactory, change->parentWidget);
460             handler->setParent(this);
461 
462             if (m_invitationPolicy == IncidenceChanger::InvitationPolicySend) {
463                 handler->setDefaultAction(ITIPHandlerDialogDelegate::ActionSendMessage);
464             } else if (m_invitationPolicy == IncidenceChanger::InvitationPolicyDontSend) {
465                 handler->setDefaultAction(ITIPHandlerDialogDelegate::ActionDontSendMessage);
466             } else if (mInvitationStatusByAtomicOperation.contains(change->atomicOperationId)) {
467                 handler->setDefaultAction(actionFromStatus(mInvitationStatusByAtomicOperation.value(change->atomicOperationId)));
468             }
469 
470             connect(handler, &ITIPHandlerHelper::finished, change.data(), &Change::emitUserDialogClosedBeforeChange);
471 
472             for (const Akonadi::Item &item : std::as_const(change->originalItems)) {
473                 Q_ASSERT(item.hasPayload<KCalendarCore::Incidence::Ptr>());
474                 Incidence::Ptr incidence = CalendarUtils::incidence(item);
475                 if (!incidence->supportsGroupwareCommunication()) {
476                     continue;
477                 }
478                 // We only send CANCEL if we're the organizer.
479                 // If we're not, then we send REPLY with PartStat=Declined in handleInvitationsAfterChange()
480                 if (Akonadi::CalendarUtils::thatIsMe(incidence->organizer().email())) {
481                     // TODO: not to popup all delete message dialogs at once :(
482                     sendOk = false;
483                     handler->sendIncidenceDeletedMessage(KCalendarCore::iTIPCancel, incidence);
484                     if (change->atomicOperationId) {
485                         mInvitationStatusByAtomicOperation.insert(change->atomicOperationId, status);
486                     }
487                     // TODO: with some status we want to break immediately
488                 }
489             }
490 
491             if (sendOk) {
492                 change->emitUserDialogClosedBeforeChange(result);
493             }
494             return;
495         }
496         case IncidenceChanger::ChangeTypeModify: {
497             if (change->originalItems.isEmpty()) {
498                 break;
499             }
500 
501             Q_ASSERT(change->originalItems.count() == 1);
502             Incidence::Ptr oldIncidence = CalendarUtils::incidence(change->originalItems.first());
503             Incidence::Ptr newIncidence = CalendarUtils::incidence(change->newItem);
504 
505             if (!oldIncidence->supportsGroupwareCommunication()) {
506                 break;
507             }
508 
509             if (allowedModificationsWithoutRevisionUpdate(newIncidence)) {
510                 change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultSuccess);
511                 return;
512             }
513 
514             if (akonadi_calendar_running_unittests && !weAreOrganizer(newIncidence)) {
515                 // This is a bit of a workaround when running tests. I don't want to show the
516                 // "You're not organizer, do you want to modify event?" dialog in unit-tests, but want
517                 // to emulate a "yes" and a "no" press.
518                 if (m_invitationPolicy == IncidenceChanger::InvitationPolicySend) {
519                     change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultSuccess);
520                     return;
521                 } else if (m_invitationPolicy == IncidenceChanger::InvitationPolicyDontSend) {
522                     change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultCanceled);
523                     return;
524                 }
525             }
526 
527             ITIPHandlerHelper handler(mFactory, change->parentWidget);
528             const bool modify = handler.handleIncidenceAboutToBeModified(newIncidence);
529             if (modify) {
530                 break;
531             } else {
532                 result = ITIPHandlerHelper::ResultCanceled;
533             }
534 
535             if (newIncidence->type() == oldIncidence->type()) {
536                 IncidenceBase *i1 = newIncidence.data();
537                 IncidenceBase *i2 = oldIncidence.data();
538                 *i1 = *i2;
539             }
540             break;
541         }
542         default:
543             Q_ASSERT(false);
544             result = ITIPHandlerHelper::ResultCanceled;
545         }
546         change->emitUserDialogClosedBeforeChange(result);
547     } else {
548         change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultSuccess);
549     }
550 }
551 
handleInvitationsAfterChange(const Change::Ptr & change)552 void IncidenceChangerPrivate::handleInvitationsAfterChange(const Change::Ptr &change)
553 {
554     if (change->useGroupwareCommunication) {
555         auto handler = new ITIPHandlerHelper(mFactory, change->parentWidget);
556         connect(handler, &ITIPHandlerHelper::finished, change.data(), &Change::emitUserDialogClosedAfterChange);
557         handler->setParent(this);
558 
559         const bool alwaysSend = (m_invitationPolicy == IncidenceChanger::InvitationPolicySend);
560         const bool neverSend = (m_invitationPolicy == IncidenceChanger::InvitationPolicyDontSend);
561         if (alwaysSend) {
562             handler->setDefaultAction(ITIPHandlerDialogDelegate::ActionSendMessage);
563         }
564 
565         if (neverSend) {
566             handler->setDefaultAction(ITIPHandlerDialogDelegate::ActionDontSendMessage);
567         }
568 
569         switch (change->type) {
570         case IncidenceChanger::ChangeTypeCreate: {
571             Incidence::Ptr incidence = CalendarUtils::incidence(change->newItem);
572             if (incidence->supportsGroupwareCommunication()) {
573                 handler->sendIncidenceCreatedMessage(KCalendarCore::iTIPRequest, incidence);
574                 return;
575             }
576             break;
577         }
578         case IncidenceChanger::ChangeTypeDelete:
579             handler->deleteLater();
580             handler = nullptr;
581             Q_ASSERT(!change->originalItems.isEmpty());
582             for (const Akonadi::Item &item : std::as_const(change->originalItems)) {
583                 Q_ASSERT(item.hasPayload());
584                 Incidence::Ptr incidence = CalendarUtils::incidence(item);
585                 Q_ASSERT(incidence);
586                 if (!incidence->supportsGroupwareCommunication()) {
587                     continue;
588                 }
589 
590                 if (!Akonadi::CalendarUtils::thatIsMe(incidence->organizer().email())) {
591                     const QStringList myEmails = Akonadi::CalendarUtils::allEmails();
592                     bool notifyOrganizer = false;
593                     const KCalendarCore::Attendee me(incidence->attendeeByMails(myEmails));
594                     if (!me.isNull()) {
595                         if (me.status() == KCalendarCore::Attendee::Accepted || me.status() == KCalendarCore::Attendee::Delegated) {
596                             notifyOrganizer = true;
597                         }
598                         KCalendarCore::Attendee newMe(me);
599                         newMe.setStatus(KCalendarCore::Attendee::Declined);
600                         incidence->clearAttendees();
601                         incidence->addAttendee(newMe);
602                         // break;
603                     }
604 
605                     if (notifyOrganizer) {
606                         MailScheduler scheduler(mFactory, change->parentWidget); // TODO make async
607                         scheduler.performTransaction(incidence, KCalendarCore::iTIPReply);
608                     }
609                 }
610             }
611             break;
612         case IncidenceChanger::ChangeTypeModify: {
613             if (change->originalItems.isEmpty()) {
614                 break;
615             }
616 
617             Q_ASSERT(change->originalItems.count() == 1);
618             Incidence::Ptr oldIncidence = CalendarUtils::incidence(change->originalItems.first());
619             Incidence::Ptr newIncidence = CalendarUtils::incidence(change->newItem);
620 
621             if (!newIncidence->supportsGroupwareCommunication() || !Akonadi::CalendarUtils::thatIsMe(newIncidence->organizer().email())) {
622                 // If we're not the organizer, the user already saw the "Do you really want to do this, incidence will become out of sync"
623                 break;
624             }
625 
626             if (allowedModificationsWithoutRevisionUpdate(newIncidence)) {
627                 break;
628             }
629 
630             if (!neverSend && !alwaysSend && mInvitationStatusByAtomicOperation.contains(change->atomicOperationId)) {
631                 handler->setDefaultAction(actionFromStatus(mInvitationStatusByAtomicOperation.value(change->atomicOperationId)));
632             }
633 
634             const bool attendeeStatusChanged = myAttendeeStatusChanged(newIncidence, oldIncidence, Akonadi::CalendarUtils::allEmails());
635 
636             handler->sendIncidenceModifiedMessage(KCalendarCore::iTIPRequest, newIncidence, attendeeStatusChanged);
637             return;
638         }
639         default:
640             handler->deleteLater();
641             handler = nullptr;
642             Q_ASSERT(false);
643             change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultCanceled);
644             return;
645         }
646         handler->deleteLater();
647         handler = nullptr;
648         change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultSuccess);
649     } else {
650         change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultSuccess);
651     }
652 }
653 
654 /** static */
myAttendeeStatusChanged(const Incidence::Ptr & newInc,const Incidence::Ptr & oldInc,const QStringList & myEmails)655 bool IncidenceChangerPrivate::myAttendeeStatusChanged(const Incidence::Ptr &newInc, const Incidence::Ptr &oldInc, const QStringList &myEmails)
656 {
657     Q_ASSERT(newInc);
658     Q_ASSERT(oldInc);
659     const Attendee oldMe = oldInc->attendeeByMails(myEmails);
660     const Attendee newMe = newInc->attendeeByMails(myEmails);
661 
662     return !oldMe.isNull() && !newMe.isNull() && oldMe.status() != newMe.status();
663 }
664 
IncidenceChanger(QObject * parent)665 IncidenceChanger::IncidenceChanger(QObject *parent)
666     : QObject(parent)
667     , d(new IncidenceChangerPrivate(/**history=*/true, /*factory=*/nullptr, this))
668 {
669 }
670 
IncidenceChanger(ITIPHandlerComponentFactory * factory,QObject * parent)671 IncidenceChanger::IncidenceChanger(ITIPHandlerComponentFactory *factory, QObject *parent)
672     : QObject(parent)
673     , d(new IncidenceChangerPrivate(/**history=*/true, factory, this))
674 {
675 }
676 
IncidenceChanger(bool enableHistory,QObject * parent)677 IncidenceChanger::IncidenceChanger(bool enableHistory, QObject *parent)
678     : QObject(parent)
679     , d(new IncidenceChangerPrivate(enableHistory, /*factory=*/nullptr, this))
680 {
681 }
682 
683 IncidenceChanger::~IncidenceChanger() = default;
684 
createFromItem(const Item & item,const Collection & collection,QWidget * parent)685 int IncidenceChanger::createFromItem(const Item &item, const Collection &collection, QWidget *parent)
686 {
687     const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0;
688 
689     const Change::Ptr change(new CreationChange(this, ++d->mLatestChangeId, atomicOperationId, parent));
690     const int changeId = change->id;
691     Q_ASSERT(!(d->mBatchOperationInProgress && !d->mAtomicOperations.contains(atomicOperationId)));
692     if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) {
693         const QString errorMessage = d->showErrorDialog(ResultCodeRolledback, parent);
694         qCWarning(AKONADICALENDAR_LOG) << errorMessage;
695 
696         change->resultCode = ResultCodeRolledback;
697         change->errorString = errorMessage;
698         d->cleanupTransaction();
699         return changeId;
700     }
701 
702     change->newItem = item;
703 
704     d->step1DetermineDestinationCollection(change, collection);
705 
706     return change->id;
707 }
708 
createIncidence(const Incidence::Ptr & incidence,const Collection & collection,QWidget * parent)709 int IncidenceChanger::createIncidence(const Incidence::Ptr &incidence, const Collection &collection, QWidget *parent)
710 {
711     if (!incidence) {
712         qCWarning(AKONADICALENDAR_LOG) << "An invalid payload is not allowed.";
713         d->cancelTransaction();
714         return -1;
715     }
716 
717     Item item;
718     item.setPayload<KCalendarCore::Incidence::Ptr>(incidence);
719     item.setMimeType(incidence->mimeType());
720 
721     return createFromItem(item, collection, parent);
722 }
723 
deleteIncidence(const Item & item,QWidget * parent)724 int IncidenceChanger::deleteIncidence(const Item &item, QWidget *parent)
725 {
726     Item::List list;
727     list.append(item);
728 
729     return deleteIncidences(list, parent);
730 }
731 
deleteIncidences(const Item::List & items,QWidget * parent)732 int IncidenceChanger::deleteIncidences(const Item::List &items, QWidget *parent)
733 {
734     if (items.isEmpty()) {
735         qCritical() << "Delete what?";
736         d->cancelTransaction();
737         return -1;
738     }
739 
740     for (const Item &item : items) {
741         if (!item.isValid()) {
742             qCritical() << "Items must be valid!";
743             d->cancelTransaction();
744             return -1;
745         }
746     }
747 
748     const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0;
749     const int changeId = ++d->mLatestChangeId;
750     const Change::Ptr change(new DeletionChange(this, changeId, atomicOperationId, parent));
751 
752     for (const Item &item : items) {
753         if (!d->hasRights(item.parentCollection(), ChangeTypeDelete)) {
754             qCWarning(AKONADICALENDAR_LOG) << "Item " << item.id() << " can't be deleted due to ACL restrictions";
755             const QString errorString = d->showErrorDialog(ResultCodePermissions, parent);
756             change->resultCode = ResultCodePermissions;
757             change->errorString = errorString;
758             d->cancelTransaction();
759             return changeId;
760         }
761     }
762 
763     if (!d->allowAtomicOperation(atomicOperationId, change)) {
764         const QString errorString = d->showErrorDialog(ResultCodeDuplicateId, parent);
765         change->resultCode = ResultCodeDuplicateId;
766         change->errorString = errorString;
767         qCWarning(AKONADICALENDAR_LOG) << errorString;
768         d->cancelTransaction();
769         return changeId;
770     }
771 
772     Item::List itemsToDelete;
773     for (const Item &item : items) {
774         if (d->deleteAlreadyCalled(item.id())) {
775             // IncidenceChanger::deleteIncidence() called twice, ignore this one.
776             qCDebug(AKONADICALENDAR_LOG) << "Item " << item.id() << " already deleted or being deleted, skipping";
777         } else {
778             itemsToDelete.append(item);
779         }
780     }
781 
782     if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) {
783         const QString errorMessage = d->showErrorDialog(ResultCodeRolledback, parent);
784         change->resultCode = ResultCodeRolledback;
785         change->errorString = errorMessage;
786         qCritical() << errorMessage;
787         d->cleanupTransaction();
788         return changeId;
789     }
790 
791     if (itemsToDelete.isEmpty()) {
792         QVector<Akonadi::Item::Id> itemIdList;
793         itemIdList.append(Item().id());
794         qCDebug(AKONADICALENDAR_LOG) << "Items already deleted or being deleted, skipping";
795         const QString errorMessage = i18n("That calendar item was already deleted, or currently being deleted.");
796         // Queued emit because return must be executed first, otherwise caller won't know this workId
797         change->resultCode = ResultCodeAlreadyDeleted;
798         change->errorString = errorMessage;
799         d->cancelTransaction();
800         qCWarning(AKONADICALENDAR_LOG) << errorMessage;
801         return changeId;
802     }
803     change->originalItems = itemsToDelete;
804 
805     d->mChangeById.insert(changeId, change);
806 
807     if (d->mGroupwareCommunication) {
808         connect(change.data(), &Change::dialogClosedBeforeChange, d.get(), &IncidenceChangerPrivate::deleteIncidences2);
809         d->handleInvitationsBeforeChange(change);
810     } else {
811         d->deleteIncidences2(changeId, ITIPHandlerHelper::ResultSuccess);
812     }
813     return changeId;
814 }
815 
deleteIncidences2(int changeId,ITIPHandlerHelper::SendResult status)816 void IncidenceChangerPrivate::deleteIncidences2(int changeId, ITIPHandlerHelper::SendResult status)
817 {
818     Q_UNUSED(status)
819     Change::Ptr change = mChangeById[changeId];
820     const uint atomicOperationId = change->atomicOperationId;
821     auto deleteJob = new ItemDeleteJob(change->originalItems, parentJob(change));
822     mChangeForJob.insert(deleteJob, change);
823 
824     if (mBatchOperationInProgress) {
825         AtomicOperation *atomic = mAtomicOperations[atomicOperationId];
826         Q_ASSERT(atomic);
827         atomic->addChange(change);
828     }
829 
830     mDeletedItemIds.reserve(mDeletedItemIds.count() + change->originalItems.count());
831     for (const Item &item : std::as_const(change->originalItems)) {
832         mDeletedItemIds << item.id();
833     }
834 
835     // Do some cleanup
836     if (mDeletedItemIds.count() > 100) {
837         mDeletedItemIds.remove(0, 50);
838     }
839 
840     // QueuedConnection because of possible sync exec calls.
841     connect(deleteJob, &KJob::result, this, &IncidenceChangerPrivate::handleDeleteJobResult, Qt::QueuedConnection);
842 }
843 
modifyIncidence(const Item & changedItem,const KCalendarCore::Incidence::Ptr & originalPayload,QWidget * parent)844 int IncidenceChanger::modifyIncidence(const Item &changedItem, const KCalendarCore::Incidence::Ptr &originalPayload, QWidget *parent)
845 {
846     if (!changedItem.isValid() || !changedItem.hasPayload<Incidence::Ptr>()) {
847         qCWarning(AKONADICALENDAR_LOG) << "An invalid item or payload is not allowed.";
848         d->cancelTransaction();
849         return -1;
850     }
851 
852     if (!d->hasRights(changedItem.parentCollection(), ChangeTypeModify)) {
853         qCWarning(AKONADICALENDAR_LOG) << "Item " << changedItem.id() << " can't be deleted due to ACL restrictions";
854         const int changeId = ++d->mLatestChangeId;
855         const QString errorString = d->showErrorDialog(ResultCodePermissions, parent);
856         emitModifyFinished(this, changeId, changedItem, ResultCodePermissions, errorString);
857         d->cancelTransaction();
858         return changeId;
859     }
860 
861     // TODO also update revision here instead of in the editor
862     changedItem.payload<Incidence::Ptr>()->setLastModified(QDateTime::currentDateTimeUtc());
863 
864     const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0;
865     const int changeId = ++d->mLatestChangeId;
866     auto modificationChange = new ModificationChange(this, changeId, atomicOperationId, parent);
867     Change::Ptr change(modificationChange);
868 
869     if (originalPayload) {
870         Item originalItem(changedItem);
871         originalItem.setPayload<KCalendarCore::Incidence::Ptr>(originalPayload);
872         modificationChange->originalItems << originalItem;
873     }
874 
875     modificationChange->newItem = changedItem;
876     d->mChangeById.insert(changeId, change);
877 
878     if (!d->allowAtomicOperation(atomicOperationId, change)) {
879         const QString errorString = d->showErrorDialog(ResultCodeDuplicateId, parent);
880 
881         change->resultCode = ResultCodeDuplicateId;
882         change->errorString = errorString;
883         d->cancelTransaction();
884         qCWarning(AKONADICALENDAR_LOG) << "Atomic operation now allowed";
885         return changeId;
886     }
887 
888     if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) {
889         const QString errorMessage = d->showErrorDialog(ResultCodeRolledback, parent);
890         qCritical() << errorMessage;
891         d->cleanupTransaction();
892         emitModifyFinished(this, changeId, changedItem, ResultCodeRolledback, errorMessage);
893     } else {
894         d->adjustRecurrence(originalPayload, CalendarUtils::incidence(modificationChange->newItem));
895         d->performModification(change);
896     }
897 
898     return changeId;
899 }
900 
performModification(const Change::Ptr & change)901 void IncidenceChangerPrivate::performModification(const Change::Ptr &change)
902 {
903     const Item::Id id = change->newItem.id();
904     Akonadi::Item &newItem = change->newItem;
905     Q_ASSERT(newItem.isValid());
906     Q_ASSERT(newItem.hasPayload<Incidence::Ptr>());
907 
908     const int changeId = change->id;
909 
910     if (deleteAlreadyCalled(id)) {
911         // IncidenceChanger::deleteIncidence() called twice, ignore this one.
912         qCDebug(AKONADICALENDAR_LOG) << "Item " << id << " already deleted or being deleted, skipping";
913 
914         // Queued emit because return must be executed first, otherwise caller won't know this workId
915         emitModifyFinished(q,
916                            change->id,
917                            newItem,
918                            IncidenceChanger::ResultCodeAlreadyDeleted,
919                            i18n("That calendar item was already deleted, or currently being deleted."));
920         return;
921     }
922 
923     const uint atomicOperationId = change->atomicOperationId;
924     const bool hasAtomicOperationId = atomicOperationId != 0;
925     if (hasAtomicOperationId && mAtomicOperations[atomicOperationId]->rolledback()) {
926         const QString errorMessage = showErrorDialog(IncidenceChanger::ResultCodeRolledback, nullptr);
927         qCritical() << errorMessage;
928         emitModifyFinished(q, changeId, newItem, IncidenceChanger::ResultCodeRolledback, errorMessage);
929         return;
930     }
931     if (mGroupwareCommunication) {
932         connect(change.data(), &Change::dialogClosedBeforeChange, this, &IncidenceChangerPrivate::performModification2);
933         handleInvitationsBeforeChange(change);
934     } else {
935         performModification2(change->id, ITIPHandlerHelper::ResultSuccess);
936     }
937 }
938 
performModification2(int changeId,ITIPHandlerHelper::SendResult status)939 void IncidenceChangerPrivate::performModification2(int changeId, ITIPHandlerHelper::SendResult status)
940 {
941     Change::Ptr change = mChangeById[changeId];
942     const Item::Id id = change->newItem.id();
943     Akonadi::Item &newItem = change->newItem;
944     Q_ASSERT(newItem.isValid());
945     Q_ASSERT(newItem.hasPayload<Incidence::Ptr>());
946     if (status == ITIPHandlerHelper::ResultCanceled) { // TODO:fireout what is right here:)
947         // User got a "You're not the organizer, do you really want to send" dialog, and said "no"
948         qCDebug(AKONADICALENDAR_LOG) << "User cancelled, giving up";
949         emitModifyFinished(q, change->id, newItem, IncidenceChanger::ResultCodeUserCanceled, QString());
950         return;
951     }
952 
953     const uint atomicOperationId = change->atomicOperationId;
954     const bool hasAtomicOperationId = atomicOperationId != 0;
955 
956     QHash<Akonadi::Item::Id, int> &latestRevisionByItemId = *(s_latestRevisionByItemId());
957     if (latestRevisionByItemId.contains(id) && latestRevisionByItemId[id] > newItem.revision()) {
958         /* When a ItemModifyJob ends, the application can still modify the old items if the user
959          * is quick because the ETM wasn't updated yet, and we'll get a STORE error, because
960          * we are not modifying the latest revision.
961          *
962          * When a job ends, we keep the new revision in s_latestRevisionByItemId
963          * so we can update the item's revision
964          */
965         newItem.setRevision(latestRevisionByItemId[id]);
966     }
967 
968     Incidence::Ptr incidence = CalendarUtils::incidence(newItem);
969     {
970         if (!allowedModificationsWithoutRevisionUpdate(incidence)) { // increment revision ( KCalendarCore revision, not akonadi )
971             const int revision = incidence->revision();
972             incidence->setRevision(revision + 1);
973         }
974 
975         // Reset attendee status, when resceduling
976         QSet<IncidenceBase::Field> resetPartStatus;
977         resetPartStatus << IncidenceBase::FieldDtStart << IncidenceBase::FieldDtEnd << IncidenceBase::FieldDtStart << IncidenceBase::FieldLocation
978                         << IncidenceBase::FieldDtDue << IncidenceBase::FieldDuration << IncidenceBase::FieldRecurrence;
979         if (!(incidence->dirtyFields() & resetPartStatus).isEmpty() && weAreOrganizer(incidence)) {
980             auto attendees = incidence->attendees();
981             for (auto &attendee : attendees) {
982                 if (attendee.role() != Attendee::NonParticipant && attendee.status() != Attendee::Delegated && !Akonadi::CalendarUtils::thatIsMe(attendee)) {
983                     attendee.setStatus(Attendee::NeedsAction);
984                     attendee.setRSVP(true);
985                 }
986             }
987             incidence->setAttendees(attendees);
988         }
989     }
990 
991     // Dav Fix
992     // Don't write back remote revision since we can't make sure it is the current one
993     newItem.setRemoteRevision(QString());
994 
995     if (mModificationsInProgress.contains(newItem.id())) {
996         // There's already a ItemModifyJob running for this item ID
997         // Let's wait for it to end.
998         queueModification(change);
999     } else {
1000         auto modifyJob = new ItemModifyJob(newItem, parentJob(change));
1001         mChangeForJob.insert(modifyJob, change);
1002         mDirtyFieldsByJob.insert(modifyJob, incidence->dirtyFields());
1003 
1004         if (hasAtomicOperationId) {
1005             AtomicOperation *atomic = mAtomicOperations[atomicOperationId];
1006             Q_ASSERT(atomic);
1007             atomic->addChange(change);
1008         }
1009 
1010         mModificationsInProgress[newItem.id()] = change;
1011         // QueuedConnection because of possible sync exec calls.
1012         connect(modifyJob, &KJob::result, this, &IncidenceChangerPrivate::handleModifyJobResult, Qt::QueuedConnection);
1013     }
1014 }
1015 
startAtomicOperation(const QString & operationDescription)1016 void IncidenceChanger::startAtomicOperation(const QString &operationDescription)
1017 {
1018     if (d->mBatchOperationInProgress) {
1019         qCDebug(AKONADICALENDAR_LOG) << "An atomic operation is already in progress.";
1020         return;
1021     }
1022 
1023     ++d->mLatestAtomicOperationId;
1024     d->mBatchOperationInProgress = true;
1025 
1026     auto atomicOperation = new AtomicOperation(d.get(), d->mLatestAtomicOperationId);
1027     atomicOperation->m_description = operationDescription;
1028     d->mAtomicOperations.insert(d->mLatestAtomicOperationId, atomicOperation);
1029 }
1030 
endAtomicOperation()1031 void IncidenceChanger::endAtomicOperation()
1032 {
1033     if (!d->mBatchOperationInProgress) {
1034         qCDebug(AKONADICALENDAR_LOG) << "No atomic operation is in progress.";
1035         return;
1036     }
1037 
1038     Q_ASSERT_X(d->mLatestAtomicOperationId != 0, "IncidenceChanger::endAtomicOperation()", "Call startAtomicOperation() first.");
1039 
1040     Q_ASSERT(d->mAtomicOperations.contains(d->mLatestAtomicOperationId));
1041     AtomicOperation *atomicOperation = d->mAtomicOperations[d->mLatestAtomicOperationId];
1042     Q_ASSERT(atomicOperation);
1043     atomicOperation->m_endCalled = true;
1044 
1045     const bool allJobsCompleted = !atomicOperation->pendingJobs();
1046 
1047     if (allJobsCompleted && atomicOperation->rolledback() && atomicOperation->m_transactionCompleted) {
1048         // The transaction job already completed, we can cleanup:
1049         delete d->mAtomicOperations.take(d->mLatestAtomicOperationId);
1050         d->mBatchOperationInProgress = false;
1051     } /* else if ( allJobsCompleted ) {
1052      Q_ASSERT( atomicOperation->transaction );
1053      atomicOperation->transaction->commit(); we using autocommit now
1054    }*/
1055 }
1056 
setShowDialogsOnError(bool enable)1057 void IncidenceChanger::setShowDialogsOnError(bool enable)
1058 {
1059     d->mShowDialogsOnError = enable;
1060     if (d->mHistory) {
1061         d->mHistory->incidenceChanger()->setShowDialogsOnError(enable);
1062     }
1063 }
1064 
showDialogsOnError() const1065 bool IncidenceChanger::showDialogsOnError() const
1066 {
1067     return d->mShowDialogsOnError;
1068 }
1069 
setRespectsCollectionRights(bool respects)1070 void IncidenceChanger::setRespectsCollectionRights(bool respects)
1071 {
1072     d->mRespectsCollectionRights = respects;
1073 }
1074 
respectsCollectionRights() const1075 bool IncidenceChanger::respectsCollectionRights() const
1076 {
1077     return d->mRespectsCollectionRights;
1078 }
1079 
setDestinationPolicy(IncidenceChanger::DestinationPolicy destinationPolicy)1080 void IncidenceChanger::setDestinationPolicy(IncidenceChanger::DestinationPolicy destinationPolicy)
1081 {
1082     d->mDestinationPolicy = destinationPolicy;
1083 }
1084 
destinationPolicy() const1085 IncidenceChanger::DestinationPolicy IncidenceChanger::destinationPolicy() const
1086 {
1087     return d->mDestinationPolicy;
1088 }
1089 
setEntityTreeModel(Akonadi::EntityTreeModel * entityTreeModel)1090 void IncidenceChanger::setEntityTreeModel(Akonadi::EntityTreeModel *entityTreeModel)
1091 {
1092     d->mEntityTreeModel = entityTreeModel;
1093 }
1094 
entityTreeModel() const1095 Akonadi::EntityTreeModel *IncidenceChanger::entityTreeModel() const
1096 {
1097     return d->mEntityTreeModel;
1098 }
1099 
setDefaultCollection(const Akonadi::Collection & collection)1100 void IncidenceChanger::setDefaultCollection(const Akonadi::Collection &collection)
1101 {
1102     d->mDefaultCollection = collection;
1103 }
1104 
defaultCollection() const1105 Collection IncidenceChanger::defaultCollection() const
1106 {
1107     return d->mDefaultCollection;
1108 }
1109 
historyEnabled() const1110 bool IncidenceChanger::historyEnabled() const
1111 {
1112     return d->mUseHistory;
1113 }
1114 
setHistoryEnabled(bool enable)1115 void IncidenceChanger::setHistoryEnabled(bool enable)
1116 {
1117     if (d->mUseHistory != enable) {
1118         d->mUseHistory = enable;
1119         if (enable && !d->mHistory) {
1120             d->mHistory = new History(d.get());
1121         }
1122     }
1123 }
1124 
history() const1125 History *IncidenceChanger::history() const
1126 {
1127     return d->mHistory;
1128 }
1129 
deletedRecently(Akonadi::Item::Id id) const1130 bool IncidenceChanger::deletedRecently(Akonadi::Item::Id id) const
1131 {
1132     return d->deleteAlreadyCalled(id);
1133 }
1134 
setGroupwareCommunication(bool enabled)1135 void IncidenceChanger::setGroupwareCommunication(bool enabled)
1136 {
1137     d->mGroupwareCommunication = enabled;
1138 }
1139 
groupwareCommunication() const1140 bool IncidenceChanger::groupwareCommunication() const
1141 {
1142     return d->mGroupwareCommunication;
1143 }
1144 
setAutoAdjustRecurrence(bool enable)1145 void IncidenceChanger::setAutoAdjustRecurrence(bool enable)
1146 {
1147     d->mAutoAdjustRecurrence = enable;
1148 }
1149 
autoAdjustRecurrence() const1150 bool IncidenceChanger::autoAdjustRecurrence() const
1151 {
1152     return d->mAutoAdjustRecurrence;
1153 }
1154 
setInvitationPolicy(IncidenceChanger::InvitationPolicy policy)1155 void IncidenceChanger::setInvitationPolicy(IncidenceChanger::InvitationPolicy policy)
1156 {
1157     d->m_invitationPolicy = policy;
1158 }
1159 
invitationPolicy() const1160 IncidenceChanger::InvitationPolicy IncidenceChanger::invitationPolicy() const
1161 {
1162     return d->m_invitationPolicy;
1163 }
1164 
lastCollectionUsed() const1165 Akonadi::Collection IncidenceChanger::lastCollectionUsed() const
1166 {
1167     return d->mLastCollectionUsed;
1168 }
1169 
showErrorDialog(IncidenceChanger::ResultCode resultCode,QWidget * parent)1170 QString IncidenceChangerPrivate::showErrorDialog(IncidenceChanger::ResultCode resultCode, QWidget *parent)
1171 {
1172     QString errorString;
1173     switch (resultCode) {
1174     case IncidenceChanger::ResultCodePermissions:
1175         errorString = i18n("Operation can not be performed due to ACL restrictions");
1176         break;
1177     case IncidenceChanger::ResultCodeInvalidUserCollection:
1178         errorString = i18n("The chosen collection is invalid");
1179         break;
1180     case IncidenceChanger::ResultCodeInvalidDefaultCollection:
1181         errorString = i18n(
1182             "Default collection is invalid or doesn't have proper ACLs"
1183             " and DestinationPolicyNeverAsk was used");
1184         break;
1185     case IncidenceChanger::ResultCodeDuplicateId:
1186         errorString = i18n("Duplicate item id in a group operation");
1187         break;
1188     case IncidenceChanger::ResultCodeRolledback:
1189         errorString = i18n(
1190             "One change belonging to a group of changes failed. "
1191             "All changes are being rolled back.");
1192         break;
1193     default:
1194         Q_ASSERT(false);
1195         return QString(i18n("Unknown error"));
1196     }
1197 
1198     if (mShowDialogsOnError) {
1199         KMessageBox::sorry(parent, errorString);
1200     }
1201 
1202     return errorString;
1203 }
1204 
adjustRecurrence(const KCalendarCore::Incidence::Ptr & originalIncidence,const KCalendarCore::Incidence::Ptr & incidence)1205 void IncidenceChangerPrivate::adjustRecurrence(const KCalendarCore::Incidence::Ptr &originalIncidence, const KCalendarCore::Incidence::Ptr &incidence)
1206 {
1207     if (!originalIncidence || !incidence->recurs() || incidence->hasRecurrenceId() || !mAutoAdjustRecurrence
1208         || !incidence->dirtyFields().contains(KCalendarCore::Incidence::FieldDtStart)) {
1209         return;
1210     }
1211 
1212     const QDate originalDate = originalIncidence->dtStart().date();
1213     const QDate newStartDate = incidence->dtStart().date();
1214 
1215     if (!originalDate.isValid() || !newStartDate.isValid() || originalDate == newStartDate) {
1216         return;
1217     }
1218 
1219     KCalendarCore::Recurrence *recurrence = incidence->recurrence();
1220     switch (recurrence->recurrenceType()) {
1221     case KCalendarCore::Recurrence::rWeekly: {
1222         QBitArray days = recurrence->days();
1223         const int oldIndex = originalDate.dayOfWeek() - 1; // QDate returns [1-7];
1224         const int newIndex = newStartDate.dayOfWeek() - 1;
1225         if (oldIndex != newIndex) {
1226             days.clearBit(oldIndex);
1227             days.setBit(newIndex);
1228             recurrence->setWeekly(recurrence->frequency(), days);
1229         }
1230     }
1231     default:
1232         break; // Other types not implemented
1233     }
1234 
1235     // Now fix cases where dtstart would be bigger than the recurrence end rendering it impossible for a view to show it:
1236     // To retrieve the recurrence end don't trust Recurrence::endDt() since it returns dtStart if the rrule's end is < than dtstart,
1237     // it seems someone made Recurrence::endDt() more robust, but getNextOccurrences() still craps out. So lets fix it here
1238     // there's no reason to write bogus ical to disk.
1239     const QDate recurrenceEndDate = recurrence->defaultRRule() ? recurrence->defaultRRule()->endDt().date() : QDate();
1240     if (recurrenceEndDate.isValid() && recurrenceEndDate < newStartDate) {
1241         recurrence->setEndDate(newStartDate);
1242     }
1243 }
1244 
cancelTransaction()1245 void IncidenceChangerPrivate::cancelTransaction()
1246 {
1247     if (mBatchOperationInProgress) {
1248         mAtomicOperations[mLatestAtomicOperationId]->setRolledback();
1249     }
1250 }
1251 
cleanupTransaction()1252 void IncidenceChangerPrivate::cleanupTransaction()
1253 {
1254     Q_ASSERT(mAtomicOperations.contains(mLatestAtomicOperationId));
1255     AtomicOperation *operation = mAtomicOperations[mLatestAtomicOperationId];
1256     Q_ASSERT(operation);
1257     Q_ASSERT(operation->rolledback());
1258     if (!operation->pendingJobs() && operation->m_endCalled && operation->m_transactionCompleted) {
1259         delete mAtomicOperations.take(mLatestAtomicOperationId);
1260         mBatchOperationInProgress = false;
1261     }
1262 }
1263 
allowAtomicOperation(int atomicOperationId,const Change::Ptr & change) const1264 bool IncidenceChangerPrivate::allowAtomicOperation(int atomicOperationId, const Change::Ptr &change) const
1265 {
1266     bool allow = true;
1267     if (atomicOperationId > 0) {
1268         Q_ASSERT(mAtomicOperations.contains(atomicOperationId));
1269         AtomicOperation *operation = mAtomicOperations.value(atomicOperationId);
1270 
1271         if (change->type == IncidenceChanger::ChangeTypeCreate) {
1272             allow = true;
1273         } else if (change->type == IncidenceChanger::ChangeTypeModify) {
1274             allow = !operation->m_itemIdsInOperation.contains(change->newItem.id());
1275         } else if (change->type == IncidenceChanger::ChangeTypeDelete) {
1276             DeletionChange::Ptr deletion = change.staticCast<DeletionChange>();
1277             for (Akonadi::Item::Id id : std::as_const(deletion->mItemIds)) {
1278                 if (operation->m_itemIdsInOperation.contains(id)) {
1279                     allow = false;
1280                     break;
1281                 }
1282             }
1283         }
1284     }
1285 
1286     if (!allow) {
1287         qCWarning(AKONADICALENDAR_LOG) << "Each change belonging to a group operation"
1288                                        << "must have a different Akonadi::Item::Id";
1289     }
1290 
1291     return allow;
1292 }
1293 
1294 /**reimp*/
emitCompletionSignal()1295 void ModificationChange::emitCompletionSignal()
1296 {
1297     emitModifyFinished(changer, id, newItem, resultCode, errorString);
1298 }
1299 
1300 /**reimp*/
emitCompletionSignal()1301 void CreationChange::emitCompletionSignal()
1302 {
1303     // Does a queued emit, with QMetaObject::invokeMethod
1304     emitCreateFinished(changer, id, newItem, resultCode, errorString);
1305 }
1306 
1307 /**reimp*/
emitCompletionSignal()1308 void DeletionChange::emitCompletionSignal()
1309 {
1310     emitDeleteFinished(changer, id, mItemIds, resultCode, errorString);
1311 }
1312 
1313 /**
1314 Lost code from KDE 4.4 that was never called/used with incidenceeditors-ng.
1315 
1316       Attendees were removed from this incidence. Only the removed attendees
1317       are present in the incidence, so we just need to send a cancel messages
1318       to all attendees groupware messages are enabled at all.
1319 
1320 void IncidenceChanger::cancelAttendees( const Akonadi::Item &aitem )
1321 {
1322   const KCalendarCore::Incidence::Ptr incidence = CalendarSupport::incidence( aitem );
1323   Q_ASSERT( incidence );
1324   if ( KCalPrefs::instance()->mUseGroupwareCommunication ) {
1325     if ( KMessageBox::questionYesNo(
1326            0,
1327            i18n( "Some attendees were removed from the incidence. "
1328                  "Shall cancel messages be sent to these attendees?" ),
1329            i18n( "Attendees Removed" ), KGuiItem( i18n( "Send Messages" ) ),
1330            KGuiItem( i18n( "Do Not Send" ) ) ) == KMessageBox::Yes ) {
1331       // don't use Akonadi::Groupware::sendICalMessage here, because that asks just
1332       // a very general question "Other people are involved, send message to
1333       // them?", which isn't helpful at all in this situation. Afterwards, it
1334       // would only call the Akonadi::MailScheduler::performTransaction, so do this
1335       // manually.
1336       CalendarSupport::MailScheduler scheduler(
1337         static_cast<CalendarSupport::Calendar*>(d->mCalendar) );
1338       scheduler.performTransaction( incidence, KCalendarCore::iTIPCancel );
1339     }
1340   }
1341 }
1342 
1343 */
1344 
AtomicOperation(IncidenceChangerPrivate * icp,uint ident)1345 AtomicOperation::AtomicOperation(IncidenceChangerPrivate *icp, uint ident)
1346     : m_id(ident)
1347     , m_endCalled(false)
1348     , m_numCompletedChanges(0)
1349     , m_transactionCompleted(false)
1350     , m_wasRolledback(false)
1351     , m_transaction(nullptr)
1352     , m_incidenceChangerPrivate(icp)
1353 {
1354     Q_ASSERT(m_id != 0);
1355 }
1356 
transaction()1357 Akonadi::TransactionSequence *AtomicOperation::transaction()
1358 {
1359     if (!m_transaction) {
1360         m_transaction = new Akonadi::TransactionSequence;
1361         m_transaction->setAutomaticCommittingEnabled(true);
1362 
1363         m_incidenceChangerPrivate->mAtomicOperationByTransaction.insert(m_transaction, m_id);
1364 
1365         QObject::connect(m_transaction, &KJob::result, m_incidenceChangerPrivate, &IncidenceChangerPrivate::handleTransactionJobResult);
1366     }
1367 
1368     return m_transaction;
1369 }
1370