File indexing completed on 2024-04-28 05:11:32

0001 /*
0002   SPDX-FileCopyrightText: 2010 Bertjan Broeksema <broeksema@kde.org>
0003   SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
0004 
0005   SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "editoritemmanager.h"
0009 #include "individualmailcomponentfactory.h"
0010 
0011 #include <CalendarSupport/KCalPrefs>
0012 
0013 #include <Akonadi/CalendarUtils>
0014 #include <Akonadi/Item>
0015 #include <Akonadi/ItemDeleteJob>
0016 #include <Akonadi/ItemFetchJob>
0017 #include <Akonadi/ItemFetchScope>
0018 #include <Akonadi/ItemMoveJob>
0019 #include <Akonadi/Monitor>
0020 #include <Akonadi/Session>
0021 #include <Akonadi/TagFetchScope>
0022 
0023 #include "incidenceeditor_debug.h"
0024 #include <KJob>
0025 #include <KLocalizedString>
0026 
0027 #include <QMessageBox>
0028 #include <QPointer>
0029 
0030 /// ItemEditorPrivate
0031 
0032 static void updateIncidenceChangerPrivacyFlags(Akonadi::IncidenceChanger *changer, IncidenceEditorNG::EditorItemManager::ItipPrivacyFlags flags)
0033 {
0034     using IncidenceEditorNG::EditorItemManager;
0035     Akonadi::IncidenceChanger::InvitationPrivacyFlags privacyFlags;
0036     privacyFlags.setFlag(Akonadi::IncidenceChanger::InvitationPrivacySign, (flags & EditorItemManager::ItipPrivacySign) == EditorItemManager::ItipPrivacySign);
0037     privacyFlags.setFlag(Akonadi::IncidenceChanger::InvitationPrivacyEncrypt,
0038                          (flags & EditorItemManager::ItipPrivacyEncrypt) == EditorItemManager::ItipPrivacyEncrypt);
0039     changer->setInvitationPrivacy(privacyFlags);
0040 }
0041 
0042 namespace IncidenceEditorNG
0043 {
0044 class ItemEditorPrivate
0045 {
0046     EditorItemManager *q_ptr;
0047     Q_DECLARE_PUBLIC(EditorItemManager)
0048 
0049 public:
0050     Akonadi::Item mItem;
0051     Akonadi::Item mPrevItem;
0052     Akonadi::ItemFetchScope mFetchScope;
0053     Akonadi::Monitor *mItemMonitor = nullptr;
0054     ItemEditorUi *mItemUi = nullptr;
0055     bool mIsCounterProposal = false;
0056     EditorItemManager::SaveAction currentAction;
0057     Akonadi::IncidenceChanger *mChanger = nullptr;
0058 
0059 public:
0060     ItemEditorPrivate(Akonadi::IncidenceChanger *changer, EditorItemManager *qq);
0061     void itemChanged(const Akonadi::Item &, const QSet<QByteArray> &);
0062     void itemFetchResult(KJob *job);
0063     void itemMoveResult(KJob *job);
0064     void onModifyFinished(const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString);
0065 
0066     void onCreateFinished(const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString);
0067 
0068     void setupMonitor();
0069     void moveJobFinished(KJob *job);
0070     void setItem(const Akonadi::Item &item);
0071 };
0072 
0073 ItemEditorPrivate::ItemEditorPrivate(Akonadi::IncidenceChanger *changer, EditorItemManager *qq)
0074     : q_ptr(qq)
0075     , currentAction(EditorItemManager::None)
0076 {
0077     mFetchScope.fetchFullPayload();
0078     mFetchScope.setAncestorRetrieval(Akonadi::ItemFetchScope::Parent);
0079     mFetchScope.setFetchTags(true);
0080     mFetchScope.tagFetchScope().setFetchIdOnly(false);
0081     mFetchScope.setFetchRemoteIdentification(false);
0082 
0083     mChanger = changer ? changer : new Akonadi::IncidenceChanger(new IndividualMailComponentFactory(qq), qq);
0084 
0085     // clang-format off
0086     qq->connect(mChanger,
0087                 &Akonadi::IncidenceChanger::modifyFinished,
0088                 qq,
0089                 [this](int, const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString) {
0090                 onModifyFinished(item, resultCode, errorString); });
0091 
0092     qq->connect(mChanger,
0093                 &Akonadi::IncidenceChanger::createFinished,
0094                 qq,
0095                 [this](int, const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString) {
0096                     onCreateFinished(item, resultCode, errorString); });
0097     // clang-format on
0098 }
0099 
0100 void ItemEditorPrivate::moveJobFinished(KJob *job)
0101 {
0102     Q_Q(EditorItemManager);
0103     if (job->error()) {
0104         qCCritical(INCIDENCEEDITOR_LOG) << "Error while moving and modifying " << job->errorString();
0105         mItemUi->reject(ItemEditorUi::ItemMoveFailed, job->errorString());
0106     } else {
0107         Akonadi::Item item(mItem.id());
0108         currentAction = EditorItemManager::MoveAndModify;
0109         q->load(item);
0110     }
0111 }
0112 
0113 void ItemEditorPrivate::itemFetchResult(KJob *job)
0114 {
0115     Q_ASSERT(job);
0116     Q_Q(EditorItemManager);
0117 
0118     EditorItemManager::SaveAction action = currentAction;
0119     currentAction = EditorItemManager::None;
0120 
0121     if (job->error()) {
0122         mItemUi->reject(ItemEditorUi::ItemFetchFailed, job->errorString());
0123         return;
0124     }
0125 
0126     auto fetchJob = qobject_cast<Akonadi::ItemFetchJob *>(job);
0127     if (fetchJob->items().isEmpty()) {
0128         mItemUi->reject(ItemEditorUi::ItemFetchFailed);
0129         return;
0130     }
0131 
0132     Akonadi::Item item = fetchJob->items().at(0);
0133     if (mItemUi->hasSupportedPayload(item)) {
0134         setItem(item);
0135         if (action != EditorItemManager::None) {
0136             // Finally enable ok/apply buttons, we've finished loading
0137             Q_EMIT q->itemSaveFinished(action);
0138         }
0139     } else {
0140         mItemUi->reject(ItemEditorUi::ItemHasInvalidPayload);
0141     }
0142 }
0143 
0144 void ItemEditorPrivate::setItem(const Akonadi::Item &item)
0145 {
0146     Q_ASSERT(item.hasPayload());
0147     mPrevItem = item;
0148     mItem = item;
0149     mItemUi->load(item);
0150     setupMonitor();
0151 }
0152 
0153 void ItemEditorPrivate::itemMoveResult(KJob *job)
0154 {
0155     Q_ASSERT(job);
0156     Q_Q(EditorItemManager);
0157 
0158     if (job->error()) {
0159         auto moveJob = qobject_cast<Akonadi::ItemMoveJob *>(job);
0160         Q_ASSERT(moveJob);
0161         Q_UNUSED(moveJob)
0162         // Q_ASSERT(!moveJob->items().isEmpty());
0163         // TODO: What is reasonable behavior at this point?
0164         qCCritical(INCIDENCEEDITOR_LOG) << "Error while moving item "; // << moveJob->items().first().id() << " to collection "
0165         //<< moveJob->destinationCollection() << job->errorString();
0166         Q_EMIT q->itemSaveFailed(EditorItemManager::Move, job->errorString());
0167     } else {
0168         // Fetch the item again, we want a new mItem, which has an updated parentCollection
0169         Akonadi::Item item(mItem.id());
0170         // set currentAction, so the fetchResult slot emits itemSavedFinished(Move);
0171         // We could emit it here, but we should only enable ok/apply buttons after the loading
0172         // is complete
0173         currentAction = EditorItemManager::Move;
0174         q->load(item);
0175     }
0176 }
0177 
0178 void ItemEditorPrivate::onModifyFinished(const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString)
0179 {
0180     Q_Q(EditorItemManager);
0181     if (resultCode == Akonadi::IncidenceChanger::ResultCodeSuccess) {
0182         if (mItem.parentCollection() == mItemUi->selectedCollection() || mItem.storageCollectionId() == mItemUi->selectedCollection().id()) {
0183             mItem = item;
0184             Q_EMIT q->itemSaveFinished(EditorItemManager::Modify);
0185             setupMonitor();
0186         } else { // There's a collection move too.
0187             auto moveJob = new Akonadi::ItemMoveJob(mItem, mItemUi->selectedCollection());
0188             q->connect(moveJob, &KJob::result, q, [this](KJob *job) {
0189                 moveJobFinished(job);
0190             });
0191         }
0192     } else if (resultCode == Akonadi::IncidenceChanger::ResultCodeUserCanceled) {
0193         Q_EMIT q->itemSaveFailed(EditorItemManager::Modify, QString());
0194         q->load(Akonadi::Item(mItem.id()));
0195     } else {
0196         qCCritical(INCIDENCEEDITOR_LOG) << "Modify failed " << errorString;
0197         Q_EMIT q->itemSaveFailed(EditorItemManager::Modify, errorString);
0198     }
0199 }
0200 
0201 void ItemEditorPrivate::onCreateFinished(const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString)
0202 {
0203     Q_Q(EditorItemManager);
0204     if (resultCode == Akonadi::IncidenceChanger::ResultCodeSuccess) {
0205         currentAction = EditorItemManager::Create;
0206         q->load(item);
0207         setupMonitor();
0208     } else {
0209         qCCritical(INCIDENCEEDITOR_LOG) << "Creation failed " << errorString;
0210         Q_EMIT q->itemSaveFailed(EditorItemManager::Create, errorString);
0211     }
0212 }
0213 
0214 void ItemEditorPrivate::setupMonitor()
0215 {
0216     // Q_Q(EditorItemManager);
0217     delete mItemMonitor;
0218     mItemMonitor = new Akonadi::Monitor;
0219     mItemMonitor->setObjectName(QLatin1StringView("EditorItemManagerMonitor"));
0220     mItemMonitor->ignoreSession(Akonadi::Session::defaultSession());
0221     mItemMonitor->itemFetchScope().fetchFullPayload();
0222     if (mItem.isValid()) {
0223         mItemMonitor->setItemMonitored(mItem);
0224     }
0225 
0226     //   q->connect(mItemMonitor, SIGNAL(itemChanged(Akonadi::Item,QSet<QByteArray>)),
0227     //               SLOT(itemChanged(Akonadi::Item,QSet<QByteArray>)));
0228 }
0229 
0230 void ItemEditorPrivate::itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &partIdentifiers)
0231 {
0232     Q_Q(EditorItemManager);
0233     if (mItemUi->containsPayloadIdentifiers(partIdentifiers)) {
0234         QPointer<QMessageBox> dlg = new QMessageBox; // krazy:exclude=qclasses
0235         dlg->setIcon(QMessageBox::Question);
0236         dlg->setInformativeText(
0237             i18n("The item has been changed by another application.\n"
0238                  "What should be done?"));
0239         dlg->addButton(i18n("Take over changes"), QMessageBox::AcceptRole);
0240         dlg->addButton(i18n("Ignore and Overwrite changes"), QMessageBox::RejectRole);
0241 
0242         if (dlg->exec() == QMessageBox::AcceptRole) {
0243             auto job = new Akonadi::ItemFetchJob(mItem);
0244             job->setFetchScope(mFetchScope);
0245 
0246             mItem = item;
0247 
0248             q->load(mItem);
0249         } else {
0250             mItem.setRevision(item.revision());
0251             q->save();
0252         }
0253 
0254         delete dlg;
0255     }
0256 
0257     // Overwrite or not, we need to update the revision and the remote id to be able
0258     // to store item later on.
0259     mItem.setRevision(item.revision());
0260 }
0261 
0262 /// ItemEditor
0263 
0264 EditorItemManager::EditorItemManager(ItemEditorUi *ui, Akonadi::IncidenceChanger *changer)
0265     : d_ptr(new ItemEditorPrivate(changer, this))
0266 {
0267     Q_D(ItemEditor);
0268     d->mItemUi = ui;
0269 }
0270 
0271 EditorItemManager::~EditorItemManager() = default;
0272 
0273 Akonadi::Item EditorItemManager::item(ItemState state) const
0274 {
0275     Q_D(const ItemEditor);
0276 
0277     switch (state) {
0278     case EditorItemManager::AfterSave:
0279         if (d->mItem.hasPayload()) {
0280             return d->mItem;
0281         } else {
0282             qCDebug(INCIDENCEEDITOR_LOG) << "Won't return mItem because isValid = " << d->mItem.isValid() << "; and haPayload is " << d->mItem.hasPayload();
0283         }
0284         break;
0285     case EditorItemManager::BeforeSave:
0286         if (d->mPrevItem.hasPayload()) {
0287             return d->mPrevItem;
0288         } else {
0289             qCDebug(INCIDENCEEDITOR_LOG) << "Won't return mPrevItem because isValid = " << d->mPrevItem.isValid() << "; and haPayload is "
0290                                          << d->mPrevItem.hasPayload();
0291         }
0292         break;
0293     }
0294     qCDebug(INCIDENCEEDITOR_LOG) << "state = " << state;
0295     Q_ASSERT_X(false, "EditorItemManager::item", "Unknown enum value");
0296     return {};
0297 }
0298 
0299 void EditorItemManager::load(const Akonadi::Item &item)
0300 {
0301     Q_D(ItemEditor);
0302 
0303     // We fetch anyways to make sure we have everything required including tags
0304     auto job = new Akonadi::ItemFetchJob(item, this);
0305     job->setFetchScope(d->mFetchScope);
0306     connect(job, &KJob::result, this, [d](KJob *job) {
0307         d->itemFetchResult(job);
0308     });
0309 }
0310 
0311 void EditorItemManager::save(ItipPrivacyFlags itipPrivacy)
0312 {
0313     Q_D(ItemEditor);
0314 
0315     if (!d->mItemUi->isValid()) {
0316         Q_EMIT itemSaveFailed(d->mItem.isValid() ? Modify : Create, QString());
0317         return;
0318     }
0319 
0320     if (!d->mItemUi->isDirty() && d->mItemUi->selectedCollection() == d->mItem.parentCollection()) {
0321         // Item did not change and was not moved
0322         Q_EMIT itemSaveFinished(None);
0323         return;
0324     }
0325 
0326     d->mChanger->setGroupwareCommunication(CalendarSupport::KCalPrefs::instance()->useGroupwareCommunication());
0327     updateIncidenceChangerPrivacyFlags(d->mChanger, itipPrivacy);
0328 
0329     Akonadi::Item updateItem = d->mItemUi->save(d->mItem);
0330     Q_ASSERT(updateItem.id() == d->mItem.id());
0331     d->mItem = updateItem;
0332 
0333     if (d->mItem.isValid()) { // A valid item. Means we're modifying.
0334         Q_ASSERT(d->mItem.parentCollection().isValid());
0335         KCalendarCore::Incidence::Ptr oldPayload = Akonadi::CalendarUtils::incidence(d->mPrevItem);
0336         if (d->mItem.parentCollection() == d->mItemUi->selectedCollection() || d->mItem.storageCollectionId() == d->mItemUi->selectedCollection().id()) {
0337             (void)d->mChanger->modifyIncidence(d->mItem, oldPayload);
0338         } else {
0339             Q_ASSERT(d->mItemUi->selectedCollection().isValid());
0340             Q_ASSERT(d->mItem.parentCollection().isValid());
0341 
0342             qCDebug(INCIDENCEEDITOR_LOG) << "Moving from" << d->mItem.parentCollection().id() << "to" << d->mItemUi->selectedCollection().id();
0343 
0344             if (d->mItemUi->isDirty()) {
0345                 (void)d->mChanger->modifyIncidence(d->mItem, oldPayload);
0346             } else {
0347                 auto itemMoveJob = new Akonadi::ItemMoveJob(d->mItem, d->mItemUi->selectedCollection());
0348                 connect(itemMoveJob, &KJob::result, this, [d](KJob *job) {
0349                     d->itemMoveResult(job);
0350                 });
0351             }
0352         }
0353     } else { // An invalid item. Means we're creating.
0354         if (d->mIsCounterProposal) {
0355             // We don't write back to akonadi, that will be done in ITipHandler.
0356             Q_EMIT itemSaveFinished(EditorItemManager::Modify);
0357         } else {
0358             Q_ASSERT(d->mItemUi->selectedCollection().isValid());
0359             (void)d->mChanger->createFromItem(d->mItem, d->mItemUi->selectedCollection());
0360         }
0361     }
0362 }
0363 
0364 void EditorItemManager::setIsCounterProposal(bool isCounterProposal)
0365 {
0366     Q_D(ItemEditor);
0367     d->mIsCounterProposal = isCounterProposal;
0368 }
0369 
0370 ItemEditorUi::~ItemEditorUi() = default;
0371 
0372 bool ItemEditorUi::isValid() const
0373 {
0374     return true;
0375 }
0376 } // namespace
0377 
0378 #include "moc_editoritemmanager.cpp"