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