File indexing completed on 2024-04-28 15:27:30

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-only
0006 */
0007 
0008 #include "paste.h"
0009 #include "kio_widgets_debug.h"
0010 
0011 #include "../utils_p.h"
0012 #include "kio/copyjob.h"
0013 #include "kio/deletejob.h"
0014 #include "kio/global.h"
0015 #include "kio/renamedialog.h"
0016 #include "kio/statjob.h"
0017 #include "pastedialog_p.h"
0018 #include <kdirnotify.h>
0019 #include <kfileitem.h>
0020 #include <kfileitemlistproperties.h>
0021 #include <kio/storedtransferjob.h>
0022 
0023 #include <KJobWidgets>
0024 #include <KLocalizedString>
0025 #include <KMessageBox>
0026 #include <KUrlMimeData>
0027 
0028 #include <QApplication>
0029 #include <QClipboard>
0030 #include <QDebug>
0031 #include <QFileInfo>
0032 #include <QInputDialog>
0033 #include <QMimeData>
0034 #include <QMimeDatabase>
0035 #include <QTemporaryFile>
0036 
0037 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 4)
0038 // This could be made a public method, if there's a need for pasting only urls
0039 // and not random data.
0040 /**
0041  * Pastes URLs from the clipboard. This results in a copy or move job,
0042  * depending on whether the user has copied or cut the items.
0043  *
0044  * @param mimeData the mimeData to paste, usually QApplication::clipboard()->mimeData()
0045  * @param destDir Destination directory where the items will be copied/moved.
0046  * @param flags the flags are passed to KIO::copy or KIO::move.
0047  * @return the copy or move job handling the operation, or @c nullptr if there is nothing to do
0048  * @since ...
0049  */
0050 // KIOWIDGETS_EXPORT Job *pasteClipboardUrls(const QUrl& destDir, JobFlags flags = DefaultFlags);
0051 static KIO::Job *pasteClipboardUrls(const QMimeData *mimeData, const QUrl &destDir, KIO::JobFlags flags = KIO::DefaultFlags)
0052 {
0053     const QList<QUrl> urls = KUrlMimeData::urlsFromMimeData(mimeData, KUrlMimeData::PreferLocalUrls);
0054     if (!urls.isEmpty()) {
0055         const bool move = KIO::isClipboardDataCut(mimeData);
0056         KIO::Job *job = nullptr;
0057         if (move) {
0058             job = KIO::move(urls, destDir, flags);
0059         } else {
0060             job = KIO::copy(urls, destDir, flags);
0061         }
0062         return job;
0063     }
0064     return nullptr;
0065 }
0066 #endif
0067 
0068 static QUrl getDestinationUrl(const QUrl &srcUrl, const QUrl &destUrl, QWidget *widget)
0069 {
0070     KIO::StatJob *job = KIO::stat(destUrl, destUrl.isLocalFile() ? KIO::HideProgressInfo : KIO::DefaultFlags);
0071     job->setDetails(KIO::StatBasic);
0072     job->setSide(KIO::StatJob::DestinationSide);
0073     KJobWidgets::setWindow(job, widget);
0074 
0075     // Check for existing destination file.
0076     // When we were using CopyJob, we couldn't let it do that (would expose
0077     // an ugly tempfile name as the source URL)
0078     // And now we're using a put job anyway, no destination checking included.
0079     if (job->exec()) {
0080         KIO::RenameDialog dlg(widget, i18n("File Already Exists"), srcUrl, destUrl, KIO::RenameDialog_Overwrite);
0081         KIO::RenameDialog_Result res = static_cast<KIO::RenameDialog_Result>(dlg.exec());
0082 
0083         if (res == KIO::Result_Rename) {
0084             return dlg.newDestUrl();
0085         } else if (res == KIO::Result_Cancel) {
0086             return QUrl();
0087         } else if (res == KIO::Result_Overwrite) {
0088             return destUrl;
0089         }
0090     }
0091 
0092     return destUrl;
0093 }
0094 
0095 static QUrl getNewFileName(const QUrl &u, const QString &text, const QString &suggestedFileName, QWidget *widget)
0096 {
0097     bool ok;
0098     QString dialogText(text);
0099     if (dialogText.isEmpty()) {
0100         dialogText = i18n("Filename for clipboard content:");
0101     }
0102     QString file = QInputDialog::getText(widget, QString(), dialogText, QLineEdit::Normal, suggestedFileName, &ok);
0103     if (!ok) {
0104         return QUrl();
0105     }
0106 
0107     QUrl myurl(u);
0108     myurl.setPath(Utils::concatPaths(myurl.path(), file));
0109 
0110     return getDestinationUrl(u, myurl, widget);
0111 }
0112 
0113 static KIO::Job *putDataAsyncTo(const QUrl &url, const QByteArray &data, QWidget *widget, KIO::JobFlags flags)
0114 {
0115     KIO::Job *job = KIO::storedPut(data, url, -1, flags);
0116     QObject::connect(job, &KIO::Job::result, [url](KJob *job) {
0117         if (job->error() == KJob::NoError) {
0118             org::kde::KDirNotify::emitFilesAdded(url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
0119         }
0120     });
0121     KJobWidgets::setWindow(job, widget);
0122     return job;
0123 }
0124 
0125 static QByteArray chooseFormatAndUrl(const QUrl &u,
0126                                      const QMimeData *mimeData,
0127                                      const QStringList &formats,
0128                                      const QString &text,
0129                                      const QString &suggestedFileName,
0130                                      QWidget *widget,
0131                                      bool clipboard,
0132                                      QUrl *newUrl)
0133 {
0134     QMimeDatabase db;
0135     QStringList formatLabels;
0136     formatLabels.reserve(formats.size());
0137     for (int i = 0; i < formats.size(); ++i) {
0138         const QString &fmt = formats[i];
0139         QMimeType mime = db.mimeTypeForName(fmt);
0140         if (mime.isValid()) {
0141             formatLabels.append(i18n("%1 (%2)", mime.comment(), fmt));
0142         } else {
0143             formatLabels.append(fmt);
0144         }
0145     }
0146 
0147     QString dialogText(text);
0148     if (dialogText.isEmpty()) {
0149         dialogText = i18n("Filename for clipboard content:");
0150     }
0151 
0152     KIO::PasteDialog dlg(QString(), dialogText, suggestedFileName, formatLabels, widget);
0153 
0154     if (dlg.exec() != QDialog::Accepted) {
0155         return QByteArray();
0156     }
0157 
0158     const QString chosenFormat = formats[dlg.comboItem()];
0159     if (clipboard && !qApp->clipboard()->mimeData()->hasFormat(chosenFormat)) {
0160         KMessageBox::information(widget,
0161                                  i18n("The clipboard has changed since you used 'paste': "
0162                                       "the chosen data format is no longer applicable. "
0163                                       "Please copy again what you wanted to paste."));
0164         return QByteArray();
0165     }
0166 
0167     const QString result = dlg.lineEditText();
0168 
0169     // qDebug() << " result=" << result << " chosenFormat=" << chosenFormat;
0170     *newUrl = u;
0171     newUrl->setPath(Utils::concatPaths(newUrl->path(), result));
0172 
0173     const QUrl destUrl = getDestinationUrl(u, *newUrl, widget);
0174     *newUrl = destUrl;
0175 
0176     // In Qt3, the result of clipboard()->mimeData() only existed until the next
0177     // event loop run (see dlg.exec() above), so we re-fetched it.
0178     // TODO: This should not be necessary with Qt5; remove this conditional
0179     // and test that it still works.
0180     if (clipboard) {
0181         mimeData = QApplication::clipboard()->mimeData();
0182     }
0183     const QByteArray ba = mimeData->data(chosenFormat);
0184     return ba;
0185 }
0186 
0187 static QStringList extractFormats(const QMimeData *mimeData)
0188 {
0189     QStringList formats;
0190     const QStringList allFormats = mimeData->formats();
0191     for (const QString &format : allFormats) {
0192         if (format == QLatin1String("application/x-qiconlist")) { // Q3IconView and kde4's libkonq
0193             continue;
0194         }
0195         if (format == QLatin1String("application/x-kde-cutselection")) { // see isClipboardDataCut
0196             continue;
0197         }
0198         if (format == QLatin1String("application/x-kde-suggestedfilename")) {
0199             continue;
0200         }
0201         if (format.startsWith(QLatin1String("application/x-qt-"))) { // Qt-internal
0202             continue;
0203         }
0204         if (format.startsWith(QLatin1String("x-kmail-drag/"))) { // app-internal
0205             continue;
0206         }
0207         if (!format.contains(QLatin1Char('/'))) { // e.g. TARGETS, MULTIPLE, TIMESTAMP
0208             continue;
0209         }
0210         formats.append(format);
0211     }
0212     return formats;
0213 }
0214 
0215 KIOWIDGETS_EXPORT bool KIO::canPasteMimeData(const QMimeData *data)
0216 {
0217     return data->hasText() || !extractFormats(data).isEmpty();
0218 }
0219 
0220 KIO::Job *pasteMimeDataImpl(const QMimeData *mimeData, const QUrl &destUrl, const QString &dialogText, QWidget *widget, bool clipboard)
0221 {
0222     QByteArray ba;
0223     const QString suggestedFilename = QString::fromUtf8(mimeData->data(QStringLiteral("application/x-kde-suggestedfilename")));
0224 
0225     // Now check for plain text
0226     // We don't want to display a MIME type choice for a QTextDrag, those MIME type look ugly.
0227     if (mimeData->hasText()) {
0228         ba = mimeData->text().toLocal8Bit(); // encoding OK?
0229     } else {
0230         const QStringList formats = extractFormats(mimeData);
0231         if (formats.isEmpty()) {
0232             return nullptr;
0233         } else if (formats.size() > 1) {
0234             QUrl newUrl;
0235             ba = chooseFormatAndUrl(destUrl, mimeData, formats, dialogText, suggestedFilename, widget, clipboard, &newUrl);
0236             if (ba.isEmpty() || newUrl.isEmpty()) {
0237                 return nullptr;
0238             }
0239             return putDataAsyncTo(newUrl, ba, widget, KIO::Overwrite);
0240         }
0241         ba = mimeData->data(formats.first());
0242     }
0243     if (ba.isEmpty()) {
0244         return nullptr;
0245     }
0246 
0247     const QUrl newUrl = getNewFileName(destUrl, dialogText, suggestedFilename, widget);
0248     if (newUrl.isEmpty()) {
0249         return nullptr;
0250     }
0251 
0252     return putDataAsyncTo(newUrl, ba, widget, KIO::Overwrite);
0253 }
0254 
0255 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 4)
0256 // The main method for pasting
0257 KIOWIDGETS_EXPORT KIO::Job *KIO::pasteClipboard(const QUrl &destUrl, QWidget *widget, bool move)
0258 {
0259     Q_UNUSED(move);
0260 
0261     if (!destUrl.isValid()) {
0262         KMessageBox::error(widget, i18n("Malformed URL\n%1", destUrl.errorString()));
0263         qCWarning(KIO_WIDGETS) << destUrl.errorString();
0264         return nullptr;
0265     }
0266 
0267     // TODO: if we passed mimeData as argument, we could write unittests that don't
0268     // mess up the clipboard and that don't need QtGui.
0269     const QMimeData *mimeData = QApplication::clipboard()->mimeData();
0270 
0271     if (mimeData->hasUrls()) {
0272         // We can ignore the bool move, KIO::paste decodes it
0273         KIO::Job *job = pasteClipboardUrls(mimeData, destUrl);
0274         if (job) {
0275             KJobWidgets::setWindow(job, widget);
0276             return job;
0277         }
0278     }
0279 
0280     return pasteMimeDataImpl(mimeData, destUrl, QString(), widget, true /*clipboard*/);
0281 }
0282 #endif
0283 
0284 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 4)
0285 QString KIO::pasteActionText()
0286 {
0287     const QMimeData *mimeData = QApplication::clipboard()->mimeData();
0288     const QList<QUrl> urls = KUrlMimeData::urlsFromMimeData(mimeData);
0289     if (!urls.isEmpty()) {
0290         if (urls.first().isLocalFile()) {
0291             return i18np("&Paste File", "&Paste %1 Files", urls.count());
0292         } else {
0293             return i18np("&Paste URL", "&Paste %1 URLs", urls.count());
0294         }
0295     } else if (!mimeData->formats().isEmpty()) {
0296         return i18n("&Paste Clipboard Contents");
0297     } else {
0298         return QString();
0299     }
0300 }
0301 #endif
0302 
0303 KIOWIDGETS_EXPORT QString KIO::pasteActionText(const QMimeData *mimeData, bool *enable, const KFileItem &destItem)
0304 {
0305     bool canPasteData = false;
0306     QList<QUrl> urls;
0307 
0308     // mimeData can be 0 according to https://bugs.kde.org/show_bug.cgi?id=335053
0309     if (mimeData) {
0310         canPasteData = KIO::canPasteMimeData(mimeData);
0311         urls = KUrlMimeData::urlsFromMimeData(mimeData);
0312     } else {
0313         qCWarning(KIO_WIDGETS) << "QApplication::clipboard()->mimeData() is 0!";
0314     }
0315 
0316     QString text;
0317     if (!urls.isEmpty() || canPasteData) {
0318         // disable the paste action if no writing is supported
0319         if (!destItem.isNull()) {
0320             if (destItem.url().isEmpty()) {
0321                 *enable = false;
0322             } else {
0323                 *enable = destItem.isWritable();
0324             }
0325         } else {
0326             *enable = false;
0327         }
0328 
0329         if (urls.count() == 1 && urls.first().isLocalFile()) {
0330             const bool isDir = QFileInfo(urls.first().toLocalFile()).isDir();
0331             text = isDir ? i18nc("@action:inmenu", "Paste One Folder") : i18nc("@action:inmenu", "Paste One File");
0332         } else if (!urls.isEmpty()) {
0333             text = i18ncp("@action:inmenu", "Paste One Item", "Paste %1 Items", urls.count());
0334         } else {
0335             text = i18nc("@action:inmenu", "Paste Clipboard Contents...");
0336         }
0337     } else {
0338         *enable = false;
0339         text = i18nc("@action:inmenu", "Paste");
0340     }
0341     return text;
0342 }
0343 
0344 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 4)
0345 // The [new] main method for dropping
0346 KIOWIDGETS_EXPORT KIO::Job *KIO::pasteMimeData(const QMimeData *mimeData, const QUrl &destUrl, const QString &dialogText, QWidget *widget)
0347 {
0348     return pasteMimeDataImpl(mimeData, destUrl, dialogText, widget, false /*not clipboard*/);
0349 }
0350 #endif
0351 
0352 KIOWIDGETS_EXPORT void KIO::setClipboardDataCut(QMimeData *mimeData, bool cut)
0353 {
0354     const QByteArray cutSelectionData = cut ? "1" : "0";
0355     mimeData->setData(QStringLiteral("application/x-kde-cutselection"), cutSelectionData);
0356 }
0357 
0358 KIOWIDGETS_EXPORT bool KIO::isClipboardDataCut(const QMimeData *mimeData)
0359 {
0360     const QByteArray a = mimeData->data(QStringLiteral("application/x-kde-cutselection"));
0361     return (!a.isEmpty() && a.at(0) == '1');
0362 }