1 /* 2 SPDX-FileCopyrightText: 2004 Reinhold Kainhofer <reinhold@kainhofer.com> 3 4 SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net 5 SPDX-FileContributor: Sergio Martins <sergio.martins@kdab.com> 6 7 SPDX-FileCopyrightText: 2010-2012 Sérgio Martins <iamsergio@gmail.com> 8 9 SPDX-License-Identifier: LGPL-2.0-or-later 10 */ 11 #pragma once 12 13 #include "history.h" 14 #include "incidencechanger.h" 15 #include "itiphandlerhelper_p.h" 16 17 #include <Akonadi/Collection> 18 #include <Akonadi/Item> 19 #include <Akonadi/TransactionSequence> 20 21 #include <QObject> 22 #include <QPointer> 23 #include <QSet> 24 #include <QVector> 25 26 class KJob; 27 class QWidget; 28 29 namespace Akonadi 30 { 31 class TransactionSequence; 32 class CollectionFetchJob; 33 34 class Change : public QObject 35 { 36 Q_OBJECT 37 public: 38 using Ptr = QSharedPointer<Change>; 39 using List = QVector<Ptr>; Change(IncidenceChanger * incidenceChanger,int changeId,IncidenceChanger::ChangeType changeType,uint operationId,QWidget * parent)40 Change(IncidenceChanger *incidenceChanger, int changeId, IncidenceChanger::ChangeType changeType, uint operationId, QWidget *parent) 41 : id(changeId) 42 , type(changeType) 43 , recordToHistory(incidenceChanger->historyEnabled()) 44 , parentWidget(parent) 45 , atomicOperationId(operationId) 46 , resultCode(Akonadi::IncidenceChanger::ResultCodeSuccess) 47 , completed(false) 48 , queuedModification(false) 49 , useGroupwareCommunication(incidenceChanger->groupwareCommunication()) 50 , changer(incidenceChanger) 51 { 52 } 53 ~Change()54 ~Change() override 55 { 56 if (parentChange) { 57 parentChange->childAboutToDie(this); 58 } 59 } 60 childAboutToDie(Change * child)61 virtual void childAboutToDie(Change *child) 62 { 63 Q_UNUSED(child) 64 } 65 66 virtual void emitCompletionSignal() = 0; 67 68 const int id; 69 const IncidenceChanger::ChangeType type; 70 const bool recordToHistory; 71 const QPointer<QWidget> parentWidget; 72 uint atomicOperationId; 73 74 // If this change is internal, i.e. not initiated by the user, mParentChange will 75 // contain the non-internal change. 76 QSharedPointer<Change> parentChange; 77 78 Akonadi::Item::List originalItems; 79 Akonadi::Item newItem; 80 81 QString errorString; 82 IncidenceChanger::ResultCode resultCode; 83 bool completed; 84 bool queuedModification; 85 bool useGroupwareCommunication; 86 87 Q_SIGNALS: 88 void dialogClosedBeforeChange(int id, ITIPHandlerHelper::SendResult status); 89 void dialogClosedAfterChange(int id, ITIPHandlerHelper::SendResult status); 90 91 public Q_SLOTS: 92 void emitUserDialogClosedAfterChange(Akonadi::ITIPHandlerHelper::SendResult status); 93 void emitUserDialogClosedBeforeChange(Akonadi::ITIPHandlerHelper::SendResult status); 94 95 protected: 96 IncidenceChanger *const changer; 97 }; 98 99 class ModificationChange : public Change 100 { 101 Q_OBJECT 102 public: 103 using Ptr = QSharedPointer<ModificationChange>; ModificationChange(IncidenceChanger * changer,int id,uint atomicOperationId,QWidget * parent)104 ModificationChange(IncidenceChanger *changer, int id, uint atomicOperationId, QWidget *parent) 105 : Change(changer, id, IncidenceChanger::ChangeTypeModify, atomicOperationId, parent) 106 { 107 } 108 ~ModificationChange()109 ~ModificationChange() override 110 { 111 if (!parentChange) { 112 emitCompletionSignal(); 113 } 114 } 115 116 void emitCompletionSignal() override; 117 }; 118 119 class CreationChange : public Change 120 { 121 Q_OBJECT 122 public: 123 using Ptr = QSharedPointer<CreationChange>; CreationChange(IncidenceChanger * changer,int id,uint atomicOperationId,QWidget * parent)124 CreationChange(IncidenceChanger *changer, int id, uint atomicOperationId, QWidget *parent) 125 : Change(changer, id, IncidenceChanger::ChangeTypeCreate, atomicOperationId, parent) 126 { 127 } 128 ~CreationChange()129 ~CreationChange() override 130 { 131 // qCDebug(AKONADICALENDAR_LOG) << "CreationChange::~ will emit signal with " << resultCode; 132 if (!parentChange) { 133 emitCompletionSignal(); 134 } 135 } 136 137 void emitCompletionSignal() override; 138 139 Akonadi::Collection mUsedCol1lection; 140 }; 141 142 class DeletionChange : public Change 143 { 144 Q_OBJECT 145 public: 146 using Ptr = QSharedPointer<DeletionChange>; DeletionChange(IncidenceChanger * changer,int id,uint atomicOperationId,QWidget * parent)147 DeletionChange(IncidenceChanger *changer, int id, uint atomicOperationId, QWidget *parent) 148 : Change(changer, id, IncidenceChanger::ChangeTypeDelete, atomicOperationId, parent) 149 { 150 } 151 ~DeletionChange()152 ~DeletionChange() override 153 { 154 // qCDebug(AKONADICALENDAR_LOG) << "DeletionChange::~ will emit signal with " << resultCode; 155 if (!parentChange) { 156 emitCompletionSignal(); 157 } 158 } 159 160 void emitCompletionSignal() override; 161 162 QVector<Akonadi::Item::Id> mItemIds; 163 }; 164 165 class AtomicOperation 166 { 167 public: 168 uint m_id; 169 170 // To make sure they are not repeated 171 QSet<Akonadi::Item::Id> m_itemIdsInOperation; 172 173 // After endAtomicOperation() is called we don't accept more changes 174 bool m_endCalled; 175 176 // Number of completed changes(jobs) 177 int m_numCompletedChanges; 178 QString m_description; 179 bool m_transactionCompleted; 180 181 AtomicOperation(IncidenceChangerPrivate *icp, uint ident); 182 ~AtomicOperation()183 ~AtomicOperation() 184 { 185 // qCDebug(AKONADICALENDAR_LOG) << "AtomicOperation::~ " << wasRolledback << changes.count(); 186 if (m_wasRolledback) { 187 for (int i = 0; i < m_changes.count(); ++i) { 188 // When a job that can finish successfully is aborted because the transaction failed 189 // because of some other job, akonadi is returning an Unknown error 190 // which isn't very specific 191 if (m_changes[i]->completed 192 && (m_changes[i]->resultCode == IncidenceChanger::ResultCodeSuccess 193 || (m_changes[i]->resultCode == IncidenceChanger::ResultCodeJobError 194 && m_changes[i]->errorString == QLatin1String("Unknown error.")))) { 195 m_changes[i]->resultCode = IncidenceChanger::ResultCodeRolledback; 196 } 197 } 198 } 199 } 200 201 // Did all jobs return ? pendingJobs()202 bool pendingJobs() const 203 { 204 return m_changes.count() > m_numCompletedChanges; 205 } 206 setRolledback()207 void setRolledback() 208 { 209 // qCDebug(AKONADICALENDAR_LOG) << "AtomicOperation::setRolledBack()"; 210 m_wasRolledback = true; 211 transaction()->rollback(); 212 } 213 rolledback()214 bool rolledback() const 215 { 216 return m_wasRolledback; 217 } 218 addChange(const Change::Ptr & change)219 void addChange(const Change::Ptr &change) 220 { 221 if (change->type == IncidenceChanger::ChangeTypeDelete) { 222 DeletionChange::Ptr deletion = change.staticCast<DeletionChange>(); 223 for (Akonadi::Item::Id id : std::as_const(deletion->mItemIds)) { 224 Q_ASSERT(!m_itemIdsInOperation.contains(id)); 225 m_itemIdsInOperation.insert(id); 226 } 227 } else if (change->type == IncidenceChanger::ChangeTypeModify) { 228 Q_ASSERT(!m_itemIdsInOperation.contains(change->newItem.id())); 229 m_itemIdsInOperation.insert(change->newItem.id()); 230 } 231 232 m_changes << change; 233 } 234 235 Akonadi::TransactionSequence *transaction(); 236 237 private: 238 Q_DISABLE_COPY(AtomicOperation) 239 QVector<Change::Ptr> m_changes; 240 bool m_wasRolledback = false; 241 Akonadi::TransactionSequence *m_transaction = nullptr; // constructed in first use 242 IncidenceChangerPrivate *m_incidenceChangerPrivate = nullptr; 243 }; 244 245 class IncidenceChangerPrivate : public QObject 246 { 247 Q_OBJECT 248 public: 249 explicit IncidenceChangerPrivate(bool enableHistory, ITIPHandlerComponentFactory *factory, IncidenceChanger *mIncidenceChanger); 250 ~IncidenceChangerPrivate() override; 251 252 void loadCollections(); // async-loading of list of writable collections 253 bool isLoadingCollections() const; 254 Collection::List collectionsForMimeType(const QString &mimeType, const Collection::List &collections); 255 256 // steps for the async operation: 257 void step1DetermineDestinationCollection(const Change::Ptr &change, const Collection &collection); 258 void step2CreateIncidence(const Change::Ptr &change, const Collection &collection); 259 260 /** 261 Returns true if, for a specific item, an ItemDeleteJob is already running, 262 or if one already run successfully. 263 */ 264 bool deleteAlreadyCalled(Akonadi::Item::Id id) const; 265 266 QString showErrorDialog(Akonadi::IncidenceChanger::ResultCode, QWidget *parent); 267 268 void adjustRecurrence(const KCalendarCore::Incidence::Ptr &originalIncidence, const KCalendarCore::Incidence::Ptr &incidence); 269 270 bool hasRights(const Akonadi::Collection &collection, IncidenceChanger::ChangeType) const; 271 void queueModification(const Change::Ptr &); 272 void performModification(const Change::Ptr &); 273 bool atomicOperationIsValid(uint atomicOperationId) const; 274 Akonadi::Job *parentJob(const Change::Ptr &change) const; 275 void cancelTransaction(); 276 void cleanupTransaction(); 277 bool allowAtomicOperation(int atomicOperationId, const Change::Ptr &change) const; 278 279 void handleInvitationsBeforeChange(const Change::Ptr &change); 280 void handleInvitationsAfterChange(const Change::Ptr &change); 281 static bool 282 myAttendeeStatusChanged(const KCalendarCore::Incidence::Ptr &newIncidence, const KCalendarCore::Incidence::Ptr &oldIncidence, const QStringList &myEmails); 283 284 public Q_SLOTS: 285 void handleCreateJobResult(KJob *job); 286 void handleModifyJobResult(KJob *job); 287 void handleDeleteJobResult(KJob *job); 288 void handleTransactionJobResult(KJob *job); 289 void performNextModification(Akonadi::Item::Id id); 290 void onCollectionsLoaded(KJob *job); 291 292 void handleCreateJobResult2(int changeId, ITIPHandlerHelper::SendResult); 293 void handleDeleteJobResult2(int changeId, ITIPHandlerHelper::SendResult); 294 void handleModifyJobResult2(int changeId, ITIPHandlerHelper::SendResult); 295 void performModification2(int changeId, ITIPHandlerHelper::SendResult); 296 void deleteIncidences2(int changeId, ITIPHandlerHelper::SendResult); 297 298 public: 299 int mLatestChangeId; 300 QHash<const KJob *, Change::Ptr> mChangeForJob; 301 bool mShowDialogsOnError = false; 302 Akonadi::Collection mDefaultCollection; 303 Akonadi::EntityTreeModel *mEntityTreeModel = nullptr; 304 IncidenceChanger::DestinationPolicy mDestinationPolicy; 305 QVector<Akonadi::Item::Id> mDeletedItemIds; 306 Change::List mPendingCreations; // Creations waiting for collections to be loaded 307 308 History *mHistory = nullptr; 309 bool mUseHistory = false; 310 311 /** 312 Queue modifications by ID. We can only send a modification to akonadi when the previous 313 one ended. 314 315 The container doesn't look like a queue because of an optimization: if there's a modification 316 A in progress, a modification B waiting (queued), and then a new one C comes in, 317 we just discard B, and queue C. The queue always has 1 element max. 318 */ 319 QHash<Akonadi::Item::Id, Change::Ptr> mQueuedModifications; 320 321 /** 322 So we know if there's already a modification in progress 323 */ 324 QHash<Akonadi::Item::Id, Change::Ptr> mModificationsInProgress; 325 326 QHash<int, Change::Ptr> mChangeById; 327 328 /** 329 Indexed by atomic operation id. 330 */ 331 QHash<uint, AtomicOperation *> mAtomicOperations; 332 333 bool mRespectsCollectionRights = false; 334 bool mGroupwareCommunication = false; 335 336 QHash<Akonadi::TransactionSequence *, uint> mAtomicOperationByTransaction; 337 QHash<uint, ITIPHandlerHelper::SendResult> mInvitationStatusByAtomicOperation; 338 339 uint mLatestAtomicOperationId; 340 bool mBatchOperationInProgress; 341 Akonadi::Collection mLastCollectionUsed; 342 bool mAutoAdjustRecurrence; 343 344 Akonadi::CollectionFetchJob *m_collectionFetchJob = nullptr; 345 346 QMap<KJob *, QSet<KCalendarCore::IncidenceBase::Field>> mDirtyFieldsByJob; 347 348 IncidenceChanger::InvitationPolicy m_invitationPolicy; 349 350 ITIPHandlerComponentFactory *mFactory = nullptr; 351 352 private: 353 IncidenceChanger *q = nullptr; 354 }; 355 } 356 357