Akonadi Calendar

incidencechanger.cpp
1 /*
2  SPDX-FileCopyrightText: 2004 Reinhold Kainhofer <[email protected]>
3  SPDX-FileCopyrightText: 2010-2012 Sérgio Martins <[email protected]>
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 <itemcreatejob.h>
13 #include <itemdeletejob.h>
14 #include <itemmodifyjob.h>
15 #include <transactionsequence.h>
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 
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:
42  case ITIPHandlerHelper::ResultSuccess:
44  default:
46  }
47 }
48 
49 static bool weAreOrganizer(const Incidence::Ptr &incidence)
50 {
51  const QString email = incidence->organizer().email();
52  return Akonadi::CalendarUtils::thatIsMe(email);
53 }
54 
55 static bool allowedModificationsWithoutRevisionUpdate(const Incidence::Ptr &incidence)
56 {
57  // Modifications that are per user allowd without getting outofsync with organisator
58  // * if only alarm settings are modified.
59  const QSet<KCalendarCore::IncidenceBase::Field> dirtyFields = incidence->dirtyFields();
61  alarmOnlyModify << IncidenceBase::FieldAlarms << IncidenceBase::FieldLastModified;
62  return dirtyFields == alarmOnlyModify;
63 }
64 
65 namespace Akonadi
66 {
67 // Does a queued emit, with QMetaObject::invokeMethod
68 static void emitCreateFinished(IncidenceChanger *changer,
69  int changeId,
70  const Akonadi::Item &item,
71  Akonadi::IncidenceChanger::ResultCode resultCode,
72  const QString &errorString)
73 {
75  "createFinished",
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
85 emitModifyFinished(IncidenceChanger *changer, int changeId, const Akonadi::Item &item, IncidenceChanger::ResultCode resultCode, const QString &errorString)
86 {
88  "modifyFinished",
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
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 {
104  "deleteFinished",
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 
114 Q_GLOBAL_STATIC(IdToRevisionHash, s_latestRevisionByItemId)
115 
116 IncidenceChanger::Private::Private(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 = DestinationPolicyDefault;
125  mRespectsCollectionRights = false;
126  mGroupwareCommunication = false;
127  mLatestAtomicOperationId = 0;
128  mBatchOperationInProgress = false;
129  mAutoAdjustRecurrence = true;
130  m_collectionFetchJob = nullptr;
131  m_invitationPolicy = 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 
140 IncidenceChanger::Private::~Private()
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 
148 bool IncidenceChanger::Private::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 
154 bool IncidenceChanger::Private::hasRights(const Collection &collection, IncidenceChanger::ChangeType changeType) const
155 {
156  bool result = false;
157  switch (changeType) {
158  case ChangeTypeCreate:
159  result = collection.rights() & Akonadi::Collection::CanCreateItem;
160  break;
161  case ChangeTypeModify:
162  result = collection.rights() & Akonadi::Collection::CanChangeItem;
163  break;
164  case 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 
174 Akonadi::Job *IncidenceChanger::Private::parentJob(const Change::Ptr &change) const
175 {
176  return (mBatchOperationInProgress && !change->queuedModification) ? mAtomicOperations[mLatestAtomicOperationId]->transaction() : nullptr;
177 }
178 
179 void IncidenceChanger::Private::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 = ResultCodeModificationDiscarded;
188  toBeDiscarded->completed = true;
189  mChangeById.remove(toBeDiscarded->id);
190  }
191 
192  change->queuedModification = true;
193  mQueuedModifications[id] = change;
194 }
195 
196 void IncidenceChanger::Private::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 
206 void IncidenceChanger::Private::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 
236 void IncidenceChanger::Private::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  ResultCode resultCode = 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, &Private::handleCreateJobResult2);
263  handleInvitationsAfterChange(change);
264  } else {
265  handleCreateJobResult2(change->id, ITIPHandlerHelper::ResultSuccess);
266  }
267  }
268 }
269 
270 void IncidenceChanger::Private::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 = ResultCodeSuccess;
301  // puff, change finally goes out of scope, and emits the incidenceCreated signal.
302 }
303 
304 void IncidenceChanger::Private::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  // Werent deleted due to error
334  mDeletedItemIds.remove(mDeletedItemIds.indexOf(item.id()));
335  }
336  mChangeById.remove(change->id);
337  change->resultCode = 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, &Private::handleDeleteJobResult2);
348  handleInvitationsAfterChange(change);
349  } else {
350  handleDeleteJobResult2(change->id, ITIPHandlerHelper::ResultSuccess);
351  }
352  }
353 }
354 
355 void IncidenceChanger::Private::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 = ResultCodeSuccess;
363  } else {
364  change->errorString = i18nc("errormessage for a job ended with an unexpected result", "An unknown error occurred");
365  change->resultCode = ResultCodeJobError;
366  }
367 
368  // puff, change finally goes out of scope, and emits the incidenceDeleted signal.
369 }
370 
371 void IncidenceChanger::Private::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  ResultCode resultCode = 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 = 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, &Private::handleModifyJobResult2);
419  handleInvitationsAfterChange(change);
420  } else {
421  handleModifyJobResult2(change->id, ITIPHandlerHelper::ResultSuccess);
422  }
423  }
424 }
425 
426 void IncidenceChanger::Private::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 = 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 
441 bool IncidenceChanger::Private::deleteAlreadyCalled(Akonadi::Item::Id id) const
442 {
443  return mDeletedItemIds.contains(id);
444 }
445 
446 void IncidenceChanger::Private::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 == InvitationPolicySend) {
463  handler->setDefaultAction(ITIPHandlerDialogDelegate::ActionSendMessage);
464  } else if (m_invitationPolicy == 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 : qAsConst(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 == InvitationPolicySend) {
519  change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultSuccess);
520  return;
521  } else if (m_invitationPolicy == 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 
552 void IncidenceChanger::Private::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 == InvitationPolicySend;
560  const bool neverSend = m_invitationPolicy == 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 : qAsConst(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 */
655 bool IncidenceChanger::Private::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 
665 IncidenceChanger::IncidenceChanger(QObject *parent)
666  : QObject(parent)
667  , d(new Private(/**history=*/true, /*factory=*/nullptr, this))
668 {
669 }
670 
671 IncidenceChanger::IncidenceChanger(ITIPHandlerComponentFactory *factory, QObject *parent)
672  : QObject(parent)
673  , d(new Private(/**history=*/true, factory, this))
674 {
675 }
676 
677 IncidenceChanger::IncidenceChanger(bool enableHistory, QObject *parent)
678  : QObject(parent)
679  , d(new Private(enableHistory, /*factory=*/nullptr, this))
680 {
681 }
682 
683 IncidenceChanger::~IncidenceChanger()
684 {
685  delete d;
686 }
687 
688 int IncidenceChanger::createFromItem(const Item &item, const Collection &collection, QWidget *parent)
689 {
690  const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0;
691 
692  const Change::Ptr change(new CreationChange(this, ++d->mLatestChangeId, atomicOperationId, parent));
693  const int changeId = change->id;
694  Q_ASSERT(!(d->mBatchOperationInProgress && !d->mAtomicOperations.contains(atomicOperationId)));
695  if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) {
696  const QString errorMessage = d->showErrorDialog(ResultCodeRolledback, parent);
697  qCWarning(AKONADICALENDAR_LOG) << errorMessage;
698 
699  change->resultCode = ResultCodeRolledback;
700  change->errorString = errorMessage;
701  d->cleanupTransaction();
702  return changeId;
703  }
704 
705  change->newItem = item;
706 
707  d->step1DetermineDestinationCollection(change, collection);
708 
709  return change->id;
710 }
711 
712 int IncidenceChanger::createIncidence(const Incidence::Ptr &incidence, const Collection &collection, QWidget *parent)
713 {
714  if (!incidence) {
715  qCWarning(AKONADICALENDAR_LOG) << "An invalid payload is not allowed.";
716  d->cancelTransaction();
717  return -1;
718  }
719 
720  Item item;
721  item.setPayload<KCalendarCore::Incidence::Ptr>(incidence);
722  item.setMimeType(incidence->mimeType());
723 
724  return createFromItem(item, collection, parent);
725 }
726 
727 int IncidenceChanger::deleteIncidence(const Item &item, QWidget *parent)
728 {
730  list.append(item);
731 
732  return deleteIncidences(list, parent);
733 }
734 
735 int IncidenceChanger::deleteIncidences(const Item::List &items, QWidget *parent)
736 {
737  if (items.isEmpty()) {
738  qCritical() << "Delete what?";
739  d->cancelTransaction();
740  return -1;
741  }
742 
743  for (const Item &item : items) {
744  if (!item.isValid()) {
745  qCritical() << "Items must be valid!";
746  d->cancelTransaction();
747  return -1;
748  }
749  }
750 
751  const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0;
752  const int changeId = ++d->mLatestChangeId;
753  const Change::Ptr change(new DeletionChange(this, changeId, atomicOperationId, parent));
754 
755  for (const Item &item : items) {
756  if (!d->hasRights(item.parentCollection(), ChangeTypeDelete)) {
757  qCWarning(AKONADICALENDAR_LOG) << "Item " << item.id() << " can't be deleted due to ACL restrictions";
758  const QString errorString = d->showErrorDialog(ResultCodePermissions, parent);
759  change->resultCode = ResultCodePermissions;
760  change->errorString = errorString;
761  d->cancelTransaction();
762  return changeId;
763  }
764  }
765 
766  if (!d->allowAtomicOperation(atomicOperationId, change)) {
767  const QString errorString = d->showErrorDialog(ResultCodeDuplicateId, parent);
768  change->resultCode = ResultCodeDuplicateId;
769  change->errorString = errorString;
770  qCWarning(AKONADICALENDAR_LOG) << errorString;
771  d->cancelTransaction();
772  return changeId;
773  }
774 
775  Item::List itemsToDelete;
776  for (const Item &item : items) {
777  if (d->deleteAlreadyCalled(item.id())) {
778  // IncidenceChanger::deleteIncidence() called twice, ignore this one.
779  qCDebug(AKONADICALENDAR_LOG) << "Item " << item.id() << " already deleted or being deleted, skipping";
780  } else {
781  itemsToDelete.append(item);
782  }
783  }
784 
785  if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) {
786  const QString errorMessage = d->showErrorDialog(ResultCodeRolledback, parent);
787  change->resultCode = ResultCodeRolledback;
788  change->errorString = errorMessage;
789  qCritical() << errorMessage;
790  d->cleanupTransaction();
791  return changeId;
792  }
793 
794  if (itemsToDelete.isEmpty()) {
795  QVector<Akonadi::Item::Id> itemIdList;
796  itemIdList.append(Item().id());
797  qCDebug(AKONADICALENDAR_LOG) << "Items already deleted or being deleted, skipping";
798  const QString errorMessage = i18n("That calendar item was already deleted, or currently being deleted.");
799  // Queued emit because return must be executed first, otherwise caller won't know this workId
800  change->resultCode = ResultCodeAlreadyDeleted;
801  change->errorString = errorMessage;
802  d->cancelTransaction();
803  qCWarning(AKONADICALENDAR_LOG) << errorMessage;
804  return changeId;
805  }
806  change->originalItems = itemsToDelete;
807 
808  d->mChangeById.insert(changeId, change);
809 
810  if (d->mGroupwareCommunication) {
811  connect(change.data(), &Change::dialogClosedBeforeChange, d, &Private::deleteIncidences2);
812  d->handleInvitationsBeforeChange(change);
813  } else {
814  d->deleteIncidences2(changeId, ITIPHandlerHelper::ResultSuccess);
815  }
816  return changeId;
817 }
818 
819 void IncidenceChanger::Private::deleteIncidences2(int changeId, ITIPHandlerHelper::SendResult status)
820 {
821  Q_UNUSED(status)
822  Change::Ptr change = mChangeById[changeId];
823  const uint atomicOperationId = change->atomicOperationId;
824  auto deleteJob = new ItemDeleteJob(change->originalItems, parentJob(change));
825  mChangeForJob.insert(deleteJob, change);
826 
827  if (mBatchOperationInProgress) {
828  AtomicOperation *atomic = mAtomicOperations[atomicOperationId];
829  Q_ASSERT(atomic);
830  atomic->addChange(change);
831  }
832 
833  mDeletedItemIds.reserve(mDeletedItemIds.count() + change->originalItems.count());
834  for (const Item &item : qAsConst(change->originalItems)) {
835  mDeletedItemIds << item.id();
836  }
837 
838  // Do some cleanup
839  if (mDeletedItemIds.count() > 100) {
840  mDeletedItemIds.remove(0, 50);
841  }
842 
843  // QueuedConnection because of possible sync exec calls.
844  connect(deleteJob, &KJob::result, this, &Private::handleDeleteJobResult, Qt::QueuedConnection);
845 }
846 
847 int IncidenceChanger::modifyIncidence(const Item &changedItem, const KCalendarCore::Incidence::Ptr &originalPayload, QWidget *parent)
848 {
849  if (!changedItem.isValid() || !changedItem.hasPayload<Incidence::Ptr>()) {
850  qCWarning(AKONADICALENDAR_LOG) << "An invalid item or payload is not allowed.";
851  d->cancelTransaction();
852  return -1;
853  }
854 
855  if (!d->hasRights(changedItem.parentCollection(), ChangeTypeModify)) {
856  qCWarning(AKONADICALENDAR_LOG) << "Item " << changedItem.id() << " can't be deleted due to ACL restrictions";
857  const int changeId = ++d->mLatestChangeId;
858  const QString errorString = d->showErrorDialog(ResultCodePermissions, parent);
859  emitModifyFinished(this, changeId, changedItem, ResultCodePermissions, errorString);
860  d->cancelTransaction();
861  return changeId;
862  }
863 
864  // TODO also update revision here instead of in the editor
865  changedItem.payload<Incidence::Ptr>()->setLastModified(QDateTime::currentDateTimeUtc());
866 
867  const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0;
868  const int changeId = ++d->mLatestChangeId;
869  auto modificationChange = new ModificationChange(this, changeId, atomicOperationId, parent);
870  Change::Ptr change(modificationChange);
871 
872  if (originalPayload) {
873  Item originalItem(changedItem);
874  originalItem.setPayload<KCalendarCore::Incidence::Ptr>(originalPayload);
875  modificationChange->originalItems << originalItem;
876  }
877 
878  modificationChange->newItem = changedItem;
879  d->mChangeById.insert(changeId, change);
880 
881  if (!d->allowAtomicOperation(atomicOperationId, change)) {
882  const QString errorString = d->showErrorDialog(ResultCodeDuplicateId, parent);
883 
884  change->resultCode = ResultCodeDuplicateId;
885  change->errorString = errorString;
886  d->cancelTransaction();
887  qCWarning(AKONADICALENDAR_LOG) << "Atomic operation now allowed";
888  return changeId;
889  }
890 
891  if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) {
892  const QString errorMessage = d->showErrorDialog(ResultCodeRolledback, parent);
893  qCritical() << errorMessage;
894  d->cleanupTransaction();
895  emitModifyFinished(this, changeId, changedItem, ResultCodeRolledback, errorMessage);
896  } else {
897  d->adjustRecurrence(originalPayload, CalendarUtils::incidence(modificationChange->newItem));
898  d->performModification(change);
899  }
900 
901  return changeId;
902 }
903 
904 void IncidenceChanger::Private::performModification(const Change::Ptr &change)
905 {
906  const Item::Id id = change->newItem.id();
907  Akonadi::Item &newItem = change->newItem;
908  Q_ASSERT(newItem.isValid());
909  Q_ASSERT(newItem.hasPayload<Incidence::Ptr>());
910 
911  const int changeId = change->id;
912 
913  if (deleteAlreadyCalled(id)) {
914  // IncidenceChanger::deleteIncidence() called twice, ignore this one.
915  qCDebug(AKONADICALENDAR_LOG) << "Item " << id << " already deleted or being deleted, skipping";
916 
917  // Queued emit because return must be executed first, otherwise caller won't know this workId
918  emitModifyFinished(q, change->id, newItem, ResultCodeAlreadyDeleted, i18n("That calendar item was already deleted, or currently being deleted."));
919  return;
920  }
921 
922  const uint atomicOperationId = change->atomicOperationId;
923  const bool hasAtomicOperationId = atomicOperationId != 0;
924  if (hasAtomicOperationId && mAtomicOperations[atomicOperationId]->rolledback()) {
925  const QString errorMessage = showErrorDialog(ResultCodeRolledback, nullptr);
926  qCritical() << errorMessage;
927  emitModifyFinished(q, changeId, newItem, ResultCodeRolledback, errorMessage);
928  return;
929  }
930  if (mGroupwareCommunication) {
931  connect(change.data(), &Change::dialogClosedBeforeChange, this, &Private::performModification2);
932  handleInvitationsBeforeChange(change);
933  } else {
934  performModification2(change->id, ITIPHandlerHelper::ResultSuccess);
935  }
936 }
937 
938 void IncidenceChanger::Private::performModification2(int changeId, ITIPHandlerHelper::SendResult status)
939 {
940  Change::Ptr change = mChangeById[changeId];
941  const Item::Id id = change->newItem.id();
942  Akonadi::Item &newItem = change->newItem;
943  Q_ASSERT(newItem.isValid());
944  Q_ASSERT(newItem.hasPayload<Incidence::Ptr>());
945  if (status == ITIPHandlerHelper::ResultCanceled) { // TODO:fireout what is right here:)
946  // User got a "You're not the organizer, do you really want to send" dialog, and said "no"
947  qCDebug(AKONADICALENDAR_LOG) << "User cancelled, giving up";
948  emitModifyFinished(q, change->id, newItem, ResultCodeUserCanceled, QString());
949  return;
950  }
951 
952  const uint atomicOperationId = change->atomicOperationId;
953  const bool hasAtomicOperationId = atomicOperationId != 0;
954 
955  QHash<Akonadi::Item::Id, int> &latestRevisionByItemId = *(s_latestRevisionByItemId());
956  if (latestRevisionByItemId.contains(id) && latestRevisionByItemId[id] > newItem.revision()) {
957  /* When a ItemModifyJob ends, the application can still modify the old items if the user
958  * is quick because the ETM wasn't updated yet, and we'll get a STORE error, because
959  * we are not modifying the latest revision.
960  *
961  * When a job ends, we keep the new revision in s_latestRevisionByItemId
962  * so we can update the item's revision
963  */
964  newItem.setRevision(latestRevisionByItemId[id]);
965  }
966 
967  Incidence::Ptr incidence = CalendarUtils::incidence(newItem);
968  {
969  if (!allowedModificationsWithoutRevisionUpdate(incidence)) { // increment revision ( KCalendarCore revision, not akonadi )
970  const int revision = incidence->revision();
971  incidence->setRevision(revision + 1);
972  }
973 
974  // Reset attendee status, when resceduling
975  QSet<IncidenceBase::Field> resetPartStatus;
976  resetPartStatus << IncidenceBase::FieldDtStart << IncidenceBase::FieldDtEnd << IncidenceBase::FieldDtStart << IncidenceBase::FieldLocation
977  << IncidenceBase::FieldDtDue << IncidenceBase::FieldDuration << IncidenceBase::FieldRecurrence;
978  if (!(incidence->dirtyFields() & resetPartStatus).isEmpty() && weAreOrganizer(incidence)) {
979  auto attendees = incidence->attendees();
980  for (auto &attendee : attendees) {
981  if (attendee.role() != Attendee::NonParticipant && attendee.status() != Attendee::Delegated && !Akonadi::CalendarUtils::thatIsMe(attendee)) {
982  attendee.setStatus(Attendee::NeedsAction);
983  attendee.setRSVP(true);
984  }
985  }
986  incidence->setAttendees(attendees);
987  }
988  }
989 
990  // Dav Fix
991  // Don't write back remote revision since we can't make sure it is the current one
992  newItem.setRemoteRevision(QString());
993 
994  if (mModificationsInProgress.contains(newItem.id())) {
995  // There's already a ItemModifyJob running for this item ID
996  // Let's wait for it to end.
997  queueModification(change);
998  } else {
999  auto modifyJob = new ItemModifyJob(newItem, parentJob(change));
1000  mChangeForJob.insert(modifyJob, change);
1001  mDirtyFieldsByJob.insert(modifyJob, incidence->dirtyFields());
1002 
1003  if (hasAtomicOperationId) {
1004  AtomicOperation *atomic = mAtomicOperations[atomicOperationId];
1005  Q_ASSERT(atomic);
1006  atomic->addChange(change);
1007  }
1008 
1009  mModificationsInProgress[newItem.id()] = change;
1010  // QueuedConnection because of possible sync exec calls.
1011  connect(modifyJob, &KJob::result, this, &Private::handleModifyJobResult, Qt::QueuedConnection);
1012  }
1013 }
1014 
1015 void IncidenceChanger::startAtomicOperation(const QString &operationDescription)
1016 {
1017  if (d->mBatchOperationInProgress) {
1018  qCDebug(AKONADICALENDAR_LOG) << "An atomic operation is already in progress.";
1019  return;
1020  }
1021 
1022  ++d->mLatestAtomicOperationId;
1023  d->mBatchOperationInProgress = true;
1024 
1025  auto atomicOperation = new AtomicOperation(d, d->mLatestAtomicOperationId);
1026  atomicOperation->m_description = operationDescription;
1027  d->mAtomicOperations.insert(d->mLatestAtomicOperationId, atomicOperation);
1028 }
1029 
1030 void IncidenceChanger::endAtomicOperation()
1031 {
1032  if (!d->mBatchOperationInProgress) {
1033  qCDebug(AKONADICALENDAR_LOG) << "No atomic operation is in progress.";
1034  return;
1035  }
1036 
1037  Q_ASSERT_X(d->mLatestAtomicOperationId != 0, "IncidenceChanger::endAtomicOperation()", "Call startAtomicOperation() first.");
1038 
1039  Q_ASSERT(d->mAtomicOperations.contains(d->mLatestAtomicOperationId));
1040  AtomicOperation *atomicOperation = d->mAtomicOperations[d->mLatestAtomicOperationId];
1041  Q_ASSERT(atomicOperation);
1042  atomicOperation->m_endCalled = true;
1043 
1044  const bool allJobsCompleted = !atomicOperation->pendingJobs();
1045 
1046  if (allJobsCompleted && atomicOperation->rolledback() && atomicOperation->m_transactionCompleted) {
1047  // The transaction job already completed, we can cleanup:
1048  delete d->mAtomicOperations.take(d->mLatestAtomicOperationId);
1049  d->mBatchOperationInProgress = false;
1050  } /* else if ( allJobsCompleted ) {
1051  Q_ASSERT( atomicOperation->transaction );
1052  atomicOperation->transaction->commit(); we using autocommit now
1053  }*/
1054 }
1055 
1056 void IncidenceChanger::setShowDialogsOnError(bool enable)
1057 {
1058  d->mShowDialogsOnError = enable;
1059  if (d->mHistory) {
1060  d->mHistory->incidenceChanger()->setShowDialogsOnError(enable);
1061  }
1062 }
1063 
1064 bool IncidenceChanger::showDialogsOnError() const
1065 {
1066  return d->mShowDialogsOnError;
1067 }
1068 
1069 void IncidenceChanger::setRespectsCollectionRights(bool respects)
1070 {
1071  d->mRespectsCollectionRights = respects;
1072 }
1073 
1074 bool IncidenceChanger::respectsCollectionRights() const
1075 {
1076  return d->mRespectsCollectionRights;
1077 }
1078 
1079 void IncidenceChanger::setDestinationPolicy(IncidenceChanger::DestinationPolicy destinationPolicy)
1080 {
1081  d->mDestinationPolicy = destinationPolicy;
1082 }
1083 
1084 IncidenceChanger::DestinationPolicy IncidenceChanger::destinationPolicy() const
1085 {
1086  return d->mDestinationPolicy;
1087 }
1088 
1089 void IncidenceChanger::setEntityTreeModel(Akonadi::EntityTreeModel *entityTreeModel)
1090 {
1091  d->mEntityTreeModel = entityTreeModel;
1092 }
1093 
1094 Akonadi::EntityTreeModel *IncidenceChanger::entityTreeModel() const
1095 {
1096  return d->mEntityTreeModel;
1097 }
1098 
1099 void IncidenceChanger::setDefaultCollection(const Akonadi::Collection &collection)
1100 {
1101  d->mDefaultCollection = collection;
1102 }
1103 
1104 Collection IncidenceChanger::defaultCollection() const
1105 {
1106  return d->mDefaultCollection;
1107 }
1108 
1109 bool IncidenceChanger::historyEnabled() const
1110 {
1111  return d->mUseHistory;
1112 }
1113 
1114 void IncidenceChanger::setHistoryEnabled(bool enable)
1115 {
1116  if (d->mUseHistory != enable) {
1117  d->mUseHistory = enable;
1118  if (enable && !d->mHistory) {
1119  d->mHistory = new History(d);
1120  }
1121  }
1122 }
1123 
1124 History *IncidenceChanger::history() const
1125 {
1126  return d->mHistory;
1127 }
1128 
1129 bool IncidenceChanger::deletedRecently(Akonadi::Item::Id id) const
1130 {
1131  return d->deleteAlreadyCalled(id);
1132 }
1133 
1134 void IncidenceChanger::setGroupwareCommunication(bool enabled)
1135 {
1136  d->mGroupwareCommunication = enabled;
1137 }
1138 
1139 bool IncidenceChanger::groupwareCommunication() const
1140 {
1141  return d->mGroupwareCommunication;
1142 }
1143 
1144 void IncidenceChanger::setAutoAdjustRecurrence(bool enable)
1145 {
1146  d->mAutoAdjustRecurrence = enable;
1147 }
1148 
1149 bool IncidenceChanger::autoAdjustRecurrence() const
1150 {
1151  return d->mAutoAdjustRecurrence;
1152 }
1153 
1154 void IncidenceChanger::setInvitationPolicy(IncidenceChanger::InvitationPolicy policy)
1155 {
1156  d->m_invitationPolicy = policy;
1157 }
1158 
1159 IncidenceChanger::InvitationPolicy IncidenceChanger::invitationPolicy() const
1160 {
1161  return d->m_invitationPolicy;
1162 }
1163 
1164 Akonadi::Collection IncidenceChanger::lastCollectionUsed() const
1165 {
1166  return d->mLastCollectionUsed;
1167 }
1168 
1169 QString IncidenceChanger::Private::showErrorDialog(IncidenceChanger::ResultCode resultCode, QWidget *parent)
1170 {
1171  QString errorString;
1172  switch (resultCode) {
1173  case IncidenceChanger::ResultCodePermissions:
1174  errorString = i18n("Operation can not be performed due to ACL restrictions");
1175  break;
1176  case IncidenceChanger::ResultCodeInvalidUserCollection:
1177  errorString = i18n("The chosen collection is invalid");
1178  break;
1179  case IncidenceChanger::ResultCodeInvalidDefaultCollection:
1180  errorString = i18n(
1181  "Default collection is invalid or doesn't have proper ACLs"
1182  " and DestinationPolicyNeverAsk was used");
1183  break;
1184  case IncidenceChanger::ResultCodeDuplicateId:
1185  errorString = i18n("Duplicate item id in a group operation");
1186  break;
1187  case IncidenceChanger::ResultCodeRolledback:
1188  errorString = i18n(
1189  "One change belonging to a group of changes failed. "
1190  "All changes are being rolled back.");
1191  break;
1192  default:
1193  Q_ASSERT(false);
1194  return QString(i18n("Unknown error"));
1195  }
1196 
1197  if (mShowDialogsOnError) {
1198  KMessageBox::sorry(parent, errorString);
1199  }
1200 
1201  return errorString;
1202 }
1203 
1204 void IncidenceChanger::Private::adjustRecurrence(const KCalendarCore::Incidence::Ptr &originalIncidence, const KCalendarCore::Incidence::Ptr &incidence)
1205 {
1206  if (!originalIncidence || !incidence->recurs() || incidence->hasRecurrenceId() || !mAutoAdjustRecurrence
1207  || !incidence->dirtyFields().contains(KCalendarCore::Incidence::FieldDtStart)) {
1208  return;
1209  }
1210 
1211  const QDate originalDate = originalIncidence->dtStart().date();
1212  const QDate newStartDate = incidence->dtStart().date();
1213 
1214  if (!originalDate.isValid() || !newStartDate.isValid() || originalDate == newStartDate) {
1215  return;
1216  }
1217 
1218  KCalendarCore::Recurrence *recurrence = incidence->recurrence();
1219  switch (recurrence->recurrenceType()) {
1220  case KCalendarCore::Recurrence::rWeekly: {
1221  QBitArray days = recurrence->days();
1222  const int oldIndex = originalDate.dayOfWeek() - 1; // QDate returns [1-7];
1223  const int newIndex = newStartDate.dayOfWeek() - 1;
1224  if (oldIndex != newIndex) {
1225  days.clearBit(oldIndex);
1226  days.setBit(newIndex);
1227  recurrence->setWeekly(recurrence->frequency(), days);
1228  }
1229  }
1230  default:
1231  break; // Other types not implemented
1232  }
1233 
1234  // Now fix cases where dtstart would be bigger than the recurrence end rendering it impossible for a view to show it:
1235  // To retrieve the recurrence end don't trust Recurrence::endDt() since it returns dtStart if the rrule's end is < than dtstart,
1236  // it seems someone made Recurrence::endDt() more robust, but getNextOccurrences() still craps out. So lets fix it here
1237  // there's no reason to write bogus ical to disk.
1238  const QDate recurrenceEndDate = recurrence->defaultRRule() ? recurrence->defaultRRule()->endDt().date() : QDate();
1239  if (recurrenceEndDate.isValid() && recurrenceEndDate < newStartDate) {
1240  recurrence->setEndDate(newStartDate);
1241  }
1242 }
1243 
1244 void IncidenceChanger::Private::cancelTransaction()
1245 {
1246  if (mBatchOperationInProgress) {
1247  mAtomicOperations[mLatestAtomicOperationId]->setRolledback();
1248  }
1249 }
1250 
1251 void IncidenceChanger::Private::cleanupTransaction()
1252 {
1253  Q_ASSERT(mAtomicOperations.contains(mLatestAtomicOperationId));
1254  AtomicOperation *operation = mAtomicOperations[mLatestAtomicOperationId];
1255  Q_ASSERT(operation);
1256  Q_ASSERT(operation->rolledback());
1257  if (!operation->pendingJobs() && operation->m_endCalled && operation->m_transactionCompleted) {
1258  delete mAtomicOperations.take(mLatestAtomicOperationId);
1259  mBatchOperationInProgress = false;
1260  }
1261 }
1262 
1263 bool IncidenceChanger::Private::allowAtomicOperation(int atomicOperationId, const Change::Ptr &change) const
1264 {
1265  bool allow = true;
1266  if (atomicOperationId > 0) {
1267  Q_ASSERT(mAtomicOperations.contains(atomicOperationId));
1268  AtomicOperation *operation = mAtomicOperations.value(atomicOperationId);
1269 
1270  if (change->type == ChangeTypeCreate) {
1271  allow = true;
1272  } else if (change->type == ChangeTypeModify) {
1273  allow = !operation->m_itemIdsInOperation.contains(change->newItem.id());
1274  } else if (change->type == ChangeTypeDelete) {
1275  DeletionChange::Ptr deletion = change.staticCast<DeletionChange>();
1276  for (Akonadi::Item::Id id : qAsConst(deletion->mItemIds)) {
1277  if (operation->m_itemIdsInOperation.contains(id)) {
1278  allow = false;
1279  break;
1280  }
1281  }
1282  }
1283  }
1284 
1285  if (!allow) {
1286  qCWarning(AKONADICALENDAR_LOG) << "Each change belonging to a group operation"
1287  << "must have a different Akonadi::Item::Id";
1288  }
1289 
1290  return allow;
1291 }
1292 
1293 /**reimp*/
1294 void ModificationChange::emitCompletionSignal()
1295 {
1296  emitModifyFinished(changer, id, newItem, resultCode, errorString);
1297 }
1298 
1299 /**reimp*/
1300 void CreationChange::emitCompletionSignal()
1301 {
1302  // Does a queued emit, with QMetaObject::invokeMethod
1303  emitCreateFinished(changer, id, newItem, resultCode, errorString);
1304 }
1305 
1306 /**reimp*/
1307 void DeletionChange::emitCompletionSignal()
1308 {
1309  emitDeleteFinished(changer, id, mItemIds, resultCode, errorString);
1310 }
1311 
1312 /**
1313 Lost code from KDE 4.4 that was never called/used with incidenceeditors-ng.
1314 
1315  Attendees were removed from this incidence. Only the removed attendees
1316  are present in the incidence, so we just need to send a cancel messages
1317  to all attendees groupware messages are enabled at all.
1318 
1319 void IncidenceChanger::cancelAttendees( const Akonadi::Item &aitem )
1320 {
1321  const KCalendarCore::Incidence::Ptr incidence = CalendarSupport::incidence( aitem );
1322  Q_ASSERT( incidence );
1323  if ( KCalPrefs::instance()->mUseGroupwareCommunication ) {
1324  if ( KMessageBox::questionYesNo(
1325  0,
1326  i18n( "Some attendees were removed from the incidence. "
1327  "Shall cancel messages be sent to these attendees?" ),
1328  i18n( "Attendees Removed" ), KGuiItem( i18n( "Send Messages" ) ),
1329  KGuiItem( i18n( "Do Not Send" ) ) ) == KMessageBox::Yes ) {
1330  // don't use Akonadi::Groupware::sendICalMessage here, because that asks just
1331  // a very general question "Other people are involved, send message to
1332  // them?", which isn't helpful at all in this situation. Afterwards, it
1333  // would only call the Akonadi::MailScheduler::performTransaction, so do this
1334  // manually.
1335  CalendarSupport::MailScheduler scheduler(
1336  static_cast<CalendarSupport::Calendar*>(d->mCalendar) );
1337  scheduler.performTransaction( incidence, KCalendarCore::iTIPCancel );
1338  }
1339  }
1340 }
1341 
1342 */
1343 
1344 AtomicOperation::AtomicOperation(IncidenceChanger::Private *icp, uint ident)
1345  : m_id(ident)
1346  , m_endCalled(false)
1347  , m_numCompletedChanges(0)
1348  , m_transactionCompleted(false)
1349  , m_wasRolledback(false)
1350  , m_transaction(nullptr)
1351  , m_incidenceChangerPrivate(icp)
1352 {
1353  Q_ASSERT(m_id != 0);
1354 }
1355 
1356 Akonadi::TransactionSequence *AtomicOperation::transaction()
1357 {
1358  if (!m_transaction) {
1359  m_transaction = new Akonadi::TransactionSequence;
1360  m_transaction->setAutomaticCommittingEnabled(true);
1361 
1362  m_incidenceChangerPrivate->mAtomicOperationByTransaction.insert(m_transaction, m_id);
1363 
1364  QObject::connect(m_transaction, &KJob::result, m_incidenceChangerPrivate, &IncidenceChanger::Private::handleTransactionJobResult);
1365  }
1366 
1367  return m_transaction;
1368 }
bool isValid() const
bool isValid() const
void setWeekly(int freq, int weekStart=1)
void append(const T &value)
Collection parentCollection() const
Factory to create MailTransport::MessageQueueJob jobs or ITIPHandlerDialogDelegate objects...
Definition: itiphandler.h:182
void setBit(int i)
virtual QString errorString() const
void insert(int i, T &&value)
void setPayload(const T &p)
T payload() const
void setEndDate(const QDate &endDate)
void setAutomaticCommittingEnabled(bool enable)
Id id() const
int dayOfWeek() const const
void setMimeType(const QString &mimeType)
QString & insert(int position, QChar ch)
void clearBit(int i)
History class for implementing undo/redo of calendar operations.
Definition: history.h:44
int revision() const
QString i18nc(const char *context, const char *text, const TYPE &arg...)
Ask the user for a descision.
Definition: itiphandler.h:66
void setRemoteRevision(const QString &revision)
bool isValid() const const
Rights rights() const
KCALUTILS_EXPORT QString errorMessage(const KCalendarCore::Exception &exception)
bool invokeMethod(QObject *obj, const char *member, Qt::ConnectionType type, QGenericReturnArgument ret, QGenericArgument val0, QGenericArgument val1, QGenericArgument val2, QGenericArgument val3, QGenericArgument val4, QGenericArgument val5, QGenericArgument val6, QGenericArgument val7, QGenericArgument val8, QGenericArgument val9)
QString i18n(const char *text, const TYPE &arg...)
QBitArray days() const
bool isEmpty() const const
void setRevision(int revision)
FreeBusyManager::Singleton.
PartStat status() const
bool contains(const Key &key) const const
ushort recurrenceType() const
void result(KJob *job)
bool hasPayload() const
QueuedConnection
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QObject * parent() const const
QSharedPointer< X > staticCast() const const
QDateTime currentDateTimeUtc()
void sorry(QWidget *parent, const QString &text, const QString &caption=QString(), Options options=Notify)
int error() const
KIOFILEWIDGETS_EXPORT QStringList list(const QString &fileClass)
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Sat Jun 19 2021 23:12:24 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.