File indexing completed on 2024-05-19 05:11:18
0001 /* 0002 SPDX-FileCopyrightText: 2004 Reinhold Kainhofer <reinhold@kainhofer.com> 0003 SPDX-FileCopyrightText: 2010-2012 Sérgio Martins <iamsergio@gmail.com> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 #include "incidencechanger.h" 0008 #include "akonadicalendar_debug.h" 0009 #include "calendarutils.h" 0010 #include "incidencechanger_p.h" 0011 #include "mailscheduler_p.h" 0012 #include "utils_p.h" 0013 #include <Akonadi/ItemCreateJob> 0014 #include <Akonadi/ItemDeleteJob> 0015 #include <Akonadi/ItemModifyJob> 0016 #include <Akonadi/TransactionSequence> 0017 0018 #include <KGuiItem> 0019 #include <KJob> 0020 #include <KLocalizedString> 0021 #include <KMessageBox> 0022 0023 #include <QBitArray> 0024 0025 using namespace Akonadi; 0026 using namespace KCalendarCore; 0027 0028 AKONADI_CALENDAR_TESTS_EXPORT bool akonadi_calendar_running_unittests = false; 0029 0030 static ITIPHandlerDialogDelegate::Action actionFromStatus(ITIPHandlerHelper::SendResult result) 0031 { 0032 // enum SendResult { 0033 // Canceled, /**< Sending was canceled by the user, meaning there are 0034 // local changes of which other attendees are not aware. */ 0035 // FailKeepUpdate, /**< Sending failed, the changes to the incidence must be kept. */ 0036 // FailAbortUpdate, /**< Sending failed, the changes to the incidence must be undone. */ 0037 // NoSendingNeeded, /**< In some cases it is not needed to send an invitation 0038 // (e.g. when we are the only attendee) */ 0039 // Success 0040 switch (result) { 0041 case ITIPHandlerHelper::ResultCanceled: 0042 return ITIPHandlerDialogDelegate::ActionDontSendMessage; 0043 case ITIPHandlerHelper::ResultSuccess: 0044 return ITIPHandlerDialogDelegate::ActionSendMessage; 0045 default: 0046 return ITIPHandlerDialogDelegate::ActionAsk; 0047 } 0048 } 0049 0050 static bool weAreOrganizer(const Incidence::Ptr &incidence) 0051 { 0052 const QString email = incidence->organizer().email(); 0053 return Akonadi::CalendarUtils::thatIsMe(email); 0054 } 0055 0056 static bool allowedModificationsWithoutRevisionUpdate(const Incidence::Ptr &incidence) 0057 { 0058 // Modifications that are per user allowed without getting outofsync with organisator 0059 // * if only alarm settings are modified. 0060 const QSet<KCalendarCore::IncidenceBase::Field> dirtyFields = incidence->dirtyFields(); 0061 QSet<KCalendarCore::IncidenceBase::Field> alarmOnlyModify; 0062 alarmOnlyModify << IncidenceBase::FieldAlarms << IncidenceBase::FieldLastModified; 0063 return dirtyFields == alarmOnlyModify; 0064 } 0065 0066 static void updateHandlerPrivacyPolicy(ITIPHandlerHelper *helper, IncidenceChanger::InvitationPrivacyFlags flags) 0067 { 0068 ITIPHandlerHelper::MessagePrivacyFlags helperFlags; 0069 helperFlags.setFlag(ITIPHandlerHelper::MessagePrivacySign, (flags & IncidenceChanger::InvitationPrivacySign) == IncidenceChanger::InvitationPrivacySign); 0070 helperFlags.setFlag(ITIPHandlerHelper::MessagePrivacyEncrypt, 0071 (flags & IncidenceChanger::InvitationPrivacyEncrypt) == IncidenceChanger::InvitationPrivacyEncrypt); 0072 helper->setMessagePrivacy(helperFlags); 0073 } 0074 0075 namespace Akonadi 0076 { 0077 // Does a queued emit, with QMetaObject::invokeMethod 0078 static void emitCreateFinished(IncidenceChanger *changer, 0079 int changeId, 0080 const Akonadi::Item &item, 0081 Akonadi::IncidenceChanger::ResultCode resultCode, 0082 const QString &errorString) 0083 { 0084 QMetaObject::invokeMethod(changer, 0085 "createFinished", 0086 Qt::QueuedConnection, 0087 Q_ARG(int, changeId), 0088 Q_ARG(Akonadi::Item, item), 0089 Q_ARG(Akonadi::IncidenceChanger::ResultCode, resultCode), 0090 Q_ARG(QString, errorString)); 0091 } 0092 0093 // Does a queued emit, with QMetaObject::invokeMethod 0094 static void 0095 emitModifyFinished(IncidenceChanger *changer, int changeId, const Akonadi::Item &item, IncidenceChanger::ResultCode resultCode, const QString &errorString) 0096 { 0097 QMetaObject::invokeMethod(changer, 0098 "modifyFinished", 0099 Qt::QueuedConnection, 0100 Q_ARG(int, changeId), 0101 Q_ARG(Akonadi::Item, item), 0102 Q_ARG(Akonadi::IncidenceChanger::ResultCode, resultCode), 0103 Q_ARG(QString, errorString)); 0104 } 0105 0106 // Does a queued emit, with QMetaObject::invokeMethod 0107 static void emitDeleteFinished(IncidenceChanger *changer, 0108 int changeId, 0109 const QList<Akonadi::Item::Id> &itemIdList, 0110 IncidenceChanger::ResultCode resultCode, 0111 const QString &errorString) 0112 { 0113 QMetaObject::invokeMethod(changer, 0114 "deleteFinished", 0115 Qt::QueuedConnection, 0116 Q_ARG(int, changeId), 0117 Q_ARG(QList<Akonadi::Item::Id>, itemIdList), 0118 Q_ARG(Akonadi::IncidenceChanger::ResultCode, resultCode), 0119 Q_ARG(QString, errorString)); 0120 } 0121 } 0122 0123 using IdToRevisionHash = QHash<Akonadi::Item::Id, int>; 0124 Q_GLOBAL_STATIC(IdToRevisionHash, s_latestRevisionByItemId) 0125 0126 IncidenceChangerPrivate::IncidenceChangerPrivate(bool enableHistory, ITIPHandlerComponentFactory *factory, IncidenceChanger *qq) 0127 : q(qq) 0128 { 0129 mLatestChangeId = 0; 0130 mShowDialogsOnError = true; 0131 mFactory = factory ? factory : new ITIPHandlerComponentFactory(this); 0132 mHistory = enableHistory ? new History(this) : nullptr; 0133 mUseHistory = enableHistory; 0134 mDestinationPolicy = IncidenceChanger::DestinationPolicyDefault; 0135 mRespectsCollectionRights = false; 0136 mGroupwareCommunication = false; 0137 mLatestAtomicOperationId = 0; 0138 mBatchOperationInProgress = false; 0139 mAutoAdjustRecurrence = true; 0140 m_collectionFetchJob = nullptr; 0141 m_invitationPolicy = IncidenceChanger::InvitationPolicyAsk; 0142 0143 qRegisterMetaType<QList<Akonadi::Item::Id>>("QList<Akonadi::Item::Id>"); 0144 qRegisterMetaType<Akonadi::Item::Id>("Akonadi::Item::Id"); 0145 qRegisterMetaType<Akonadi::Item>("Akonadi::Item"); 0146 qRegisterMetaType<Akonadi::IncidenceChanger::ResultCode>("Akonadi::IncidenceChanger::ResultCode"); 0147 qRegisterMetaType<ITIPHandlerHelper::SendResult>("ITIPHandlerHelper::SendResult"); 0148 } 0149 0150 IncidenceChangerPrivate::~IncidenceChangerPrivate() 0151 { 0152 if (!mAtomicOperations.isEmpty() || !mQueuedModifications.isEmpty() || !mModificationsInProgress.isEmpty()) { 0153 qCDebug(AKONADICALENDAR_LOG) << "Normal if the application was being used. " 0154 "But might indicate a memory leak if it wasn't"; 0155 } 0156 } 0157 0158 bool IncidenceChangerPrivate::atomicOperationIsValid(uint atomicOperationId) const 0159 { 0160 // Changes must be done between startAtomicOperation() and endAtomicOperation() 0161 return mAtomicOperations.contains(atomicOperationId) && !mAtomicOperations[atomicOperationId]->m_endCalled; 0162 } 0163 0164 bool IncidenceChangerPrivate::hasRights(const Collection &collection, IncidenceChanger::ChangeType changeType) const 0165 { 0166 bool result = false; 0167 switch (changeType) { 0168 case IncidenceChanger::ChangeTypeCreate: 0169 result = collection.rights() & Akonadi::Collection::CanCreateItem; 0170 break; 0171 case IncidenceChanger::ChangeTypeModify: 0172 result = collection.rights() & Akonadi::Collection::CanChangeItem; 0173 break; 0174 case IncidenceChanger::ChangeTypeDelete: 0175 result = collection.rights() & Akonadi::Collection::CanDeleteItem; 0176 break; 0177 default: 0178 Q_ASSERT_X(false, "hasRights", "invalid type"); 0179 } 0180 0181 return !collection.isValid() || !mRespectsCollectionRights || result; 0182 } 0183 0184 Akonadi::Job *IncidenceChangerPrivate::parentJob(const Change::Ptr &change) const 0185 { 0186 return (mBatchOperationInProgress && !change->queuedModification) ? mAtomicOperations[mLatestAtomicOperationId]->transaction() : nullptr; 0187 } 0188 0189 void IncidenceChangerPrivate::queueModification(const Change::Ptr &change) 0190 { 0191 // If there's already a change queued we just discard it 0192 // and send the newer change, which already includes 0193 // previous modifications 0194 const Akonadi::Item::Id id = change->newItem.id(); 0195 if (mQueuedModifications.contains(id)) { 0196 Change::Ptr toBeDiscarded = mQueuedModifications.take(id); 0197 toBeDiscarded->resultCode = IncidenceChanger::ResultCodeModificationDiscarded; 0198 toBeDiscarded->completed = true; 0199 mChangeById.remove(toBeDiscarded->id); 0200 } 0201 0202 change->queuedModification = true; 0203 mQueuedModifications[id] = change; 0204 } 0205 0206 void IncidenceChangerPrivate::performNextModification(Akonadi::Item::Id id) 0207 { 0208 mModificationsInProgress.remove(id); 0209 0210 if (mQueuedModifications.contains(id)) { 0211 const Change::Ptr change = mQueuedModifications.take(id); 0212 performModification(change); 0213 } 0214 } 0215 0216 void IncidenceChangerPrivate::handleTransactionJobResult(KJob *job) 0217 { 0218 auto transaction = qobject_cast<TransactionSequence *>(job); 0219 Q_ASSERT(transaction); 0220 Q_ASSERT(mAtomicOperationByTransaction.contains(transaction)); 0221 0222 const uint atomicOperationId = mAtomicOperationByTransaction.take(transaction); 0223 0224 Q_ASSERT(mAtomicOperations.contains(atomicOperationId)); 0225 AtomicOperation *operation = mAtomicOperations[atomicOperationId]; 0226 Q_ASSERT(operation); 0227 Q_ASSERT(operation->m_id == atomicOperationId); 0228 if (job->error()) { 0229 if (!operation->rolledback()) { 0230 operation->setRolledback(); 0231 } 0232 qCritical() << "Transaction failed, everything was rolledback. " << job->errorString(); 0233 } else { 0234 Q_ASSERT(operation->m_endCalled); 0235 Q_ASSERT(!operation->pendingJobs()); 0236 } 0237 0238 if (!operation->pendingJobs() && operation->m_endCalled) { 0239 delete mAtomicOperations.take(atomicOperationId); 0240 mBatchOperationInProgress = false; 0241 } else { 0242 operation->m_transactionCompleted = true; 0243 } 0244 } 0245 0246 void IncidenceChangerPrivate::handleCreateJobResult(KJob *job) 0247 { 0248 Change::Ptr change = mChangeForJob.take(job); 0249 0250 const auto j = qobject_cast<const ItemCreateJob *>(job); 0251 Q_ASSERT(j); 0252 Akonadi::Item item = j->item(); 0253 0254 if (j->error()) { 0255 const QString errorString = j->errorString(); 0256 IncidenceChanger::ResultCode resultCode = IncidenceChanger::ResultCodeJobError; 0257 item = change->newItem; 0258 qCritical() << errorString; 0259 if (mShowDialogsOnError) { 0260 KMessageBox::error(change->parentWidget, i18n("Error while trying to create calendar item. Error was: %1", errorString)); 0261 } 0262 mChangeById.remove(change->id); 0263 change->errorString = errorString; 0264 change->resultCode = resultCode; 0265 // puff, change finally goes out of scope, and emits the incidenceCreated signal. 0266 } else { 0267 Q_ASSERT(item.isValid()); 0268 Q_ASSERT(item.hasPayload<KCalendarCore::Incidence::Ptr>()); 0269 change->newItem = item; 0270 0271 if (change->useGroupwareCommunication) { 0272 connect(change.data(), &Change::dialogClosedAfterChange, this, &IncidenceChangerPrivate::handleCreateJobResult2); 0273 handleInvitationsAfterChange(change); 0274 } else { 0275 handleCreateJobResult2(change->id, ITIPHandlerHelper::ResultSuccess); 0276 } 0277 } 0278 } 0279 0280 void IncidenceChangerPrivate::handleCreateJobResult2(int changeId, ITIPHandlerHelper::SendResult status) 0281 { 0282 Change::Ptr change = mChangeById[changeId]; 0283 Akonadi::Item item = change->newItem; 0284 0285 mChangeById.remove(changeId); 0286 0287 if (status == ITIPHandlerHelper::ResultFailAbortUpdate) { 0288 qCritical() << "Sending invitations failed, but did not delete the incidence"; 0289 } 0290 0291 const uint atomicOperationId = change->atomicOperationId; 0292 if (atomicOperationId != 0) { 0293 mInvitationStatusByAtomicOperation.insert(atomicOperationId, status); 0294 } 0295 0296 QString description; 0297 if (change->atomicOperationId != 0) { 0298 AtomicOperation *a = mAtomicOperations[change->atomicOperationId]; 0299 ++a->m_numCompletedChanges; 0300 change->completed = true; 0301 description = a->m_description; 0302 } 0303 0304 // for user undo/redo 0305 if (change->recordToHistory) { 0306 mHistory->recordCreation(item, description, change->atomicOperationId); 0307 } 0308 0309 change->errorString = QString(); 0310 change->resultCode = IncidenceChanger::ResultCodeSuccess; 0311 // puff, change finally goes out of scope, and emits the incidenceCreated signal. 0312 } 0313 0314 void IncidenceChangerPrivate::handleDeleteJobResult(KJob *job) 0315 { 0316 Change::Ptr change = mChangeForJob.take(job); 0317 0318 const auto j = qobject_cast<const ItemDeleteJob *>(job); 0319 const Item::List items = j->deletedItems(); 0320 0321 QSharedPointer<DeletionChange> deletionChange = change.staticCast<DeletionChange>(); 0322 0323 deletionChange->mItemIds.reserve(deletionChange->mItemIds.count() + items.count()); 0324 for (const Akonadi::Item &item : items) { 0325 deletionChange->mItemIds.append(item.id()); 0326 } 0327 QString description; 0328 if (change->atomicOperationId != 0) { 0329 AtomicOperation *a = mAtomicOperations[change->atomicOperationId]; 0330 a->m_numCompletedChanges++; 0331 change->completed = true; 0332 description = a->m_description; 0333 } 0334 if (j->error()) { 0335 const QString errorString = j->errorString(); 0336 qCritical() << errorString; 0337 0338 if (mShowDialogsOnError) { 0339 KMessageBox::error(change->parentWidget, i18n("Error while trying to delete calendar item. Error was: %1", errorString)); 0340 } 0341 0342 for (const Item &item : items) { 0343 // Weren't deleted due to error 0344 mDeletedItemIds.remove(mDeletedItemIds.indexOf(item.id())); 0345 } 0346 mChangeById.remove(change->id); 0347 change->resultCode = IncidenceChanger::ResultCodeJobError; 0348 change->errorString = errorString; 0349 change->emitCompletionSignal(); 0350 } else { // success 0351 if (change->recordToHistory) { 0352 Q_ASSERT(mHistory); 0353 mHistory->recordDeletions(items, description, change->atomicOperationId); 0354 } 0355 0356 if (change->useGroupwareCommunication) { 0357 connect(change.data(), &Change::dialogClosedAfterChange, this, &IncidenceChangerPrivate::handleDeleteJobResult2); 0358 handleInvitationsAfterChange(change); 0359 } else { 0360 handleDeleteJobResult2(change->id, ITIPHandlerHelper::ResultSuccess); 0361 } 0362 } 0363 } 0364 0365 void IncidenceChangerPrivate::handleDeleteJobResult2(int changeId, ITIPHandlerHelper::SendResult status) 0366 { 0367 Change::Ptr change = mChangeById[changeId]; 0368 mChangeById.remove(change->id); 0369 0370 if (status == ITIPHandlerHelper::ResultSuccess) { 0371 change->errorString = QString(); 0372 change->resultCode = IncidenceChanger::ResultCodeSuccess; 0373 } else { 0374 change->errorString = i18nc("errormessage for a job ended with an unexpected result", "An unknown error occurred"); 0375 change->resultCode = IncidenceChanger::ResultCodeJobError; 0376 } 0377 0378 // puff, change finally goes out of scope, and emits the incidenceDeleted signal. 0379 } 0380 0381 void IncidenceChangerPrivate::handleModifyJobResult(KJob *job) 0382 { 0383 Change::Ptr change = mChangeForJob.take(job); 0384 0385 const auto j = qobject_cast<const ItemModifyJob *>(job); 0386 const Item item = j->item(); 0387 Q_ASSERT(mDirtyFieldsByJob.contains(job)); 0388 Q_ASSERT(item.hasPayload<KCalendarCore::Incidence::Ptr>()); 0389 const QSet<KCalendarCore::IncidenceBase::Field> dirtyFields = mDirtyFieldsByJob.value(job); 0390 item.payload<KCalendarCore::Incidence::Ptr>()->setDirtyFields(dirtyFields); 0391 QString description; 0392 if (change->atomicOperationId != 0) { 0393 AtomicOperation *a = mAtomicOperations[change->atomicOperationId]; 0394 a->m_numCompletedChanges++; 0395 change->completed = true; 0396 description = a->m_description; 0397 } 0398 if (j->error()) { 0399 const QString errorString = j->errorString(); 0400 IncidenceChanger::ResultCode resultCode = IncidenceChanger::ResultCodeJobError; 0401 if (deleteAlreadyCalled(item.id())) { 0402 // User deleted the item almost at the same time he changed it. We could just return success 0403 // but the delete is probably already recorded to History, and that would make undo not work 0404 // in the proper order. 0405 resultCode = IncidenceChanger::ResultCodeAlreadyDeleted; 0406 qCWarning(AKONADICALENDAR_LOG) << "Trying to change item " << item.id() << " while deletion is in progress."; 0407 } else { 0408 qCritical() << errorString; 0409 } 0410 if (mShowDialogsOnError) { 0411 KMessageBox::error(change->parentWidget, i18n("Error while trying to modify calendar item. Error was: %1", errorString)); 0412 } 0413 mChangeById.remove(change->id); 0414 change->errorString = errorString; 0415 change->resultCode = resultCode; 0416 // puff, change finally goes out of scope, and emits the incidenceModified signal. 0417 0418 QMetaObject::invokeMethod(this, "performNextModification", Qt::QueuedConnection, Q_ARG(Akonadi::Item::Id, item.id())); 0419 } else { // success 0420 (*(s_latestRevisionByItemId()))[item.id()] = item.revision(); 0421 change->newItem = item; 0422 if (change->recordToHistory && !change->originalItems.isEmpty()) { 0423 Q_ASSERT(change->originalItems.count() == 1); 0424 mHistory->recordModification(change->originalItems.constFirst(), item, description, change->atomicOperationId); 0425 } 0426 0427 if (change->useGroupwareCommunication) { 0428 connect(change.data(), &Change::dialogClosedAfterChange, this, &IncidenceChangerPrivate::handleModifyJobResult2); 0429 handleInvitationsAfterChange(change); 0430 } else { 0431 handleModifyJobResult2(change->id, ITIPHandlerHelper::ResultSuccess); 0432 } 0433 } 0434 } 0435 0436 void IncidenceChangerPrivate::handleModifyJobResult2(int changeId, ITIPHandlerHelper::SendResult status) 0437 { 0438 Change::Ptr change = mChangeById[changeId]; 0439 0440 mChangeById.remove(changeId); 0441 if (change->atomicOperationId != 0) { 0442 mInvitationStatusByAtomicOperation.insert(change->atomicOperationId, status); 0443 } 0444 change->errorString = QString(); 0445 change->resultCode = IncidenceChanger::ResultCodeSuccess; 0446 // puff, change finally goes out of scope, and emits the incidenceModified signal. 0447 0448 QMetaObject::invokeMethod(this, "performNextModification", Qt::QueuedConnection, Q_ARG(Akonadi::Item::Id, change->newItem.id())); 0449 } 0450 0451 bool IncidenceChangerPrivate::deleteAlreadyCalled(Akonadi::Item::Id id) const 0452 { 0453 return mDeletedItemIds.contains(id); 0454 } 0455 0456 void IncidenceChangerPrivate::handleInvitationsBeforeChange(const Change::Ptr &change) 0457 { 0458 if (mGroupwareCommunication) { 0459 ITIPHandlerHelper::SendResult result = ITIPHandlerHelper::ResultSuccess; 0460 switch (change->type) { 0461 case IncidenceChanger::ChangeTypeCreate: 0462 // nothing needs to be done 0463 break; 0464 case IncidenceChanger::ChangeTypeDelete: { 0465 ITIPHandlerHelper::SendResult status; 0466 bool sendOk = true; 0467 Q_ASSERT(!change->originalItems.isEmpty()); 0468 0469 auto handler = new ITIPHandlerHelper(mFactory, change->parentWidget); 0470 handler->setParent(this); 0471 updateHandlerPrivacyPolicy(handler, m_invitationPrivacy); 0472 0473 if (m_invitationPolicy == IncidenceChanger::InvitationPolicySend) { 0474 handler->setDefaultAction(ITIPHandlerDialogDelegate::ActionSendMessage); 0475 } else if (m_invitationPolicy == IncidenceChanger::InvitationPolicyDontSend) { 0476 handler->setDefaultAction(ITIPHandlerDialogDelegate::ActionDontSendMessage); 0477 } else if (mInvitationStatusByAtomicOperation.contains(change->atomicOperationId)) { 0478 handler->setDefaultAction(actionFromStatus(mInvitationStatusByAtomicOperation.value(change->atomicOperationId))); 0479 } 0480 0481 connect(handler, &ITIPHandlerHelper::finished, change.data(), &Change::emitUserDialogClosedBeforeChange); 0482 0483 for (const Akonadi::Item &item : std::as_const(change->originalItems)) { 0484 Q_ASSERT(item.hasPayload<KCalendarCore::Incidence::Ptr>()); 0485 Incidence::Ptr incidence = CalendarUtils::incidence(item); 0486 if (!incidence->supportsGroupwareCommunication()) { 0487 continue; 0488 } 0489 // We only send CANCEL if we're the organizer. 0490 // If we're not, then we send REPLY with PartStat=Declined in handleInvitationsAfterChange() 0491 if (Akonadi::CalendarUtils::thatIsMe(incidence->organizer().email())) { 0492 // TODO: not to popup all delete message dialogs at once :( 0493 sendOk = false; 0494 handler->sendIncidenceDeletedMessage(KCalendarCore::iTIPCancel, incidence); 0495 if (change->atomicOperationId) { 0496 mInvitationStatusByAtomicOperation.insert(change->atomicOperationId, status); 0497 } 0498 // TODO: with some status we want to break immediately 0499 } 0500 } 0501 0502 if (sendOk) { 0503 change->emitUserDialogClosedBeforeChange(result); 0504 } 0505 return; 0506 } 0507 case IncidenceChanger::ChangeTypeModify: { 0508 if (change->originalItems.isEmpty()) { 0509 break; 0510 } 0511 0512 Q_ASSERT(change->originalItems.count() == 1); 0513 Incidence::Ptr oldIncidence = CalendarUtils::incidence(change->originalItems.first()); 0514 Incidence::Ptr newIncidence = CalendarUtils::incidence(change->newItem); 0515 0516 if (!oldIncidence->supportsGroupwareCommunication()) { 0517 break; 0518 } 0519 0520 if (allowedModificationsWithoutRevisionUpdate(newIncidence)) { 0521 change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultSuccess); 0522 return; 0523 } 0524 0525 if (akonadi_calendar_running_unittests && !weAreOrganizer(newIncidence)) { 0526 // This is a bit of a workaround when running tests. I don't want to show the 0527 // "You're not organizer, do you want to modify event?" dialog in unit-tests, but want 0528 // to emulate a "yes" and a "no" press. 0529 if (m_invitationPolicy == IncidenceChanger::InvitationPolicySend) { 0530 change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultSuccess); 0531 return; 0532 } else if (m_invitationPolicy == IncidenceChanger::InvitationPolicyDontSend) { 0533 change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultCanceled); 0534 return; 0535 } 0536 } 0537 0538 ITIPHandlerHelper handler(mFactory, change->parentWidget); 0539 const bool modify = handler.handleIncidenceAboutToBeModified(newIncidence); 0540 if (modify) { 0541 break; 0542 } else { 0543 result = ITIPHandlerHelper::ResultCanceled; 0544 } 0545 0546 if (newIncidence->type() == oldIncidence->type()) { 0547 IncidenceBase *i1 = newIncidence.data(); 0548 IncidenceBase *i2 = oldIncidence.data(); 0549 *i1 = *i2; 0550 } 0551 break; 0552 } 0553 default: 0554 Q_ASSERT(false); 0555 result = ITIPHandlerHelper::ResultCanceled; 0556 } 0557 change->emitUserDialogClosedBeforeChange(result); 0558 } else { 0559 change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultSuccess); 0560 } 0561 } 0562 0563 void IncidenceChangerPrivate::handleInvitationsAfterChange(const Change::Ptr &change) 0564 { 0565 if (change->useGroupwareCommunication) { 0566 auto handler = new ITIPHandlerHelper(mFactory, change->parentWidget); 0567 connect(handler, &ITIPHandlerHelper::finished, change.data(), &Change::emitUserDialogClosedAfterChange); 0568 handler->setParent(this); 0569 updateHandlerPrivacyPolicy(handler, m_invitationPrivacy); 0570 0571 const bool alwaysSend = (m_invitationPolicy == IncidenceChanger::InvitationPolicySend); 0572 const bool neverSend = (m_invitationPolicy == IncidenceChanger::InvitationPolicyDontSend); 0573 if (alwaysSend) { 0574 handler->setDefaultAction(ITIPHandlerDialogDelegate::ActionSendMessage); 0575 } 0576 0577 if (neverSend) { 0578 handler->setDefaultAction(ITIPHandlerDialogDelegate::ActionDontSendMessage); 0579 } 0580 0581 switch (change->type) { 0582 case IncidenceChanger::ChangeTypeCreate: { 0583 Incidence::Ptr incidence = CalendarUtils::incidence(change->newItem); 0584 if (incidence->supportsGroupwareCommunication()) { 0585 handler->sendIncidenceCreatedMessage(KCalendarCore::iTIPRequest, incidence); 0586 return; 0587 } 0588 break; 0589 } 0590 case IncidenceChanger::ChangeTypeDelete: 0591 handler->deleteLater(); 0592 handler = nullptr; 0593 Q_ASSERT(!change->originalItems.isEmpty()); 0594 for (const Akonadi::Item &item : std::as_const(change->originalItems)) { 0595 Q_ASSERT(item.hasPayload()); 0596 Incidence::Ptr incidence = CalendarUtils::incidence(item); 0597 Q_ASSERT(incidence); 0598 if (!incidence->supportsGroupwareCommunication()) { 0599 continue; 0600 } 0601 0602 if (!Akonadi::CalendarUtils::thatIsMe(incidence->organizer().email())) { 0603 const QStringList myEmails = Akonadi::CalendarUtils::allEmails(); 0604 bool notifyOrganizer = false; 0605 const KCalendarCore::Attendee me(incidence->attendeeByMails(myEmails)); 0606 if (!me.isNull()) { 0607 if (me.status() == KCalendarCore::Attendee::Accepted || me.status() == KCalendarCore::Attendee::Delegated) { 0608 notifyOrganizer = true; 0609 } 0610 KCalendarCore::Attendee newMe(me); 0611 newMe.setStatus(KCalendarCore::Attendee::Declined); 0612 incidence->clearAttendees(); 0613 incidence->addAttendee(newMe); 0614 // break; 0615 } 0616 0617 if (notifyOrganizer) { 0618 MailScheduler scheduler(mFactory, change->parentWidget); // TODO make async 0619 scheduler.performTransaction(incidence, KCalendarCore::iTIPReply); 0620 } 0621 } 0622 } 0623 break; 0624 case IncidenceChanger::ChangeTypeModify: { 0625 if (change->originalItems.isEmpty()) { 0626 break; 0627 } 0628 0629 Q_ASSERT(change->originalItems.count() == 1); 0630 Incidence::Ptr oldIncidence = CalendarUtils::incidence(change->originalItems.first()); 0631 Incidence::Ptr newIncidence = CalendarUtils::incidence(change->newItem); 0632 0633 if (!newIncidence->supportsGroupwareCommunication() || !Akonadi::CalendarUtils::thatIsMe(newIncidence->organizer().email())) { 0634 // If we're not the organizer, the user already saw the "Do you really want to do this, incidence will become out of sync" 0635 break; 0636 } 0637 0638 if (allowedModificationsWithoutRevisionUpdate(newIncidence)) { 0639 break; 0640 } 0641 0642 if (!neverSend && !alwaysSend && mInvitationStatusByAtomicOperation.contains(change->atomicOperationId)) { 0643 handler->setDefaultAction(actionFromStatus(mInvitationStatusByAtomicOperation.value(change->atomicOperationId))); 0644 } 0645 0646 const bool attendeeStatusChanged = myAttendeeStatusChanged(newIncidence, oldIncidence, Akonadi::CalendarUtils::allEmails()); 0647 0648 handler->sendIncidenceModifiedMessage(KCalendarCore::iTIPRequest, newIncidence, attendeeStatusChanged); 0649 return; 0650 } 0651 default: 0652 handler->deleteLater(); 0653 handler = nullptr; 0654 Q_ASSERT(false); 0655 change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultCanceled); 0656 return; 0657 } 0658 handler->deleteLater(); 0659 handler = nullptr; 0660 change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultSuccess); 0661 } else { 0662 change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultSuccess); 0663 } 0664 } 0665 0666 /** static */ 0667 bool IncidenceChangerPrivate::myAttendeeStatusChanged(const Incidence::Ptr &newInc, const Incidence::Ptr &oldInc, const QStringList &myEmails) 0668 { 0669 Q_ASSERT(newInc); 0670 Q_ASSERT(oldInc); 0671 const Attendee oldMe = oldInc->attendeeByMails(myEmails); 0672 const Attendee newMe = newInc->attendeeByMails(myEmails); 0673 0674 return !oldMe.isNull() && !newMe.isNull() && oldMe.status() != newMe.status(); 0675 } 0676 0677 IncidenceChanger::IncidenceChanger(QObject *parent) 0678 : QObject(parent) 0679 , d(new IncidenceChangerPrivate(/**history=*/true, /*factory=*/nullptr, this)) 0680 { 0681 } 0682 0683 IncidenceChanger::IncidenceChanger(ITIPHandlerComponentFactory *factory, QObject *parent) 0684 : QObject(parent) 0685 , d(new IncidenceChangerPrivate(/**history=*/true, factory, this)) 0686 { 0687 } 0688 0689 IncidenceChanger::IncidenceChanger(bool enableHistory, QObject *parent) 0690 : QObject(parent) 0691 , d(new IncidenceChangerPrivate(enableHistory, /*factory=*/nullptr, this)) 0692 { 0693 } 0694 0695 IncidenceChanger::~IncidenceChanger() = default; 0696 0697 int IncidenceChanger::createFromItem(const Item &item, const Collection &collection, QWidget *parent) 0698 { 0699 const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0; 0700 0701 const Change::Ptr change(new CreationChange(this, ++d->mLatestChangeId, atomicOperationId, parent)); 0702 const int changeId = change->id; 0703 Q_ASSERT(!(d->mBatchOperationInProgress && !d->mAtomicOperations.contains(atomicOperationId))); 0704 if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) { 0705 const QString errorMessage = d->showErrorDialog(ResultCodeRolledback, parent); 0706 qCWarning(AKONADICALENDAR_LOG) << errorMessage; 0707 0708 change->resultCode = ResultCodeRolledback; 0709 change->errorString = errorMessage; 0710 d->cleanupTransaction(); 0711 return changeId; 0712 } 0713 0714 change->newItem = item; 0715 0716 d->step1DetermineDestinationCollection(change, collection); 0717 0718 return change->id; 0719 } 0720 0721 int IncidenceChanger::createIncidence(const Incidence::Ptr &incidence, const Collection &collection, QWidget *parent) 0722 { 0723 if (!incidence) { 0724 qCWarning(AKONADICALENDAR_LOG) << "An invalid payload is not allowed."; 0725 d->cancelTransaction(); 0726 return -1; 0727 } 0728 0729 Item item; 0730 item.setPayload<KCalendarCore::Incidence::Ptr>(incidence); 0731 item.setMimeType(incidence->mimeType()); 0732 0733 return createFromItem(item, collection, parent); 0734 } 0735 0736 int IncidenceChanger::deleteIncidence(const Item &item, QWidget *parent) 0737 { 0738 Item::List list; 0739 list.append(item); 0740 0741 return deleteIncidences(list, parent); 0742 } 0743 0744 int IncidenceChanger::deleteIncidences(const Item::List &items, QWidget *parent) 0745 { 0746 if (items.isEmpty()) { 0747 qCritical() << "Delete what?"; 0748 d->cancelTransaction(); 0749 return -1; 0750 } 0751 0752 for (const Item &item : items) { 0753 if (!item.isValid()) { 0754 qCritical() << "Items must be valid!"; 0755 d->cancelTransaction(); 0756 return -1; 0757 } 0758 } 0759 0760 const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0; 0761 const int changeId = ++d->mLatestChangeId; 0762 const Change::Ptr change(new DeletionChange(this, changeId, atomicOperationId, parent)); 0763 0764 for (const Item &item : items) { 0765 if (!d->hasRights(item.parentCollection(), ChangeTypeDelete)) { 0766 qCWarning(AKONADICALENDAR_LOG) << "Item " << item.id() << " can't be deleted due to ACL restrictions"; 0767 const QString errorString = d->showErrorDialog(ResultCodePermissions, parent); 0768 change->resultCode = ResultCodePermissions; 0769 change->errorString = errorString; 0770 d->cancelTransaction(); 0771 return changeId; 0772 } 0773 } 0774 0775 if (!d->allowAtomicOperation(atomicOperationId, change)) { 0776 const QString errorString = d->showErrorDialog(ResultCodeDuplicateId, parent); 0777 change->resultCode = ResultCodeDuplicateId; 0778 change->errorString = errorString; 0779 qCWarning(AKONADICALENDAR_LOG) << errorString; 0780 d->cancelTransaction(); 0781 return changeId; 0782 } 0783 0784 Item::List itemsToDelete; 0785 for (const Item &item : items) { 0786 if (d->deleteAlreadyCalled(item.id())) { 0787 // IncidenceChanger::deleteIncidence() called twice, ignore this one. 0788 qCDebug(AKONADICALENDAR_LOG) << "Item " << item.id() << " already deleted or being deleted, skipping"; 0789 } else { 0790 itemsToDelete.append(item); 0791 } 0792 } 0793 0794 if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) { 0795 const QString errorMessage = d->showErrorDialog(ResultCodeRolledback, parent); 0796 change->resultCode = ResultCodeRolledback; 0797 change->errorString = errorMessage; 0798 qCritical() << errorMessage; 0799 d->cleanupTransaction(); 0800 return changeId; 0801 } 0802 0803 if (itemsToDelete.isEmpty()) { 0804 QList<Akonadi::Item::Id> itemIdList; 0805 itemIdList.append(Item().id()); 0806 qCDebug(AKONADICALENDAR_LOG) << "Items already deleted or being deleted, skipping"; 0807 const QString errorMessage = i18n("That calendar item was already deleted, or currently being deleted."); 0808 // Queued emit because return must be executed first, otherwise caller won't know this workId 0809 change->resultCode = ResultCodeAlreadyDeleted; 0810 change->errorString = errorMessage; 0811 d->cancelTransaction(); 0812 qCWarning(AKONADICALENDAR_LOG) << errorMessage; 0813 return changeId; 0814 } 0815 change->originalItems = itemsToDelete; 0816 0817 d->mChangeById.insert(changeId, change); 0818 0819 if (d->mGroupwareCommunication) { 0820 connect(change.data(), &Change::dialogClosedBeforeChange, d.get(), &IncidenceChangerPrivate::deleteIncidences2); 0821 d->handleInvitationsBeforeChange(change); 0822 } else { 0823 d->deleteIncidences2(changeId, ITIPHandlerHelper::ResultSuccess); 0824 } 0825 return changeId; 0826 } 0827 0828 void IncidenceChangerPrivate::deleteIncidences2(int changeId, ITIPHandlerHelper::SendResult status) 0829 { 0830 Q_UNUSED(status) 0831 Change::Ptr change = mChangeById[changeId]; 0832 const uint atomicOperationId = change->atomicOperationId; 0833 auto deleteJob = new ItemDeleteJob(change->originalItems, parentJob(change)); 0834 mChangeForJob.insert(deleteJob, change); 0835 0836 if (mBatchOperationInProgress) { 0837 AtomicOperation *atomic = mAtomicOperations[atomicOperationId]; 0838 Q_ASSERT(atomic); 0839 atomic->addChange(change); 0840 } 0841 0842 mDeletedItemIds.reserve(mDeletedItemIds.count() + change->originalItems.count()); 0843 for (const Item &item : std::as_const(change->originalItems)) { 0844 mDeletedItemIds << item.id(); 0845 } 0846 0847 // Do some cleanup 0848 if (mDeletedItemIds.count() > 100) { 0849 mDeletedItemIds.remove(0, 50); 0850 } 0851 0852 // QueuedConnection because of possible sync exec calls. 0853 connect(deleteJob, &KJob::result, this, &IncidenceChangerPrivate::handleDeleteJobResult, Qt::QueuedConnection); 0854 } 0855 0856 int IncidenceChanger::modifyIncidence(const Item &changedItem, const KCalendarCore::Incidence::Ptr &originalPayload, QWidget *parent) 0857 { 0858 if (!changedItem.isValid() || !changedItem.hasPayload<Incidence::Ptr>()) { 0859 qCWarning(AKONADICALENDAR_LOG) << "An invalid item or payload is not allowed."; 0860 d->cancelTransaction(); 0861 return -1; 0862 } 0863 0864 if (!d->hasRights(changedItem.parentCollection(), ChangeTypeModify)) { 0865 qCWarning(AKONADICALENDAR_LOG) << "Item " << changedItem.id() << " can't be deleted due to ACL restrictions"; 0866 const int changeId = ++d->mLatestChangeId; 0867 const QString errorString = d->showErrorDialog(ResultCodePermissions, parent); 0868 emitModifyFinished(this, changeId, changedItem, ResultCodePermissions, errorString); 0869 d->cancelTransaction(); 0870 return changeId; 0871 } 0872 0873 // TODO also update revision here instead of in the editor 0874 changedItem.payload<Incidence::Ptr>()->setLastModified(QDateTime::currentDateTimeUtc()); 0875 0876 const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0; 0877 const int changeId = ++d->mLatestChangeId; 0878 auto modificationChange = new ModificationChange(this, changeId, atomicOperationId, parent); 0879 Change::Ptr change(modificationChange); 0880 0881 if (originalPayload) { 0882 Item originalItem(changedItem); 0883 originalItem.setPayload<KCalendarCore::Incidence::Ptr>(originalPayload); 0884 modificationChange->originalItems << originalItem; 0885 } 0886 0887 modificationChange->newItem = changedItem; 0888 d->mChangeById.insert(changeId, change); 0889 0890 if (!d->allowAtomicOperation(atomicOperationId, change)) { 0891 const QString errorString = d->showErrorDialog(ResultCodeDuplicateId, parent); 0892 0893 change->resultCode = ResultCodeDuplicateId; 0894 change->errorString = errorString; 0895 d->cancelTransaction(); 0896 qCWarning(AKONADICALENDAR_LOG) << "Atomic operation now allowed"; 0897 return changeId; 0898 } 0899 0900 if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) { 0901 const QString errorMessage = d->showErrorDialog(ResultCodeRolledback, parent); 0902 qCritical() << errorMessage; 0903 d->cleanupTransaction(); 0904 emitModifyFinished(this, changeId, changedItem, ResultCodeRolledback, errorMessage); 0905 } else { 0906 d->adjustRecurrence(originalPayload, CalendarUtils::incidence(modificationChange->newItem)); 0907 d->performModification(change); 0908 } 0909 0910 return changeId; 0911 } 0912 0913 void IncidenceChangerPrivate::performModification(const Change::Ptr &change) 0914 { 0915 const Item::Id id = change->newItem.id(); 0916 Akonadi::Item &newItem = change->newItem; 0917 Q_ASSERT(newItem.isValid()); 0918 Q_ASSERT(newItem.hasPayload<Incidence::Ptr>()); 0919 0920 const int changeId = change->id; 0921 0922 if (deleteAlreadyCalled(id)) { 0923 // IncidenceChanger::deleteIncidence() called twice, ignore this one. 0924 qCDebug(AKONADICALENDAR_LOG) << "Item " << id << " already deleted or being deleted, skipping"; 0925 0926 // Queued emit because return must be executed first, otherwise caller won't know this workId 0927 emitModifyFinished(q, 0928 change->id, 0929 newItem, 0930 IncidenceChanger::ResultCodeAlreadyDeleted, 0931 i18n("That calendar item was already deleted, or currently being deleted.")); 0932 return; 0933 } 0934 0935 const uint atomicOperationId = change->atomicOperationId; 0936 const bool hasAtomicOperationId = atomicOperationId != 0; 0937 if (hasAtomicOperationId && mAtomicOperations[atomicOperationId]->rolledback()) { 0938 const QString errorMessage = showErrorDialog(IncidenceChanger::ResultCodeRolledback, nullptr); 0939 qCritical() << errorMessage; 0940 emitModifyFinished(q, changeId, newItem, IncidenceChanger::ResultCodeRolledback, errorMessage); 0941 return; 0942 } 0943 if (mGroupwareCommunication) { 0944 connect(change.data(), &Change::dialogClosedBeforeChange, this, &IncidenceChangerPrivate::performModification2); 0945 handleInvitationsBeforeChange(change); 0946 } else { 0947 performModification2(change->id, ITIPHandlerHelper::ResultSuccess); 0948 } 0949 } 0950 0951 void IncidenceChangerPrivate::performModification2(int changeId, ITIPHandlerHelper::SendResult status) 0952 { 0953 Change::Ptr change = mChangeById[changeId]; 0954 const Item::Id id = change->newItem.id(); 0955 Akonadi::Item &newItem = change->newItem; 0956 Q_ASSERT(newItem.isValid()); 0957 Q_ASSERT(newItem.hasPayload<Incidence::Ptr>()); 0958 if (status == ITIPHandlerHelper::ResultCanceled) { // TODO:fireout what is right here:) 0959 // User got a "You're not the organizer, do you really want to send" dialog, and said "no" 0960 qCDebug(AKONADICALENDAR_LOG) << "User cancelled, giving up"; 0961 emitModifyFinished(q, change->id, newItem, IncidenceChanger::ResultCodeUserCanceled, QString()); 0962 return; 0963 } 0964 0965 const uint atomicOperationId = change->atomicOperationId; 0966 const bool hasAtomicOperationId = atomicOperationId != 0; 0967 0968 QHash<Akonadi::Item::Id, int> &latestRevisionByItemId = *(s_latestRevisionByItemId()); 0969 if (latestRevisionByItemId.contains(id) && latestRevisionByItemId[id] > newItem.revision()) { 0970 /* When a ItemModifyJob ends, the application can still modify the old items if the user 0971 * is quick because the ETM wasn't updated yet, and we'll get a STORE error, because 0972 * we are not modifying the latest revision. 0973 * 0974 * When a job ends, we keep the new revision in s_latestRevisionByItemId 0975 * so we can update the item's revision 0976 */ 0977 newItem.setRevision(latestRevisionByItemId[id]); 0978 } 0979 0980 Incidence::Ptr incidence = CalendarUtils::incidence(newItem); 0981 { 0982 if (!allowedModificationsWithoutRevisionUpdate(incidence)) { // increment revision ( KCalendarCore revision, not akonadi ) 0983 const int revision = incidence->revision(); 0984 incidence->setRevision(revision + 1); 0985 } 0986 0987 // Reset attendee status, when resceduling 0988 QSet<IncidenceBase::Field> resetPartStatus; 0989 resetPartStatus << IncidenceBase::FieldDtStart << IncidenceBase::FieldDtEnd << IncidenceBase::FieldDtStart << IncidenceBase::FieldLocation 0990 << IncidenceBase::FieldDtDue << IncidenceBase::FieldDuration << IncidenceBase::FieldRecurrence; 0991 if (!(incidence->dirtyFields() & resetPartStatus).isEmpty() && weAreOrganizer(incidence)) { 0992 auto attendees = incidence->attendees(); 0993 for (auto &attendee : attendees) { 0994 if (attendee.role() != Attendee::NonParticipant && attendee.status() != Attendee::Delegated && !Akonadi::CalendarUtils::thatIsMe(attendee)) { 0995 attendee.setStatus(Attendee::NeedsAction); 0996 attendee.setRSVP(true); 0997 } 0998 } 0999 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 1028 void 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 1043 void 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 1069 void IncidenceChanger::setShowDialogsOnError(bool enable) 1070 { 1071 d->mShowDialogsOnError = enable; 1072 if (d->mHistory) { 1073 d->mHistory->incidenceChanger()->setShowDialogsOnError(enable); 1074 } 1075 } 1076 1077 bool IncidenceChanger::showDialogsOnError() const 1078 { 1079 return d->mShowDialogsOnError; 1080 } 1081 1082 void IncidenceChanger::setRespectsCollectionRights(bool respects) 1083 { 1084 d->mRespectsCollectionRights = respects; 1085 } 1086 1087 bool IncidenceChanger::respectsCollectionRights() const 1088 { 1089 return d->mRespectsCollectionRights; 1090 } 1091 1092 void IncidenceChanger::setDestinationPolicy(IncidenceChanger::DestinationPolicy destinationPolicy) 1093 { 1094 d->mDestinationPolicy = destinationPolicy; 1095 } 1096 1097 IncidenceChanger::DestinationPolicy IncidenceChanger::destinationPolicy() const 1098 { 1099 return d->mDestinationPolicy; 1100 } 1101 1102 void IncidenceChanger::setEntityTreeModel(Akonadi::EntityTreeModel *entityTreeModel) 1103 { 1104 d->mEntityTreeModel = entityTreeModel; 1105 } 1106 1107 Akonadi::EntityTreeModel *IncidenceChanger::entityTreeModel() const 1108 { 1109 return d->mEntityTreeModel; 1110 } 1111 1112 void IncidenceChanger::setDefaultCollection(const Akonadi::Collection &collection) 1113 { 1114 d->mDefaultCollection = collection; 1115 } 1116 1117 Collection IncidenceChanger::defaultCollection() const 1118 { 1119 return d->mDefaultCollection; 1120 } 1121 1122 bool IncidenceChanger::historyEnabled() const 1123 { 1124 return d->mUseHistory; 1125 } 1126 1127 void 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 1137 History *IncidenceChanger::history() const 1138 { 1139 return d->mHistory; 1140 } 1141 1142 bool IncidenceChanger::deletedRecently(Akonadi::Item::Id id) const 1143 { 1144 return d->deleteAlreadyCalled(id); 1145 } 1146 1147 void IncidenceChanger::setGroupwareCommunication(bool enabled) 1148 { 1149 d->mGroupwareCommunication = enabled; 1150 } 1151 1152 bool IncidenceChanger::groupwareCommunication() const 1153 { 1154 return d->mGroupwareCommunication; 1155 } 1156 1157 void IncidenceChanger::setAutoAdjustRecurrence(bool enable) 1158 { 1159 d->mAutoAdjustRecurrence = enable; 1160 } 1161 1162 bool IncidenceChanger::autoAdjustRecurrence() const 1163 { 1164 return d->mAutoAdjustRecurrence; 1165 } 1166 1167 void IncidenceChanger::setInvitationPolicy(IncidenceChanger::InvitationPolicy policy) 1168 { 1169 d->m_invitationPolicy = policy; 1170 } 1171 1172 IncidenceChanger::InvitationPolicy IncidenceChanger::invitationPolicy() const 1173 { 1174 return d->m_invitationPolicy; 1175 } 1176 1177 Akonadi::Collection IncidenceChanger::lastCollectionUsed() const 1178 { 1179 return d->mLastCollectionUsed; 1180 } 1181 1182 void IncidenceChanger::setInvitationPrivacy(IncidenceChanger::InvitationPrivacyFlags invitationPrivacy) 1183 { 1184 d->m_invitationPrivacy = invitationPrivacy; 1185 } 1186 1187 IncidenceChanger::InvitationPrivacyFlags IncidenceChanger::invitationPrivacy() const 1188 { 1189 return d->m_invitationPrivacy; 1190 } 1191 1192 QString 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 1227 void 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 1267 void IncidenceChangerPrivate::cancelTransaction() 1268 { 1269 if (mBatchOperationInProgress) { 1270 mAtomicOperations[mLatestAtomicOperationId]->setRolledback(); 1271 } 1272 } 1273 1274 void 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 1286 bool 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*/ 1317 void ModificationChange::emitCompletionSignal() 1318 { 1319 emitModifyFinished(changer, id, newItem, resultCode, errorString); 1320 } 1321 1322 /**reimp*/ 1323 void CreationChange::emitCompletionSignal() 1324 { 1325 // Does a queued emit, with QMetaObject::invokeMethod 1326 emitCreateFinished(changer, id, newItem, resultCode, errorString); 1327 } 1328 1329 /**reimp*/ 1330 void DeletionChange::emitCompletionSignal() 1331 { 1332 emitDeleteFinished(changer, id, mItemIds, resultCode, errorString); 1333 } 1334 1335 /** 1336 Lost 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 1342 void 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 1367 AtomicOperation::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 1379 Akonadi::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"