File indexing completed on 2024-11-10 04:40:32

0001 /*
0002     SPDX-FileCopyrightText: 2011 Christian Mollekopf <chrigi_1@fastmail.fm>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "trashjob.h"
0008 
0009 #include "entitydeletedattribute.h"
0010 #include "job_p.h"
0011 #include "trashsettings.h"
0012 
0013 #include <KLocalizedString>
0014 
0015 #include "collectiondeletejob.h"
0016 #include "collectionfetchjob.h"
0017 #include "collectionfetchscope.h"
0018 #include "collectionmodifyjob.h"
0019 #include "collectionmovejob.h"
0020 #include "itemdeletejob.h"
0021 #include "itemfetchjob.h"
0022 #include "itemfetchscope.h"
0023 #include "itemmodifyjob.h"
0024 #include "itemmovejob.h"
0025 
0026 #include "akonadicore_debug.h"
0027 
0028 #include <QHash>
0029 
0030 using namespace Akonadi;
0031 
0032 class Akonadi::TrashJobPrivate : public JobPrivate
0033 {
0034 public:
0035     explicit TrashJobPrivate(TrashJob *parent)
0036         : JobPrivate(parent)
0037     {
0038     }
0039     // 4.
0040     void selectResult(KJob *job);
0041     // 3.
0042     // Helper functions to recursively set the attribute on deleted collections
0043     void setAttribute(const Akonadi::Collection::List & /*list*/);
0044     void setAttribute(const Akonadi::Item::List & /*list*/);
0045     // Set attributes after ensuring that move job was successful
0046     void setAttribute(KJob *job);
0047 
0048     // 2.
0049     // called after parent of the trashed item was fetched (needed to see in which resource the item is in)
0050     void parentCollectionReceived(const Akonadi::Collection::List & /*collections*/);
0051 
0052     // 1.
0053     // called after initial fetch of trashed items
0054     void itemsReceived(const Akonadi::Item::List & /*items*/);
0055     // called after initial fetch of trashed collection
0056     void collectionsReceived(const Akonadi::Collection::List & /*collections*/);
0057 
0058     Q_DECLARE_PUBLIC(TrashJob)
0059 
0060     Item::List mItems;
0061     Collection mCollection;
0062     Collection mRestoreCollection;
0063     Collection mTrashCollection;
0064     bool mKeepTrashInCollection = false;
0065     bool mSetRestoreCollection = false; // only set restore collection when moved to trash collection (not in place)
0066     bool mDeleteIfInTrash = false;
0067     QHash<Collection, Item::List> mCollectionItems; // list of trashed items sorted according to parent collection
0068     QHash<Item::Id, Collection> mParentCollections; // fetched parent collection of items (containing the resource name)
0069 };
0070 
0071 void TrashJobPrivate::selectResult(KJob *job)
0072 {
0073     Q_Q(TrashJob);
0074     if (job->error()) {
0075         qCWarning(AKONADICORE_LOG) << job->objectName();
0076         qCWarning(AKONADICORE_LOG) << job->errorString();
0077         return; // KCompositeJob takes care of errors
0078     }
0079 
0080     if (!q->hasSubjobs() || (q->subjobs().contains(static_cast<KJob *>(q->sender())) && q->subjobs().size() == 1)) {
0081         q->emitResult();
0082     }
0083 }
0084 
0085 void TrashJobPrivate::setAttribute(const Akonadi::Collection::List &list)
0086 {
0087     Q_Q(TrashJob);
0088     QListIterator<Collection> i(list);
0089     while (i.hasNext()) {
0090         const Collection &col = i.next();
0091         auto eda = new EntityDeletedAttribute();
0092         if (mSetRestoreCollection) {
0093             Q_ASSERT(mRestoreCollection.isValid());
0094             eda->setRestoreCollection(mRestoreCollection);
0095         }
0096 
0097         Collection modCol(col.id()); // really only modify attribute (forget old remote ids, etc.), otherwise we have an error because of the move
0098         modCol.addAttribute(eda);
0099 
0100         auto job = new CollectionModifyJob(modCol, q);
0101         q->connect(job, &KJob::result, q, [this](KJob *job) {
0102             selectResult(job);
0103         });
0104 
0105         auto itemFetchJob = new ItemFetchJob(col, q);
0106         // TODO not sure if it is guaranteed that itemsReceived is always before result (otherwise the result is emitted before the attributes are set)
0107         q->connect(itemFetchJob, &ItemFetchJob::itemsReceived, q, [this](const auto &items) {
0108             setAttribute(items);
0109         });
0110         q->connect(itemFetchJob, &KJob::result, q, [this](KJob *job) {
0111             selectResult(job);
0112         });
0113     }
0114 }
0115 
0116 void TrashJobPrivate::setAttribute(const Akonadi::Item::List &list)
0117 {
0118     Q_Q(TrashJob);
0119     Item::List items = list;
0120     QMutableListIterator<Item> i(items);
0121     while (i.hasNext()) {
0122         const Item &item = i.next();
0123         auto eda = new EntityDeletedAttribute();
0124         if (mSetRestoreCollection) {
0125             // When deleting a collection, we want to restore the deleted collection's items restored to the deleted collection's parent, not the items parent
0126             if (mRestoreCollection.isValid()) {
0127                 eda->setRestoreCollection(mRestoreCollection);
0128             } else {
0129                 Q_ASSERT(mParentCollections.contains(item.parentCollection().id()));
0130                 eda->setRestoreCollection(mParentCollections.value(item.parentCollection().id()));
0131             }
0132         }
0133 
0134         Item modItem(item.id()); // really only modify attribute (forget old remote ids, etc.)
0135         modItem.addAttribute(eda);
0136         auto job = new ItemModifyJob(modItem, q);
0137         job->setIgnorePayload(true);
0138         q->connect(job, &KJob::result, q, [this](KJob *job) {
0139             selectResult(job);
0140         });
0141     }
0142 
0143     // For some reason it is not possible to apply this change to multiple items at once
0144     /*ItemModifyJob *job = new ItemModifyJob(items, q);
0145     q->connect( job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) );*/
0146 }
0147 
0148 void TrashJobPrivate::setAttribute(KJob *job)
0149 {
0150     Q_Q(TrashJob);
0151     if (job->error()) {
0152         qCWarning(AKONADICORE_LOG) << job->objectName();
0153         qCWarning(AKONADICORE_LOG) << job->errorString();
0154         q->setError(Job::Unknown);
0155         q->setErrorText(i18n("Move to trash collection failed, aborting trash operation"));
0156         return;
0157     }
0158 
0159     // For Items
0160     const QVariant var = job->property("MovedItems");
0161     if (var.isValid()) {
0162         int id = var.toInt();
0163         Q_ASSERT(id >= 0);
0164         setAttribute(mCollectionItems.value(Collection(id)));
0165         return;
0166     }
0167 
0168     // For a collection
0169     Q_ASSERT(mCollection.isValid());
0170     setAttribute(Collection::List() << mCollection);
0171     // Set the attribute on all subcollections and items
0172     auto colFetchJob = new CollectionFetchJob(mCollection, CollectionFetchJob::Recursive, q);
0173     q->connect(colFetchJob, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) {
0174         setAttribute(cols);
0175     });
0176     q->connect(colFetchJob, &KJob::result, q, [this](KJob *job) {
0177         selectResult(job);
0178     });
0179 }
0180 
0181 void TrashJobPrivate::parentCollectionReceived(const Akonadi::Collection::List &collections)
0182 {
0183     Q_Q(TrashJob);
0184     Q_ASSERT(collections.size() == 1);
0185     const Collection &parentCollection = collections.first();
0186 
0187     // store attribute
0188     Q_ASSERT(!parentCollection.resource().isEmpty());
0189     Collection trashCollection = mTrashCollection;
0190     if (!mTrashCollection.isValid()) {
0191         trashCollection = TrashSettings::getTrashCollection(parentCollection.resource());
0192     }
0193     if (!mKeepTrashInCollection && trashCollection.isValid()) { // Only set the restore collection if the item is moved to trash
0194         mSetRestoreCollection = true;
0195     }
0196 
0197     mParentCollections.insert(parentCollection.id(), parentCollection);
0198 
0199     if (trashCollection.isValid()) { // Move the items to the correct collection if available
0200         auto job = new ItemMoveJob(mCollectionItems.value(parentCollection), trashCollection, q);
0201         job->setProperty("MovedItems", parentCollection.id());
0202         q->connect(job, &KJob::result, q, [this](KJob *job) {
0203             setAttribute(job);
0204         }); // Wait until the move finished to set the attribute
0205         q->connect(job, &KJob::result, q, [this](KJob *job) {
0206             selectResult(job);
0207         });
0208     } else {
0209         setAttribute(mCollectionItems.value(parentCollection));
0210     }
0211 }
0212 
0213 void TrashJobPrivate::itemsReceived(const Akonadi::Item::List &items)
0214 {
0215     Q_Q(TrashJob);
0216     if (items.isEmpty()) {
0217         q->setError(Job::Unknown);
0218         q->setErrorText(i18n("Invalid items passed"));
0219         q->emitResult();
0220         return;
0221     }
0222 
0223     Item::List toDelete;
0224     QListIterator<Item> i(items);
0225     while (i.hasNext()) {
0226         const Item &item = i.next();
0227         if (item.hasAttribute<EntityDeletedAttribute>()) {
0228             toDelete.append(item);
0229             continue;
0230         }
0231         Q_ASSERT(item.parentCollection().isValid());
0232         mCollectionItems[item.parentCollection()].append(item); // Sort by parent col ( = restore collection)
0233     }
0234 
0235     for (auto it = mCollectionItems.cbegin(), e = mCollectionItems.cend(); it != e; ++it) {
0236         auto job = new CollectionFetchJob(it.key(), Akonadi::CollectionFetchJob::Base, q);
0237         q->connect(job, &CollectionFetchJob::collectionsReceived, q, [this](const auto &cols) {
0238             parentCollectionReceived(cols);
0239         });
0240     }
0241 
0242     if (mDeleteIfInTrash && !toDelete.isEmpty()) {
0243         auto job = new ItemDeleteJob(toDelete, q);
0244         q->connect(job, &KJob::result, q, [this](KJob *job) {
0245             selectResult(job);
0246         });
0247     } else if (mCollectionItems.isEmpty()) { // No job started, so we abort the job
0248         qCWarning(AKONADICORE_LOG) << "Nothing to do";
0249         q->emitResult();
0250     }
0251 }
0252 
0253 void TrashJobPrivate::collectionsReceived(const Akonadi::Collection::List &collections)
0254 {
0255     Q_Q(TrashJob);
0256     if (collections.isEmpty()) {
0257         q->setError(Job::Unknown);
0258         q->setErrorText(i18n("Invalid collection passed"));
0259         q->emitResult();
0260         return;
0261     }
0262     Q_ASSERT(collections.size() == 1);
0263     mCollection = collections.first();
0264 
0265     if (mCollection.hasAttribute<EntityDeletedAttribute>()) { // marked as deleted
0266         if (mDeleteIfInTrash) {
0267             auto job = new CollectionDeleteJob(mCollection, q);
0268             q->connect(job, &KJob::result, q, [this](KJob *job) {
0269                 selectResult(job);
0270             });
0271         } else {
0272             qCWarning(AKONADICORE_LOG) << "Nothing to do";
0273             q->emitResult();
0274         }
0275         return;
0276     }
0277 
0278     Collection trashCollection = mTrashCollection;
0279     if (!mTrashCollection.isValid()) {
0280         trashCollection = TrashSettings::getTrashCollection(mCollection.resource());
0281     }
0282     if (!mKeepTrashInCollection && trashCollection.isValid()) { // only set the restore collection if the item is moved to trash
0283         mSetRestoreCollection = true;
0284         Q_ASSERT(mCollection.parentCollection().isValid());
0285         mRestoreCollection = mCollection.parentCollection();
0286         mRestoreCollection.setResource(mCollection.resource()); // The parent collection doesn't contain the resource, so we have to set it manually
0287     }
0288 
0289     if (trashCollection.isValid()) {
0290         auto job = new CollectionMoveJob(mCollection, trashCollection, q);
0291         q->connect(job, &KJob::result, q, [this](KJob *job) {
0292             setAttribute(job);
0293         });
0294         q->connect(job, &KJob::result, q, [this](KJob *job) {
0295             selectResult(job);
0296         });
0297     } else {
0298         setAttribute(Collection::List() << mCollection);
0299     }
0300 }
0301 
0302 TrashJob::TrashJob(const Item &item, QObject *parent)
0303     : Job(new TrashJobPrivate(this), parent)
0304 {
0305     Q_D(TrashJob);
0306     d->mItems << item;
0307 }
0308 
0309 TrashJob::TrashJob(const Item::List &items, QObject *parent)
0310     : Job(new TrashJobPrivate(this), parent)
0311 {
0312     Q_D(TrashJob);
0313     d->mItems = items;
0314 }
0315 
0316 TrashJob::TrashJob(const Collection &collection, QObject *parent)
0317     : Job(new TrashJobPrivate(this), parent)
0318 {
0319     Q_D(TrashJob);
0320     d->mCollection = collection;
0321 }
0322 
0323 TrashJob::~TrashJob()
0324 {
0325 }
0326 
0327 Item::List TrashJob::items() const
0328 {
0329     Q_D(const TrashJob);
0330     return d->mItems;
0331 }
0332 
0333 void TrashJob::setTrashCollection(const Akonadi::Collection &collection)
0334 {
0335     Q_D(TrashJob);
0336     d->mTrashCollection = collection;
0337 }
0338 
0339 void TrashJob::keepTrashInCollection(bool enable)
0340 {
0341     Q_D(TrashJob);
0342     d->mKeepTrashInCollection = enable;
0343 }
0344 
0345 void TrashJob::deleteIfInTrash(bool enable)
0346 {
0347     Q_D(TrashJob);
0348     d->mDeleteIfInTrash = enable;
0349 }
0350 
0351 void TrashJob::doStart()
0352 {
0353     Q_D(TrashJob);
0354 
0355     // Fetch items first to ensure that the EntityDeletedAttribute is available
0356     if (!d->mItems.isEmpty()) {
0357         auto job = new ItemFetchJob(d->mItems, this);
0358         job->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::Parent); // so we have access to the resource
0359         // job->fetchScope().setCacheOnly(true);
0360         job->fetchScope().fetchAttribute<EntityDeletedAttribute>(true);
0361         connect(job, &ItemFetchJob::itemsReceived, this, [d](const auto &items) {
0362             d->itemsReceived(items);
0363         });
0364 
0365     } else if (d->mCollection.isValid()) {
0366         auto job = new CollectionFetchJob(d->mCollection, CollectionFetchJob::Base, this);
0367         job->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::Parent);
0368         connect(job, &CollectionFetchJob::collectionsReceived, this, [d](const auto &cols) {
0369             d->collectionsReceived(cols);
0370         });
0371 
0372     } else {
0373         qCWarning(AKONADICORE_LOG) << "No valid collection or empty itemlist";
0374         setError(Job::Unknown);
0375         setErrorText(i18n("No valid collection or empty itemlist"));
0376         emitResult();
0377     }
0378 }
0379 
0380 #include "moc_trashjob.cpp"