Akonadi Calendar

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

KDE's Doxygen guidelines are available online.