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));
388 Q_ASSERT(item.hasPayload<KCalendarCore::Incidence::Ptr>());
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)) {
484 Q_ASSERT(item.hasPayload<KCalendarCore::Incidence::Ptr>());
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 handler->deleteLater();
592 handler = nullptr;
593 Q_ASSERT(!change->originalItems.isEmpty());
594 for (const Akonadi::Item &item : std::as_const(change->originalItems)) {
595 Q_ASSERT(item.hasPayload());
597 Q_ASSERT(incidence);
598 if (!incidence->supportsGroupwareCommunication()) {
599 continue;
600 }
601
602 if (!Akonadi::CalendarUtils::thatIsMe(incidence->organizer().email())) {
603 const QStringList myEmails = Akonadi::CalendarUtils::allEmails();
604 bool notifyOrganizer = false;
605 const KCalendarCore::Attendee me(incidence->attendeeByMails(myEmails));
606 if (!me.isNull()) {
607 if (me.status() == KCalendarCore::Attendee::Accepted || me.status() == KCalendarCore::Attendee::Delegated) {
608 notifyOrganizer = true;
609 }
610 KCalendarCore::Attendee newMe(me);
611 newMe.setStatus(KCalendarCore::Attendee::Declined);
612 incidence->clearAttendees();
613 incidence->addAttendee(newMe);
614 // break;
615 }
616
617 if (notifyOrganizer) {
618 MailScheduler scheduler(mFactory, change->parentWidget); // TODO make async
619 scheduler.performTransaction(incidence, KCalendarCore::iTIPReply);
620 }
621 }
622 }
623 break;
624 case IncidenceChanger::ChangeTypeModify: {
625 if (change->originalItems.isEmpty()) {
626 break;
627 }
628
629 Q_ASSERT(change->originalItems.count() == 1);
630 Incidence::Ptr oldIncidence = CalendarUtils::incidence(change->originalItems.first());
631 Incidence::Ptr newIncidence = CalendarUtils::incidence(change->newItem);
632
633 if (!newIncidence->supportsGroupwareCommunication() || !Akonadi::CalendarUtils::thatIsMe(newIncidence->organizer().email())) {
634 // If we're not the organizer, the user already saw the "Do you really want to do this, incidence will become out of sync"
635 break;
636 }
637
638 if (allowedModificationsWithoutRevisionUpdate(newIncidence)) {
639 break;
640 }
641
642 if (!neverSend && !alwaysSend && mInvitationStatusByAtomicOperation.contains(change->atomicOperationId)) {
643 handler->setDefaultAction(actionFromStatus(mInvitationStatusByAtomicOperation.value(change->atomicOperationId)));
644 }
645
646 const bool attendeeStatusChanged = myAttendeeStatusChanged(newIncidence, oldIncidence, Akonadi::CalendarUtils::allEmails());
647
648 handler->sendIncidenceModifiedMessage(KCalendarCore::iTIPRequest, newIncidence, attendeeStatusChanged);
649 return;
650 }
651 default:
652 handler->deleteLater();
653 handler = nullptr;
654 Q_ASSERT(false);
655 change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultCanceled);
656 return;
657 }
658 handler->deleteLater();
659 handler = nullptr;
660 change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultSuccess);
661 } else {
662 change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultSuccess);
663 }
664}
665
666/** static */
667bool IncidenceChangerPrivate::myAttendeeStatusChanged(const Incidence::Ptr &newInc, const Incidence::Ptr &oldInc, const QStringList &myEmails)
668{
669 Q_ASSERT(newInc);
670 Q_ASSERT(oldInc);
671 const Attendee oldMe = oldInc->attendeeByMails(myEmails);
672 const Attendee newMe = newInc->attendeeByMails(myEmails);
673
674 return !oldMe.isNull() && !newMe.isNull() && oldMe.status() != newMe.status();
675}
676
677IncidenceChanger::IncidenceChanger(QObject *parent)
678 : QObject(parent)
679 , d(new IncidenceChangerPrivate(/**history=*/true, /*factory=*/nullptr, this))
680{
681}
682
683IncidenceChanger::IncidenceChanger(ITIPHandlerComponentFactory *factory, QObject *parent)
684 : QObject(parent)
685 , d(new IncidenceChangerPrivate(/**history=*/true, factory, this))
686{
687}
688
689IncidenceChanger::IncidenceChanger(bool enableHistory, QObject *parent)
690 : QObject(parent)
691 , d(new IncidenceChangerPrivate(enableHistory, /*factory=*/nullptr, this))
692{
693}
694
695IncidenceChanger::~IncidenceChanger() = default;
696
697int IncidenceChanger::createFromItem(const Item &item, const Collection &collection, QWidget *parent)
698{
699 const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0;
700
701 const Change::Ptr change(new CreationChange(this, ++d->mLatestChangeId, atomicOperationId, parent));
702 const int changeId = change->id;
703 Q_ASSERT(!(d->mBatchOperationInProgress && !d->mAtomicOperations.contains(atomicOperationId)));
704 if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) {
705 const QString errorMessage = d->showErrorDialog(ResultCodeRolledback, parent);
706 qCWarning(AKONADICALENDAR_LOG) << errorMessage;
707
708 change->resultCode = ResultCodeRolledback;
709 change->errorString = errorMessage;
710 d->cleanupTransaction();
711 return changeId;
712 }
713
714 change->newItem = item;
715
716 d->step1DetermineDestinationCollection(change, collection);
717
718 return change->id;
719}
720
721int IncidenceChanger::createIncidence(const Incidence::Ptr &incidence, const Collection &collection, QWidget *parent)
722{
723 if (!incidence) {
724 qCWarning(AKONADICALENDAR_LOG) << "An invalid payload is not allowed.";
725 d->cancelTransaction();
726 return -1;
727 }
728
729 Item item;
731 item.setMimeType(incidence->mimeType());
732
733 return createFromItem(item, collection, parent);
734}
735
736int IncidenceChanger::deleteIncidence(const Item &item, QWidget *parent)
737{
739 list.append(item);
740
741 return deleteIncidences(list, parent);
742}
743
744int IncidenceChanger::deleteIncidences(const Item::List &items, QWidget *parent)
745{
746 if (items.isEmpty()) {
747 qCritical() << "Delete what?";
748 d->cancelTransaction();
749 return -1;
750 }
751
752 for (const Item &item : items) {
753 if (!item.isValid()) {
754 qCritical() << "Items must be valid!";
755 d->cancelTransaction();
756 return -1;
757 }
758 }
759
760 const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0;
761 const int changeId = ++d->mLatestChangeId;
762 const Change::Ptr change(new DeletionChange(this, changeId, atomicOperationId, parent));
763
764 for (const Item &item : items) {
765 if (!d->hasRights(item.parentCollection(), ChangeTypeDelete)) {
766 qCWarning(AKONADICALENDAR_LOG) << "Item " << item.id() << " can't be deleted due to ACL restrictions";
767 const QString errorString = d->showErrorDialog(ResultCodePermissions, parent);
768 change->resultCode = ResultCodePermissions;
769 change->errorString = errorString;
770 d->cancelTransaction();
771 return changeId;
772 }
773 }
774
775 if (!d->allowAtomicOperation(atomicOperationId, change)) {
776 const QString errorString = d->showErrorDialog(ResultCodeDuplicateId, parent);
777 change->resultCode = ResultCodeDuplicateId;
778 change->errorString = errorString;
779 qCWarning(AKONADICALENDAR_LOG) << errorString;
780 d->cancelTransaction();
781 return changeId;
782 }
783
784 Item::List itemsToDelete;
785 for (const Item &item : items) {
786 if (d->deleteAlreadyCalled(item.id())) {
787 // IncidenceChanger::deleteIncidence() called twice, ignore this one.
788 qCDebug(AKONADICALENDAR_LOG) << "Item " << item.id() << " already deleted or being deleted, skipping";
789 } else {
790 itemsToDelete.append(item);
791 }
792 }
793
794 if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) {
795 const QString errorMessage = d->showErrorDialog(ResultCodeRolledback, parent);
796 change->resultCode = ResultCodeRolledback;
797 change->errorString = errorMessage;
798 qCritical() << errorMessage;
799 d->cleanupTransaction();
800 return changeId;
801 }
802
803 if (itemsToDelete.isEmpty()) {
804 QList<Akonadi::Item::Id> itemIdList;
805 itemIdList.append(Item().id());
806 qCDebug(AKONADICALENDAR_LOG) << "Items already deleted or being deleted, skipping";
807 const QString errorMessage = i18n("That calendar item was already deleted, or currently being deleted.");
808 // Queued emit because return must be executed first, otherwise caller won't know this workId
809 change->resultCode = ResultCodeAlreadyDeleted;
810 change->errorString = errorMessage;
811 d->cancelTransaction();
812 qCWarning(AKONADICALENDAR_LOG) << errorMessage;
813 return changeId;
814 }
815 change->originalItems = itemsToDelete;
816
817 d->mChangeById.insert(changeId, change);
818
819 if (d->mGroupwareCommunication) {
820 connect(change.data(), &Change::dialogClosedBeforeChange, d.get(), &IncidenceChangerPrivate::deleteIncidences2);
821 d->handleInvitationsBeforeChange(change);
822 } else {
823 d->deleteIncidences2(changeId, ITIPHandlerHelper::ResultSuccess);
824 }
825 return changeId;
826}
827
828void IncidenceChangerPrivate::deleteIncidences2(int changeId, ITIPHandlerHelper::SendResult status)
829{
830 Q_UNUSED(status)
831 Change::Ptr change = mChangeById[changeId];
832 const uint atomicOperationId = change->atomicOperationId;
833 auto deleteJob = new ItemDeleteJob(change->originalItems, parentJob(change));
834 mChangeForJob.insert(deleteJob, change);
835
836 if (mBatchOperationInProgress) {
837 AtomicOperation *atomic = mAtomicOperations[atomicOperationId];
838 Q_ASSERT(atomic);
839 atomic->addChange(change);
840 }
841
842 mDeletedItemIds.reserve(mDeletedItemIds.count() + change->originalItems.count());
843 for (const Item &item : std::as_const(change->originalItems)) {
844 mDeletedItemIds << item.id();
845 }
846
847 // Do some cleanup
848 if (mDeletedItemIds.count() > 100) {
849 mDeletedItemIds.remove(0, 50);
850 }
851
852 // QueuedConnection because of possible sync exec calls.
853 connect(deleteJob, &KJob::result, this, &IncidenceChangerPrivate::handleDeleteJobResult, Qt::QueuedConnection);
854}
855
856int IncidenceChanger::modifyIncidence(const Item &changedItem, const KCalendarCore::Incidence::Ptr &originalPayload, QWidget *parent)
857{
858 if (!changedItem.isValid() || !changedItem.hasPayload<Incidence::Ptr>()) {
859 qCWarning(AKONADICALENDAR_LOG) << "An invalid item or payload is not allowed.";
860 d->cancelTransaction();
861 return -1;
862 }
863
864 if (!d->hasRights(changedItem.parentCollection(), ChangeTypeModify)) {
865 qCWarning(AKONADICALENDAR_LOG) << "Item " << changedItem.id() << " can't be deleted due to ACL restrictions";
866 const int changeId = ++d->mLatestChangeId;
867 const QString errorString = d->showErrorDialog(ResultCodePermissions, parent);
868 emitModifyFinished(this, changeId, changedItem, ResultCodePermissions, errorString);
869 d->cancelTransaction();
870 return changeId;
871 }
872
873 // TODO also update revision here instead of in the editor
874 changedItem.payload<Incidence::Ptr>()->setLastModified(QDateTime::currentDateTimeUtc());
875
876 const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0;
877 const int changeId = ++d->mLatestChangeId;
878 auto modificationChange = new ModificationChange(this, changeId, atomicOperationId, parent);
879 Change::Ptr change(modificationChange);
880
881 if (originalPayload) {
882 Item originalItem(changedItem);
883 originalItem.setPayload<KCalendarCore::Incidence::Ptr>(originalPayload);
884 modificationChange->originalItems << originalItem;
885 }
886
887 modificationChange->newItem = changedItem;
888 d->mChangeById.insert(changeId, change);
889
890 if (!d->allowAtomicOperation(atomicOperationId, change)) {
891 const QString errorString = d->showErrorDialog(ResultCodeDuplicateId, parent);
892
893 change->resultCode = ResultCodeDuplicateId;
894 change->errorString = errorString;
895 d->cancelTransaction();
896 qCWarning(AKONADICALENDAR_LOG) << "Atomic operation now allowed";
897 return changeId;
898 }
899
900 if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) {
901 const QString errorMessage = d->showErrorDialog(ResultCodeRolledback, parent);
902 qCritical() << errorMessage;
903 d->cleanupTransaction();
904 emitModifyFinished(this, changeId, changedItem, ResultCodeRolledback, errorMessage);
905 } else {
906 d->adjustRecurrence(originalPayload, CalendarUtils::incidence(modificationChange->newItem));
907 d->performModification(change);
908 }
909
910 return changeId;
911}
912
913void IncidenceChangerPrivate::performModification(const Change::Ptr &change)
914{
915 const Item::Id id = change->newItem.id();
916 Akonadi::Item &newItem = change->newItem;
917 Q_ASSERT(newItem.isValid());
918 Q_ASSERT(newItem.hasPayload<Incidence::Ptr>());
919
920 const int changeId = change->id;
921
922 if (deleteAlreadyCalled(id)) {
923 // IncidenceChanger::deleteIncidence() called twice, ignore this one.
924 qCDebug(AKONADICALENDAR_LOG) << "Item " << id << " already deleted or being deleted, skipping";
925
926 // Queued emit because return must be executed first, otherwise caller won't know this workId
927 emitModifyFinished(q,
928 change->id,
929 newItem,
930 IncidenceChanger::ResultCodeAlreadyDeleted,
931 i18n("That calendar item was already deleted, or currently being deleted."));
932 return;
933 }
934
935 const uint atomicOperationId = change->atomicOperationId;
936 const bool hasAtomicOperationId = atomicOperationId != 0;
937 if (hasAtomicOperationId && mAtomicOperations[atomicOperationId]->rolledback()) {
938 const QString errorMessage = showErrorDialog(IncidenceChanger::ResultCodeRolledback, nullptr);
939 qCritical() << errorMessage;
940 emitModifyFinished(q, changeId, newItem, IncidenceChanger::ResultCodeRolledback, errorMessage);
941 return;
942 }
943 if (mGroupwareCommunication) {
944 connect(change.data(), &Change::dialogClosedBeforeChange, this, &IncidenceChangerPrivate::performModification2);
945 handleInvitationsBeforeChange(change);
946 } else {
947 performModification2(change->id, ITIPHandlerHelper::ResultSuccess);
948 }
949}
950
951void IncidenceChangerPrivate::performModification2(int changeId, ITIPHandlerHelper::SendResult status)
952{
953 Change::Ptr change = mChangeById[changeId];
954 const Item::Id id = change->newItem.id();
955 Akonadi::Item &newItem = change->newItem;
956 Q_ASSERT(newItem.isValid());
957 Q_ASSERT(newItem.hasPayload<Incidence::Ptr>());
958 if (status == ITIPHandlerHelper::ResultCanceled) { // TODO:fireout what is right here:)
959 // User got a "You're not the organizer, do you really want to send" dialog, and said "no"
960 qCDebug(AKONADICALENDAR_LOG) << "User cancelled, giving up";
961 emitModifyFinished(q, change->id, newItem, IncidenceChanger::ResultCodeUserCanceled, QString());
962 return;
963 }
964
965 const uint atomicOperationId = change->atomicOperationId;
966 const bool hasAtomicOperationId = atomicOperationId != 0;
967
968 QHash<Akonadi::Item::Id, int> &latestRevisionByItemId = *(s_latestRevisionByItemId());
969 if (latestRevisionByItemId.contains(id) && latestRevisionByItemId[id] > newItem.revision()) {
970 /* When a ItemModifyJob ends, the application can still modify the old items if the user
971 * is quick because the ETM wasn't updated yet, and we'll get a STORE error, because
972 * we are not modifying the latest revision.
973 *
974 * When a job ends, we keep the new revision in s_latestRevisionByItemId
975 * so we can update the item's revision
976 */
977 newItem.setRevision(latestRevisionByItemId[id]);
978 }
979
981 {
982 if (!allowedModificationsWithoutRevisionUpdate(incidence)) { // increment revision ( KCalendarCore revision, not akonadi )
983 const int revision = incidence->revision();
984 incidence->setRevision(revision + 1);
985 }
986
987 // Reset attendee status, when resceduling
988 QSet<IncidenceBase::Field> resetPartStatus;
991 if (!(incidence->dirtyFields() & resetPartStatus).isEmpty() && weAreOrganizer(incidence)) {
992 auto attendees = incidence->attendees();
993 for (auto &attendee : attendees) {
994 if (attendee.role() != Attendee::NonParticipant && attendee.status() != Attendee::Delegated && !Akonadi::CalendarUtils::thatIsMe(attendee)) {
995 attendee.setStatus(Attendee::NeedsAction);
996 attendee.setRSVP(true);
997 }
998 }
999 incidence->setAttendees(attendees);
1000 }
1001 }
1002
1003 // Dav Fix
1004 // Don't write back remote revision since we can't make sure it is the current one
1005 newItem.setRemoteRevision(QString());
1006
1007 if (mModificationsInProgress.contains(newItem.id())) {
1008 // There's already a ItemModifyJob running for this item ID
1009 // Let's wait for it to end.
1010 queueModification(change);
1011 } else {
1012 auto modifyJob = new ItemModifyJob(newItem, parentJob(change));
1013 mChangeForJob.insert(modifyJob, change);
1014 mDirtyFieldsByJob.insert(modifyJob, incidence->dirtyFields());
1015
1016 if (hasAtomicOperationId) {
1017 AtomicOperation *atomic = mAtomicOperations[atomicOperationId];
1018 Q_ASSERT(atomic);
1019 atomic->addChange(change);
1020 }
1021
1022 mModificationsInProgress[newItem.id()] = change;
1023 // QueuedConnection because of possible sync exec calls.
1024 connect(modifyJob, &KJob::result, this, &IncidenceChangerPrivate::handleModifyJobResult, Qt::QueuedConnection);
1025 }
1026}
1027
1028void IncidenceChanger::startAtomicOperation(const QString &operationDescription)
1029{
1030 if (d->mBatchOperationInProgress) {
1031 qCDebug(AKONADICALENDAR_LOG) << "An atomic operation is already in progress.";
1032 return;
1033 }
1034
1035 ++d->mLatestAtomicOperationId;
1036 d->mBatchOperationInProgress = true;
1037
1038 auto atomicOperation = new AtomicOperation(d.get(), d->mLatestAtomicOperationId);
1039 atomicOperation->m_description = operationDescription;
1040 d->mAtomicOperations.insert(d->mLatestAtomicOperationId, atomicOperation);
1041}
1042
1043void IncidenceChanger::endAtomicOperation()
1044{
1045 if (!d->mBatchOperationInProgress) {
1046 qCDebug(AKONADICALENDAR_LOG) << "No atomic operation is in progress.";
1047 return;
1048 }
1049
1050 Q_ASSERT_X(d->mLatestAtomicOperationId != 0, "IncidenceChanger::endAtomicOperation()", "Call startAtomicOperation() first.");
1051
1052 Q_ASSERT(d->mAtomicOperations.contains(d->mLatestAtomicOperationId));
1053 AtomicOperation *atomicOperation = d->mAtomicOperations[d->mLatestAtomicOperationId];
1054 Q_ASSERT(atomicOperation);
1055 atomicOperation->m_endCalled = true;
1056
1057 const bool allJobsCompleted = !atomicOperation->pendingJobs();
1058
1059 if (allJobsCompleted && atomicOperation->rolledback() && atomicOperation->m_transactionCompleted) {
1060 // The transaction job already completed, we can cleanup:
1061 delete d->mAtomicOperations.take(d->mLatestAtomicOperationId);
1062 d->mBatchOperationInProgress = false;
1063 } /* else if ( allJobsCompleted ) {
1064 Q_ASSERT( atomicOperation->transaction );
1065 atomicOperation->transaction->commit(); we using autocommit now
1066 }*/
1067}
1068
1069void IncidenceChanger::setShowDialogsOnError(bool enable)
1070{
1071 d->mShowDialogsOnError = enable;
1072 if (d->mHistory) {
1073 d->mHistory->incidenceChanger()->setShowDialogsOnError(enable);
1074 }
1075}
1076
1077bool IncidenceChanger::showDialogsOnError() const
1078{
1079 return d->mShowDialogsOnError;
1080}
1081
1082void IncidenceChanger::setRespectsCollectionRights(bool respects)
1083{
1084 d->mRespectsCollectionRights = respects;
1085}
1086
1087bool IncidenceChanger::respectsCollectionRights() const
1088{
1089 return d->mRespectsCollectionRights;
1090}
1091
1092void IncidenceChanger::setDestinationPolicy(IncidenceChanger::DestinationPolicy destinationPolicy)
1093{
1094 d->mDestinationPolicy = destinationPolicy;
1095}
1096
1097IncidenceChanger::DestinationPolicy IncidenceChanger::destinationPolicy() const
1098{
1099 return d->mDestinationPolicy;
1100}
1101
1102void IncidenceChanger::setEntityTreeModel(Akonadi::EntityTreeModel *entityTreeModel)
1103{
1104 d->mEntityTreeModel = entityTreeModel;
1105}
1106
1107Akonadi::EntityTreeModel *IncidenceChanger::entityTreeModel() const
1108{
1109 return d->mEntityTreeModel;
1110}
1111
1112void IncidenceChanger::setDefaultCollection(const Akonadi::Collection &collection)
1113{
1114 d->mDefaultCollection = collection;
1115}
1116
1117Collection IncidenceChanger::defaultCollection() const
1118{
1119 return d->mDefaultCollection;
1120}
1121
1122bool IncidenceChanger::historyEnabled() const
1123{
1124 return d->mUseHistory;
1125}
1126
1127void IncidenceChanger::setHistoryEnabled(bool enable)
1128{
1129 if (d->mUseHistory != enable) {
1130 d->mUseHistory = enable;
1131 if (enable && !d->mHistory) {
1132 d->mHistory = new History(d.get());
1133 }
1134 }
1135}
1136
1137History *IncidenceChanger::history() const
1138{
1139 return d->mHistory;
1140}
1141
1142bool IncidenceChanger::deletedRecently(Akonadi::Item::Id id) const
1143{
1144 return d->deleteAlreadyCalled(id);
1145}
1146
1147void IncidenceChanger::setGroupwareCommunication(bool enabled)
1148{
1149 d->mGroupwareCommunication = enabled;
1150}
1151
1152bool IncidenceChanger::groupwareCommunication() const
1153{
1154 return d->mGroupwareCommunication;
1155}
1156
1157void IncidenceChanger::setAutoAdjustRecurrence(bool enable)
1158{
1159 d->mAutoAdjustRecurrence = enable;
1160}
1161
1162bool IncidenceChanger::autoAdjustRecurrence() const
1163{
1164 return d->mAutoAdjustRecurrence;
1165}
1166
1167void IncidenceChanger::setInvitationPolicy(IncidenceChanger::InvitationPolicy policy)
1168{
1169 d->m_invitationPolicy = policy;
1170}
1171
1172IncidenceChanger::InvitationPolicy IncidenceChanger::invitationPolicy() const
1173{
1174 return d->m_invitationPolicy;
1175}
1176
1177Akonadi::Collection IncidenceChanger::lastCollectionUsed() const
1178{
1179 return d->mLastCollectionUsed;
1180}
1181
1182void IncidenceChanger::setInvitationPrivacy(IncidenceChanger::InvitationPrivacyFlags invitationPrivacy)
1183{
1184 d->m_invitationPrivacy = invitationPrivacy;
1185}
1186
1187IncidenceChanger::InvitationPrivacyFlags IncidenceChanger::invitationPrivacy() const
1188{
1189 return d->m_invitationPrivacy;
1190}
1191
1192QString IncidenceChangerPrivate::showErrorDialog(IncidenceChanger::ResultCode resultCode, QWidget *parent)
1193{
1194 QString errorString;
1195 switch (resultCode) {
1196 case IncidenceChanger::ResultCodePermissions:
1197 errorString = i18n("Operation can not be performed due to ACL restrictions");
1198 break;
1199 case IncidenceChanger::ResultCodeInvalidUserCollection:
1200 errorString = i18n("The chosen collection is invalid");
1201 break;
1202 case IncidenceChanger::ResultCodeInvalidDefaultCollection:
1203 errorString = i18n(
1204 "Default collection is invalid or doesn't have proper ACLs"
1205 " and DestinationPolicyNeverAsk was used");
1206 break;
1207 case IncidenceChanger::ResultCodeDuplicateId:
1208 errorString = i18n("Duplicate item id in a group operation");
1209 break;
1210 case IncidenceChanger::ResultCodeRolledback:
1211 errorString = i18n(
1212 "One change belonging to a group of changes failed. "
1213 "All changes are being rolled back.");
1214 break;
1215 default:
1216 Q_ASSERT(false);
1217 return QString(i18n("Unknown error"));
1218 }
1219
1220 if (mShowDialogsOnError) {
1221 KMessageBox::error(parent, errorString);
1222 }
1223
1224 return errorString;
1225}
1226
1227void IncidenceChangerPrivate::adjustRecurrence(const KCalendarCore::Incidence::Ptr &originalIncidence, const KCalendarCore::Incidence::Ptr &incidence)
1228{
1229 if (!originalIncidence || !incidence->recurs() || incidence->hasRecurrenceId() || !mAutoAdjustRecurrence
1230 || !incidence->dirtyFields().contains(KCalendarCore::Incidence::FieldDtStart)) {
1231 return;
1232 }
1233
1234 const QDate originalDate = originalIncidence->dtStart().date();
1235 const QDate newStartDate = incidence->dtStart().date();
1236
1237 if (!originalDate.isValid() || !newStartDate.isValid() || originalDate == newStartDate) {
1238 return;
1239 }
1240
1241 KCalendarCore::Recurrence *recurrence = incidence->recurrence();
1242 switch (recurrence->recurrenceType()) {
1243 case KCalendarCore::Recurrence::rWeekly: {
1244 QBitArray days = recurrence->days();
1245 const int oldIndex = originalDate.dayOfWeek() - 1; // QDate returns [1-7];
1246 const int newIndex = newStartDate.dayOfWeek() - 1;
1247 if (oldIndex != newIndex) {
1248 days.clearBit(oldIndex);
1249 days.setBit(newIndex);
1250 recurrence->setWeekly(recurrence->frequency(), days);
1251 }
1252 }
1253 default:
1254 break; // Other types not implemented
1255 }
1256
1257 // Now fix cases where dtstart would be bigger than the recurrence end rendering it impossible for a view to show it:
1258 // To retrieve the recurrence end don't trust Recurrence::endDt() since it returns dtStart if the rrule's end is < than dtstart,
1259 // it seems someone made Recurrence::endDt() more robust, but getNextOccurrences() still craps out. So lets fix it here
1260 // there's no reason to write bogus ical to disk.
1261 const QDate recurrenceEndDate = recurrence->defaultRRule() ? recurrence->defaultRRule()->endDt().date() : QDate();
1262 if (recurrenceEndDate.isValid() && recurrenceEndDate < newStartDate) {
1263 recurrence->setEndDate(newStartDate);
1264 }
1265}
1266
1267void IncidenceChangerPrivate::cancelTransaction()
1268{
1269 if (mBatchOperationInProgress) {
1270 mAtomicOperations[mLatestAtomicOperationId]->setRolledback();
1271 }
1272}
1273
1274void IncidenceChangerPrivate::cleanupTransaction()
1275{
1276 Q_ASSERT(mAtomicOperations.contains(mLatestAtomicOperationId));
1277 AtomicOperation *operation = mAtomicOperations[mLatestAtomicOperationId];
1278 Q_ASSERT(operation);
1279 Q_ASSERT(operation->rolledback());
1280 if (!operation->pendingJobs() && operation->m_endCalled && operation->m_transactionCompleted) {
1281 delete mAtomicOperations.take(mLatestAtomicOperationId);
1282 mBatchOperationInProgress = false;
1283 }
1284}
1285
1286bool IncidenceChangerPrivate::allowAtomicOperation(int atomicOperationId, const Change::Ptr &change) const
1287{
1288 bool allow = true;
1289 if (atomicOperationId > 0) {
1290 Q_ASSERT(mAtomicOperations.contains(atomicOperationId));
1291 AtomicOperation *operation = mAtomicOperations.value(atomicOperationId);
1292
1293 if (change->type == IncidenceChanger::ChangeTypeCreate) {
1294 allow = true;
1295 } else if (change->type == IncidenceChanger::ChangeTypeModify) {
1296 allow = !operation->m_itemIdsInOperation.contains(change->newItem.id());
1297 } else if (change->type == IncidenceChanger::ChangeTypeDelete) {
1298 DeletionChange::Ptr deletion = change.staticCast<DeletionChange>();
1299 for (Akonadi::Item::Id id : std::as_const(deletion->mItemIds)) {
1300 if (operation->m_itemIdsInOperation.contains(id)) {
1301 allow = false;
1302 break;
1303 }
1304 }
1305 }
1306 }
1307
1308 if (!allow) {
1309 qCWarning(AKONADICALENDAR_LOG) << "Each change belonging to a group operation"
1310 << "must have a different Akonadi::Item::Id";
1311 }
1312
1313 return allow;
1314}
1315
1316/**reimp*/
1317void ModificationChange::emitCompletionSignal()
1318{
1319 emitModifyFinished(changer, id, newItem, resultCode, errorString);
1320}
1321
1322/**reimp*/
1323void CreationChange::emitCompletionSignal()
1324{
1325 // Does a queued emit, with QMetaObject::invokeMethod
1326 emitCreateFinished(changer, id, newItem, resultCode, errorString);
1327}
1328
1329/**reimp*/
1330void DeletionChange::emitCompletionSignal()
1331{
1332 emitDeleteFinished(changer, id, mItemIds, resultCode, errorString);
1333}
1334
1335/**
1336Lost code from KDE 4.4 that was never called/used with incidenceeditors-ng.
1337
1338 Attendees were removed from this incidence. Only the removed attendees
1339 are present in the incidence, so we just need to send a cancel messages
1340 to all attendees groupware messages are enabled at all.
1341
1342void IncidenceChanger::cancelAttendees( const Akonadi::Item &aitem )
1343{
1344 const KCalendarCore::Incidence::Ptr incidence = CalendarSupport::incidence( aitem );
1345 Q_ASSERT( incidence );
1346 if ( KCalPrefs::instance()->mUseGroupwareCommunication ) {
1347 if ( KMessageBox::questionYesNo(
1348 0,
1349 i18n( "Some attendees were removed from the incidence. "
1350 "Shall cancel messages be sent to these attendees?" ),
1351 i18nc("@title:window", "Attendees Removed" ), KGuiItem( i18n( "Send Messages" ) ),
1352 KGuiItem( i18n( "Do Not Send" ) ) ) == KMessageBox::ButtonCode::PrimaryAction) {
1353 // don't use Akonadi::Groupware::sendICalMessage here, because that asks just
1354 // a very general question "Other people are involved, send message to
1355 // them?", which isn't helpful at all in this situation. Afterwards, it
1356 // would only call the Akonadi::MailScheduler::performTransaction, so do this
1357 // manually.
1358 CalendarSupport::MailScheduler scheduler(
1359 static_cast<CalendarSupport::Calendar*>(d->mCalendar) );
1360 scheduler.performTransaction( incidence, KCalendarCore::iTIPCancel );
1361 }
1362 }
1363}
1364
1365*/
1366
1367AtomicOperation::AtomicOperation(IncidenceChangerPrivate *icp, uint ident)
1368 : m_id(ident)
1369 , m_endCalled(false)
1370 , m_numCompletedChanges(0)
1371 , m_transactionCompleted(false)
1372 , m_wasRolledback(false)
1373 , m_transaction(nullptr)
1374 , m_incidenceChangerPrivate(icp)
1375{
1376 Q_ASSERT(m_id != 0);
1377}
1378
1379Akonadi::TransactionSequence *AtomicOperation::transaction()
1380{
1381 if (!m_transaction) {
1382 m_transaction = new Akonadi::TransactionSequence;
1383 m_transaction->setAutomaticCommittingEnabled(true);
1384
1385 m_incidenceChangerPrivate->mAtomicOperationByTransaction.insert(m_transaction, m_id);
1386
1387 QObject::connect(m_transaction, &KJob::result, m_incidenceChangerPrivate, &IncidenceChangerPrivate::handleTransactionJobResult);
1388 }
1389
1390 return m_transaction;
1391}
1392
1393#include "moc_incidencechanger.cpp"
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 setRevision(int revision)
bool hasPayload() const
Id id() const
int revision() 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 Tue Mar 26 2024 11:17:16 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.