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

0001 /*
0002     SPDX-FileCopyrightText: 2008 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "pastehelper_p.h"
0008 
0009 #include "collectioncopyjob.h"
0010 #include "collectionfetchjob.h"
0011 #include "collectionmovejob.h"
0012 #include "item.h"
0013 #include "itemcopyjob.h"
0014 #include "itemcreatejob.h"
0015 #include "itemmodifyjob.h"
0016 #include "itemmovejob.h"
0017 #include "linkjob.h"
0018 #include "session.h"
0019 #include "transactionsequence.h"
0020 #include "unlinkjob.h"
0021 
0022 #include "akonadicore_debug.h"
0023 
0024 #include <QUrl>
0025 #include <QUrlQuery>
0026 
0027 #include <QByteArray>
0028 #include <QMimeData>
0029 
0030 #include <functional>
0031 
0032 using namespace Akonadi;
0033 
0034 class PasteHelperJob : public Akonadi::TransactionSequence
0035 {
0036     Q_OBJECT
0037 
0038 public:
0039     explicit PasteHelperJob(Qt::DropAction action,
0040                             const Akonadi::Item::List &items,
0041                             const Akonadi::Collection::List &collections,
0042                             const Akonadi::Collection &destination,
0043                             QObject *parent = nullptr);
0044     ~PasteHelperJob() override;
0045 
0046 private Q_SLOTS:
0047     void onDragSourceCollectionFetched(KJob *job);
0048 
0049 private:
0050     void runActions();
0051     void runItemsActions();
0052     void runCollectionsActions();
0053 
0054 private:
0055     Akonadi::Item::List mItems;
0056     Akonadi::Collection::List mCollections;
0057     Akonadi::Collection mDestCollection;
0058     Qt::DropAction mAction;
0059 };
0060 
0061 PasteHelperJob::PasteHelperJob(Qt::DropAction action,
0062                                const Item::List &items,
0063                                const Collection::List &collections,
0064                                const Collection &destination,
0065                                QObject *parent)
0066     : TransactionSequence(parent)
0067     , mItems(items)
0068     , mCollections(collections)
0069     , mDestCollection(destination)
0070     , mAction(action)
0071 {
0072     // FIXME: The below code disables transactions in order to avoid data loss due to nested
0073     // transactions (copy and colcopy in the server doesn't see the items retrieved into the cache and copies empty payloads).
0074     // Remove once this is fixed properly, see the other FIXME comments.
0075     setProperty("transactionsDisabled", true);
0076 
0077     Collection dragSourceCollection;
0078     if (!items.isEmpty() && items.first().parentCollection().isValid()) {
0079         // Check if all items have the same parent collection ID
0080         const Collection parent = items.first().parentCollection();
0081         if (!std::any_of(items.cbegin(), items.cend(), [parent](const Item &item) {
0082                 return item.parentCollection() != parent;
0083             })) {
0084             dragSourceCollection = parent;
0085         }
0086     }
0087 
0088     if (dragSourceCollection.isValid()) {
0089         // Disable autocommitting, because starting a Link/Unlink/Copy/Move job
0090         // after the transaction has ended leaves the job hanging
0091         setAutomaticCommittingEnabled(false);
0092 
0093         auto fetch = new CollectionFetchJob(dragSourceCollection, CollectionFetchJob::Base, this);
0094         QObject::connect(fetch, &KJob::finished, this, &PasteHelperJob::onDragSourceCollectionFetched);
0095     } else {
0096         runActions();
0097     }
0098 }
0099 
0100 PasteHelperJob::~PasteHelperJob()
0101 {
0102 }
0103 
0104 void PasteHelperJob::onDragSourceCollectionFetched(KJob *job)
0105 {
0106     auto fetch = qobject_cast<CollectionFetchJob *>(job);
0107     qCDebug(AKONADICORE_LOG) << fetch->error() << fetch->collections().count();
0108     if (fetch->error() || fetch->collections().count() != 1) {
0109         runActions();
0110         commit();
0111         return;
0112     }
0113 
0114     // If the source collection is virtual, treat copy and move actions differently
0115     const Collection sourceCollection = fetch->collections().at(0);
0116     qCDebug(AKONADICORE_LOG) << "FROM: " << sourceCollection.id() << sourceCollection.name() << sourceCollection.isVirtual();
0117     qCDebug(AKONADICORE_LOG) << "DEST: " << mDestCollection.id() << mDestCollection.name() << mDestCollection.isVirtual();
0118     qCDebug(AKONADICORE_LOG) << "ACTN:" << mAction;
0119     if (sourceCollection.isVirtual()) {
0120         switch (mAction) {
0121         case Qt::CopyAction:
0122             if (mDestCollection.isVirtual()) {
0123                 new LinkJob(mDestCollection, mItems, this);
0124             } else {
0125                 new ItemCopyJob(mItems, mDestCollection, this);
0126             }
0127             break;
0128         case Qt::MoveAction:
0129             new UnlinkJob(sourceCollection, mItems, this);
0130             if (mDestCollection.isVirtual()) {
0131                 new LinkJob(mDestCollection, mItems, this);
0132             } else {
0133                 new ItemCopyJob(mItems, mDestCollection, this);
0134             }
0135             break;
0136         case Qt::LinkAction:
0137             new LinkJob(mDestCollection, mItems, this);
0138             break;
0139         default:
0140             Q_ASSERT(false);
0141         }
0142         runCollectionsActions();
0143         commit();
0144     } else {
0145         runActions();
0146     }
0147 
0148     commit();
0149 }
0150 
0151 void PasteHelperJob::runActions()
0152 {
0153     runItemsActions();
0154     runCollectionsActions();
0155 }
0156 
0157 void PasteHelperJob::runItemsActions()
0158 {
0159     if (mItems.isEmpty()) {
0160         return;
0161     }
0162 
0163     switch (mAction) {
0164     case Qt::CopyAction:
0165         new ItemCopyJob(mItems, mDestCollection, this);
0166         break;
0167     case Qt::MoveAction:
0168         new ItemMoveJob(mItems, mDestCollection, this);
0169         break;
0170     case Qt::LinkAction:
0171         new LinkJob(mDestCollection, mItems, this);
0172         break;
0173     default:
0174         Q_ASSERT(false); // WTF?!
0175     }
0176 }
0177 
0178 void PasteHelperJob::runCollectionsActions()
0179 {
0180     if (mCollections.isEmpty()) {
0181         return;
0182     }
0183 
0184     switch (mAction) {
0185     case Qt::CopyAction:
0186         for (const Collection &col : std::as_const(mCollections)) { // FIXME: remove once we have a batch job for collections as well
0187             new CollectionCopyJob(col, mDestCollection, this);
0188         }
0189         break;
0190     case Qt::MoveAction:
0191         for (const Collection &col : std::as_const(mCollections)) { // FIXME: remove once we have a batch job for collections as well
0192             new CollectionMoveJob(col, mDestCollection, this);
0193         }
0194         break;
0195     case Qt::LinkAction:
0196         // Not supported for collections
0197         break;
0198     default:
0199         Q_ASSERT(false); // WTF?!
0200     }
0201 }
0202 
0203 bool PasteHelper::canPaste(const QMimeData *mimeData, const Collection &collection, Qt::DropAction action)
0204 {
0205     if (!mimeData || !collection.isValid()) {
0206         return false;
0207     }
0208 
0209     // check that the target collection has the rights to
0210     // create the pasted items resp. collections
0211     Collection::Rights neededRights = Collection::ReadOnly;
0212     if (mimeData->hasUrls()) {
0213         const QList<QUrl> urls = mimeData->urls();
0214         for (const QUrl &url : urls) {
0215             const QUrlQuery query(url);
0216             if (query.hasQueryItem(QStringLiteral("item"))) {
0217                 if (action == Qt::LinkAction) {
0218                     neededRights |= Collection::CanLinkItem;
0219                 } else {
0220                     neededRights |= Collection::CanCreateItem;
0221                 }
0222             } else if (query.hasQueryItem(QStringLiteral("collection"))) {
0223                 neededRights |= Collection::CanCreateCollection;
0224             }
0225         }
0226 
0227         if ((collection.rights() & neededRights) == 0) {
0228             return false;
0229         }
0230 
0231         // check that the target collection supports the mime types of the
0232         // items/collections that shall be pasted
0233         bool supportsMimeTypes = true;
0234         for (const QUrl &url : std::as_const(urls)) {
0235             const QUrlQuery query(url);
0236             // collections do not provide mimetype information, so ignore this check
0237             if (query.hasQueryItem(QStringLiteral("collection"))) {
0238                 continue;
0239             }
0240 
0241             const QString mimeType = query.queryItemValue(QStringLiteral("type"));
0242             if (!collection.contentMimeTypes().contains(mimeType)) {
0243                 supportsMimeTypes = false;
0244                 break;
0245             }
0246         }
0247 
0248         return supportsMimeTypes;
0249     }
0250 
0251     return false;
0252 }
0253 
0254 KJob *PasteHelper::paste(const QMimeData *mimeData, const Collection &collection, Qt::DropAction action, Session *session)
0255 {
0256     if (!canPaste(mimeData, collection, action)) {
0257         return nullptr;
0258     }
0259 
0260     // we try to drop data not coming with the akonadi:// url
0261     // find a type the target collection supports
0262     const QStringList lstFormats = mimeData->formats();
0263     for (const QString &type : lstFormats) {
0264         if (!collection.contentMimeTypes().contains(type)) {
0265             continue;
0266         }
0267 
0268         QByteArray item = mimeData->data(type);
0269         // HACK for some unknown reason the data is sometimes 0-terminated...
0270         if (!item.isEmpty() && item.at(item.size() - 1) == 0) {
0271             item.resize(item.size() - 1);
0272         }
0273 
0274         Item it;
0275         it.setMimeType(type);
0276         it.setPayloadFromData(item);
0277 
0278         auto job = new ItemCreateJob(it, collection);
0279         return job;
0280     }
0281 
0282     if (!mimeData->hasUrls()) {
0283         return nullptr;
0284     }
0285 
0286     // data contains an url list
0287     return pasteUriList(mimeData, collection, action, session);
0288 }
0289 
0290 KJob *PasteHelper::pasteUriList(const QMimeData *mimeData, const Collection &destination, Qt::DropAction action, Session *session)
0291 {
0292     if (!mimeData->hasUrls()) {
0293         return nullptr;
0294     }
0295 
0296     if (!canPaste(mimeData, destination, action)) {
0297         return nullptr;
0298     }
0299 
0300     const QList<QUrl> urls = mimeData->urls();
0301     Collection::List collections;
0302     Item::List items;
0303     for (const QUrl &url : urls) {
0304         const QUrlQuery query(url);
0305         const Collection collection = Collection::fromUrl(url);
0306         if (collection.isValid()) {
0307             collections.append(collection);
0308         }
0309         Item item = Item::fromUrl(url);
0310         if (query.hasQueryItem(QStringLiteral("parent"))) {
0311             item.setParentCollection(Collection(query.queryItemValue(QStringLiteral("parent")).toLongLong()));
0312         }
0313         if (item.isValid()) {
0314             items.append(item);
0315         }
0316         // TODO: handle non Akonadi URLs?
0317     }
0318 
0319     auto job = new PasteHelperJob(action, items, collections, destination, session);
0320 
0321     return job;
0322 }
0323 
0324 #include "pastehelper.moc"