File indexing completed on 2024-04-21 03:55:52

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 static QUrl getDestinationUrl(const QUrl &srcUrl, const QUrl &destUrl, QWidget *widget)
0038 {
0039     KIO::StatJob *job = KIO::stat(destUrl, destUrl.isLocalFile() ? KIO::HideProgressInfo : KIO::DefaultFlags);
0040     job->setDetails(KIO::StatBasic);
0041     job->setSide(KIO::StatJob::DestinationSide);
0042     KJobWidgets::setWindow(job, widget);
0043 
0044     // Check for existing destination file.
0045     // When we were using CopyJob, we couldn't let it do that (would expose
0046     // an ugly tempfile name as the source URL)
0047     // And now we're using a put job anyway, no destination checking included.
0048     if (job->exec()) {
0049         KIO::RenameDialog dlg(widget, i18n("File Already Exists"), srcUrl, destUrl, KIO::RenameDialog_Overwrite);
0050         KIO::RenameDialog_Result res = static_cast<KIO::RenameDialog_Result>(dlg.exec());
0051 
0052         if (res == KIO::Result_Rename) {
0053             return dlg.newDestUrl();
0054         } else if (res == KIO::Result_Cancel) {
0055             return QUrl();
0056         } else if (res == KIO::Result_Overwrite) {
0057             return destUrl;
0058         }
0059     }
0060 
0061     return destUrl;
0062 }
0063 
0064 static QUrl getNewFileName(const QUrl &u, const QString &text, const QString &suggestedFileName, QWidget *widget)
0065 {
0066     bool ok;
0067     QString dialogText(text);
0068     if (dialogText.isEmpty()) {
0069         dialogText = i18n("Filename for clipboard content:");
0070     }
0071     QString file = QInputDialog::getText(widget, QString(), dialogText, QLineEdit::Normal, suggestedFileName, &ok);
0072     if (!ok) {
0073         return QUrl();
0074     }
0075 
0076     QUrl myurl(u);
0077     myurl.setPath(Utils::concatPaths(myurl.path(), file));
0078 
0079     return getDestinationUrl(u, myurl, widget);
0080 }
0081 
0082 static KIO::Job *putDataAsyncTo(const QUrl &url, const QByteArray &data, QWidget *widget, KIO::JobFlags flags)
0083 {
0084     KIO::Job *job = KIO::storedPut(data, url, -1, flags);
0085     QObject::connect(job, &KIO::Job::result, [url](KJob *job) {
0086         if (job->error() == KJob::NoError) {
0087             org::kde::KDirNotify::emitFilesAdded(url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
0088         }
0089     });
0090     KJobWidgets::setWindow(job, widget);
0091     return job;
0092 }
0093 
0094 static QByteArray chooseFormatAndUrl(const QUrl &u,
0095                                      const QMimeData *mimeData,
0096                                      const QStringList &formats,
0097                                      const QString &text,
0098                                      const QString &suggestedFileName,
0099                                      QWidget *widget,
0100                                      bool clipboard,
0101                                      QUrl *newUrl)
0102 {
0103     QMimeDatabase db;
0104     QStringList formatLabels;
0105     formatLabels.reserve(formats.size());
0106     for (int i = 0; i < formats.size(); ++i) {
0107         const QString &fmt = formats[i];
0108         QMimeType mime = db.mimeTypeForName(fmt);
0109         if (mime.isValid()) {
0110             formatLabels.append(i18n("%1 (%2)", mime.comment(), fmt));
0111         } else {
0112             formatLabels.append(fmt);
0113         }
0114     }
0115 
0116     QString dialogText(text);
0117     if (dialogText.isEmpty()) {
0118         dialogText = i18n("Filename for clipboard content:");
0119     }
0120 
0121     KIO::PasteDialog dlg(QString(), dialogText, suggestedFileName, formatLabels, widget);
0122 
0123     if (dlg.exec() != QDialog::Accepted) {
0124         return QByteArray();
0125     }
0126 
0127     const QString chosenFormat = formats[dlg.comboItem()];
0128     if (clipboard && !qApp->clipboard()->mimeData()->hasFormat(chosenFormat)) {
0129         KMessageBox::information(widget,
0130                                  i18n("The clipboard has changed since you used 'paste': "
0131                                       "the chosen data format is no longer applicable. "
0132                                       "Please copy again what you wanted to paste."));
0133         return QByteArray();
0134     }
0135 
0136     const QString result = dlg.lineEditText();
0137 
0138     // qDebug() << " result=" << result << " chosenFormat=" << chosenFormat;
0139     *newUrl = u;
0140     newUrl->setPath(Utils::concatPaths(newUrl->path(), result));
0141 
0142     const QUrl destUrl = getDestinationUrl(u, *newUrl, widget);
0143     *newUrl = destUrl;
0144 
0145     // In Qt3, the result of clipboard()->mimeData() only existed until the next
0146     // event loop run (see dlg.exec() above), so we re-fetched it.
0147     // TODO: This should not be necessary with Qt5; remove this conditional
0148     // and test that it still works.
0149     if (clipboard) {
0150         mimeData = QApplication::clipboard()->mimeData();
0151     }
0152     const QByteArray ba = mimeData->data(chosenFormat);
0153     return ba;
0154 }
0155 
0156 static QStringList extractFormats(const QMimeData *mimeData)
0157 {
0158     QStringList formats;
0159     const QStringList allFormats = mimeData->formats();
0160     for (const QString &format : allFormats) {
0161         if (format == QLatin1String("application/x-qiconlist")) { // Q3IconView and kde4's libkonq
0162             continue;
0163         }
0164         if (format == QLatin1String("application/x-kde-cutselection")) { // see isClipboardDataCut
0165             continue;
0166         }
0167         if (format == QLatin1String("application/x-kde-suggestedfilename")) {
0168             continue;
0169         }
0170         if (format.startsWith(QLatin1String("application/x-qt-"))) { // Qt-internal
0171             continue;
0172         }
0173         if (format.startsWith(QLatin1String("x-kmail-drag/"))) { // app-internal
0174             continue;
0175         }
0176         if (!format.contains(QLatin1Char('/'))) { // e.g. TARGETS, MULTIPLE, TIMESTAMP
0177             continue;
0178         }
0179         formats.append(format);
0180     }
0181     return formats;
0182 }
0183 
0184 KIOWIDGETS_EXPORT bool KIO::canPasteMimeData(const QMimeData *data)
0185 {
0186     return data->hasText() || !extractFormats(data).isEmpty();
0187 }
0188 
0189 KIO::Job *pasteMimeDataImpl(const QMimeData *mimeData, const QUrl &destUrl, const QString &dialogText, QWidget *widget, bool clipboard)
0190 {
0191     QByteArray ba;
0192     const QString suggestedFilename = QString::fromUtf8(mimeData->data(QStringLiteral("application/x-kde-suggestedfilename")));
0193 
0194     // Now check for plain text
0195     // We don't want to display a MIME type choice for a QTextDrag, those MIME type look ugly.
0196     if (mimeData->hasText()) {
0197         ba = mimeData->text().toLocal8Bit(); // encoding OK?
0198     } else {
0199         const QStringList formats = extractFormats(mimeData);
0200         if (formats.isEmpty()) {
0201             return nullptr;
0202         } else if (formats.size() > 1) {
0203             QUrl newUrl;
0204             ba = chooseFormatAndUrl(destUrl, mimeData, formats, dialogText, suggestedFilename, widget, clipboard, &newUrl);
0205             if (ba.isEmpty() || newUrl.isEmpty()) {
0206                 return nullptr;
0207             }
0208             return putDataAsyncTo(newUrl, ba, widget, KIO::Overwrite);
0209         }
0210         ba = mimeData->data(formats.first());
0211     }
0212     if (ba.isEmpty()) {
0213         return nullptr;
0214     }
0215 
0216     const QUrl newUrl = getNewFileName(destUrl, dialogText, suggestedFilename, widget);
0217     if (newUrl.isEmpty()) {
0218         return nullptr;
0219     }
0220 
0221     return putDataAsyncTo(newUrl, ba, widget, KIO::Overwrite);
0222 }
0223 
0224 KIOWIDGETS_EXPORT QString KIO::pasteActionText(const QMimeData *mimeData, bool *enable, const KFileItem &destItem)
0225 {
0226     bool canPasteData = false;
0227     QList<QUrl> urls;
0228 
0229     // mimeData can be 0 according to https://bugs.kde.org/show_bug.cgi?id=335053
0230     if (mimeData) {
0231         canPasteData = KIO::canPasteMimeData(mimeData);
0232         urls = KUrlMimeData::urlsFromMimeData(mimeData);
0233     } else {
0234         qCWarning(KIO_WIDGETS) << "QApplication::clipboard()->mimeData() is 0!";
0235     }
0236 
0237     QString text;
0238     if (!urls.isEmpty() || canPasteData) {
0239         // disable the paste action if no writing is supported
0240         if (!destItem.isNull()) {
0241             if (destItem.url().isEmpty()) {
0242                 *enable = false;
0243             } else {
0244                 *enable = destItem.isWritable();
0245             }
0246         } else {
0247             *enable = false;
0248         }
0249 
0250         if (urls.count() == 1 && urls.first().isLocalFile()) {
0251             const bool isDir = QFileInfo(urls.first().toLocalFile()).isDir();
0252             text = isDir ? i18nc("@action:inmenu", "Paste One Folder") : i18nc("@action:inmenu", "Paste One File");
0253         } else if (!urls.isEmpty()) {
0254             text = i18ncp("@action:inmenu", "Paste One Item", "Paste %1 Items", urls.count());
0255         } else {
0256             text = i18nc("@action:inmenu", "Paste Clipboard Contents...");
0257         }
0258     } else {
0259         *enable = false;
0260         text = i18nc("@action:inmenu", "Paste");
0261     }
0262     return text;
0263 }
0264 
0265 KIOWIDGETS_EXPORT void KIO::setClipboardDataCut(QMimeData *mimeData, bool cut)
0266 {
0267     const QByteArray cutSelectionData = cut ? "1" : "0";
0268     mimeData->setData(QStringLiteral("application/x-kde-cutselection"), cutSelectionData);
0269 }
0270 
0271 KIOWIDGETS_EXPORT bool KIO::isClipboardDataCut(const QMimeData *mimeData)
0272 {
0273     const QByteArray a = mimeData->data(QStringLiteral("application/x-kde-cutselection"));
0274     return (!a.isEmpty() && a.at(0) == '1');
0275 }