File indexing completed on 2024-05-19 05:11:18

0001 /*
0002   SPDX-FileCopyrightText: 2013 Sérgio Martins <iamsergio@gmail.com>
0003 
0004   SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "incidencechanger_p.h"
0008 #include "akonadicalendar_debug.h"
0009 #include "calendarutils.h"
0010 #include "utils_p.h"
0011 #include <Akonadi/CollectionFetchJob>
0012 #include <Akonadi/CollectionFetchScope>
0013 #include <Akonadi/ItemCreateJob>
0014 #include <KCalendarCore/Incidence>
0015 #include <QDialog>
0016 #include <QString>
0017 
0018 using namespace Akonadi;
0019 
0020 void Change::emitUserDialogClosedAfterChange(Akonadi::ITIPHandlerHelper::SendResult status)
0021 {
0022     Q_EMIT dialogClosedAfterChange(id, status);
0023 }
0024 
0025 void Change::emitUserDialogClosedBeforeChange(Akonadi::ITIPHandlerHelper::SendResult status)
0026 {
0027     Q_EMIT dialogClosedBeforeChange(id, status);
0028 }
0029 
0030 void IncidenceChangerPrivate::loadCollections()
0031 {
0032     if (isLoadingCollections()) {
0033         // Collections are already loading
0034         return;
0035     }
0036 
0037     m_collectionFetchJob = new Akonadi::CollectionFetchJob(Akonadi::Collection::root(), Akonadi::CollectionFetchJob::Recursive);
0038 
0039     m_collectionFetchJob->fetchScope().setContentMimeTypes(KCalendarCore::Incidence::mimeTypes());
0040     connect(m_collectionFetchJob, &KJob::result, this, &IncidenceChangerPrivate::onCollectionsLoaded);
0041     m_collectionFetchJob->start();
0042 }
0043 
0044 Collection::List IncidenceChangerPrivate::collectionsForMimeType(const QString &mimeType, const Collection::List &collections)
0045 {
0046     Collection::List result;
0047     for (const Akonadi::Collection &collection : collections) {
0048         if (collection.contentMimeTypes().contains(mimeType)) {
0049             result << collection;
0050         }
0051     }
0052 
0053     return result;
0054 }
0055 
0056 void IncidenceChangerPrivate::onCollectionsLoaded(KJob *job)
0057 {
0058     Q_ASSERT(!mPendingCreations.isEmpty());
0059     if (job->error() != 0 || !m_collectionFetchJob) {
0060         qCritical() << "Error loading collections:" << job->errorString();
0061         return;
0062     }
0063 
0064     Q_ASSERT(job == m_collectionFetchJob);
0065     Akonadi::Collection::List allCollections;
0066     const auto collections = m_collectionFetchJob->collections();
0067     for (const Akonadi::Collection &collection : collections) {
0068         if (collection.rights() & Akonadi::Collection::CanCreateItem) {
0069             allCollections << collection;
0070         }
0071     }
0072 
0073     m_collectionFetchJob = nullptr;
0074     bool canceled = false;
0075 
0076     // These two will never be true, maybe even assert
0077     bool noAcl = false;
0078     bool invalidCollection = false;
0079     Collection collectionToUse;
0080     const auto lstPendingCreations = mPendingCreations;
0081     for (const Change::Ptr &change : lstPendingCreations) {
0082         mPendingCreations.removeAll(change);
0083 
0084         if (canceled) {
0085             change->resultCode = IncidenceChanger::ResultCodeUserCanceled;
0086             continue;
0087         }
0088 
0089         if (noAcl) {
0090             change->resultCode = IncidenceChanger::ResultCodePermissions;
0091             continue;
0092         }
0093 
0094         if (invalidCollection) {
0095             change->resultCode = IncidenceChanger::ResultCodeInvalidUserCollection;
0096             continue;
0097         }
0098 
0099         if (collectionToUse.isValid()) {
0100             // We don't show the dialog multiple times
0101             step2CreateIncidence(change, collectionToUse);
0102             continue;
0103         }
0104 
0105         KCalendarCore::Incidence::Ptr incidence = CalendarUtils::incidence(change->newItem);
0106         Collection::List candidateCollections = collectionsForMimeType(incidence->mimeType(), allCollections);
0107         if (candidateCollections.count() == 1 && candidateCollections.first().isValid()) {
0108             // We only have 1 writable collection, don't bother the user with a dialog
0109             collectionToUse = candidateCollections.first();
0110             qCDebug(AKONADICALENDAR_LOG) << "Only one collection exists, will not show collection dialog: " << collectionToUse.displayName();
0111             step2CreateIncidence(change, collectionToUse);
0112             continue;
0113         }
0114 
0115         // Lets ask the user which collection to use:
0116         int dialogCode;
0117         QWidget *parent = change->parentWidget;
0118 
0119         const QStringList mimeTypes(incidence->mimeType());
0120         collectionToUse = CalendarUtils::selectCollection(parent, /*by-ref*/ dialogCode, mimeTypes, mDefaultCollection);
0121         if (dialogCode != QDialog::Accepted) {
0122             qCDebug(AKONADICALENDAR_LOG) << "User canceled collection choosing";
0123             change->resultCode = IncidenceChanger::ResultCodeUserCanceled;
0124             canceled = true;
0125             cancelTransaction();
0126             continue;
0127         }
0128 
0129         if (collectionToUse.isValid() && !hasRights(collectionToUse, IncidenceChanger::ChangeTypeCreate)) {
0130             qCWarning(AKONADICALENDAR_LOG) << "No ACLs for incidence creation";
0131             const QString errorMessage = showErrorDialog(IncidenceChanger::ResultCodePermissions, parent);
0132             change->resultCode = IncidenceChanger::ResultCodePermissions;
0133             change->errorString = errorMessage;
0134             noAcl = true;
0135             cancelTransaction();
0136             continue;
0137         }
0138 
0139         // TODO: add unit test for these two situations after reviewing API
0140         if (!collectionToUse.isValid()) {
0141             qCritical() << "Invalid collection selected. Can't create incidence.";
0142             change->resultCode = IncidenceChanger::ResultCodeInvalidUserCollection;
0143             const QString errorString = showErrorDialog(IncidenceChanger::ResultCodeInvalidUserCollection, parent);
0144             change->errorString = errorString;
0145             invalidCollection = true;
0146             cancelTransaction();
0147             continue;
0148         }
0149 
0150         step2CreateIncidence(change, collectionToUse);
0151     }
0152 }
0153 
0154 bool IncidenceChangerPrivate::isLoadingCollections() const
0155 {
0156     return m_collectionFetchJob != nullptr;
0157 }
0158 
0159 void IncidenceChangerPrivate::step1DetermineDestinationCollection(const Change::Ptr &change, const Akonadi::Collection &collection)
0160 {
0161     QWidget *parent = change->parentWidget.data();
0162     if (collection.isValid() && hasRights(collection, IncidenceChanger::ChangeTypeCreate)) {
0163         // The collection passed always has priority
0164         step2CreateIncidence(change, collection);
0165     } else {
0166         switch (mDestinationPolicy) {
0167         case IncidenceChanger::DestinationPolicyDefault:
0168             if (mDefaultCollection.isValid() && hasRights(mDefaultCollection, IncidenceChanger::ChangeTypeCreate)) {
0169                 step2CreateIncidence(change, mDefaultCollection);
0170                 break;
0171             }
0172             qCWarning(AKONADICALENDAR_LOG) << "Destination policy is to use the default collection."
0173                                            << "But it's invalid or doesn't have proper ACLs."
0174                                            << "isValid = " << mDefaultCollection.isValid()
0175                                            << "has ACLs = " << hasRights(mDefaultCollection, IncidenceChanger::ChangeTypeCreate);
0176             // else fallthrough, and ask the user.
0177             [[fallthrough]];
0178         case IncidenceChanger::DestinationPolicyAsk:
0179             mPendingCreations << change;
0180             loadCollections(); // Now we wait, collections are being loaded async
0181             break;
0182         case IncidenceChanger::DestinationPolicyNeverAsk: {
0183             const bool hasRights = this->hasRights(mDefaultCollection, IncidenceChanger::ChangeTypeCreate);
0184             if (mDefaultCollection.isValid() && hasRights) {
0185                 step2CreateIncidence(change, mDefaultCollection);
0186             } else {
0187                 const QString errorString = showErrorDialog(IncidenceChanger::ResultCodeInvalidDefaultCollection, parent);
0188                 qCritical() << errorString << "; rights are " << hasRights;
0189                 change->resultCode = hasRights ? IncidenceChanger::ResultCodeInvalidDefaultCollection : IncidenceChanger::ResultCodePermissions;
0190                 change->errorString = errorString;
0191                 cancelTransaction();
0192             }
0193             break;
0194         }
0195         default:
0196             // Never happens
0197             Q_ASSERT_X(false, "createIncidence()", "unknown destination policy");
0198             cancelTransaction();
0199         }
0200     }
0201 }
0202 
0203 void IncidenceChangerPrivate::step2CreateIncidence(const Change::Ptr &change, const Akonadi::Collection &collection)
0204 {
0205     Q_ASSERT(change);
0206 
0207     mLastCollectionUsed = collection;
0208 
0209     auto createJob = new ItemCreateJob(change->newItem, collection, parentJob(change));
0210     mChangeForJob.insert(createJob, change);
0211 
0212     if (mBatchOperationInProgress) {
0213         AtomicOperation *atomic = mAtomicOperations[mLatestAtomicOperationId];
0214         Q_ASSERT(atomic);
0215         atomic->addChange(change);
0216     }
0217 
0218     // QueuedConnection because of possible sync exec calls.
0219     connect(createJob, &KJob::result, this, &IncidenceChangerPrivate::handleCreateJobResult, Qt::QueuedConnection);
0220 
0221     mChangeById.insert(change->id, change);
0222 }
0223 
0224 #include "moc_incidencechanger_p.cpp"