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"