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