1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-only
6*/
7
8#include "paste.h"
9#include "kio_widgets_debug.h"
10
11#include "../utils_p.h"
12#include "kio/copyjob.h"
13#include "kio/deletejob.h"
14#include "kio/global.h"
15#include "kio/renamedialog.h"
16#include "kio/statjob.h"
17#include "pastedialog_p.h"
18#include <kdirnotify.h>
19#include <kfileitem.h>
20#include <kfileitemlistproperties.h>
21#include <kio/storedtransferjob.h>
22
23#include <KJobWidgets>
24#include <KLocalizedString>
25#include <KMessageBox>
26#include <KUrlMimeData>
27
28#include <QApplication>
29#include <QClipboard>
30#include <QDebug>
31#include <QFileInfo>
32#include <QInputDialog>
33#include <QMimeData>
34#include <QTemporaryFile>
35
36static QUrl getDestinationUrl(const QUrl &srcUrl, const QUrl &destUrl, QWidget *widget)
37{
38 KIO::StatJob *job = KIO::stat(url: destUrl, flags: destUrl.isLocalFile() ? KIO::HideProgressInfo : KIO::DefaultFlags);
39 job->setDetails(KIO::StatBasic);
40 job->setSide(KIO::StatJob::DestinationSide);
41 KJobWidgets::setWindow(job, widget);
42
43 // Check for existing destination file.
44 // When we were using CopyJob, we couldn't let it do that (would expose
45 // an ugly tempfile name as the source URL)
46 // And now we're using a put job anyway, no destination checking included.
47 if (job->exec()) {
48 KIO::RenameDialog dlg(widget, i18n("File Already Exists"), srcUrl, destUrl, KIO::RenameDialog_Overwrite);
49 KIO::RenameDialog_Result res = static_cast<KIO::RenameDialog_Result>(dlg.exec());
50
51 if (res == KIO::Result_Rename) {
52 return dlg.newDestUrl();
53 } else if (res == KIO::Result_Cancel) {
54 return QUrl();
55 } else if (res == KIO::Result_Overwrite) {
56 return destUrl;
57 }
58 }
59
60 return destUrl;
61}
62
63static QUrl getNewFileName(const QUrl &u, const QString &text, const QString &suggestedFileName, QWidget *widget)
64{
65 bool ok;
66 QString dialogText(text);
67 if (dialogText.isEmpty()) {
68 dialogText = i18n("Filename for clipboard content:");
69 }
70 QString file = QInputDialog::getText(parent: widget, title: QString(), label: dialogText, echo: QLineEdit::Normal, text: suggestedFileName, ok: &ok);
71 if (!ok) {
72 return QUrl();
73 }
74
75 QUrl myurl(u);
76 myurl.setPath(path: Utils::concatPaths(path1: myurl.path(), path2: file));
77
78 return getDestinationUrl(srcUrl: u, destUrl: myurl, widget);
79}
80
81static KIO::Job *putDataAsyncTo(const QUrl &url, const QByteArray &data, QWidget *widget, KIO::JobFlags flags)
82{
83 KIO::Job *job = KIO::storedPut(arr: data, url, permissions: -1, flags);
84 QObject::connect(sender: job, signal: &KIO::Job::result, slot: [url](KJob *job) {
85 if (job->error() == KJob::NoError) {
86#ifdef WITH_QTDBUS
87 org::kde::KDirNotify::emitFilesAdded(directory: url.adjusted(options: QUrl::RemoveFilename | QUrl::StripTrailingSlash));
88#endif
89 }
90 });
91 KJobWidgets::setWindow(job, widget);
92 return job;
93}
94
95static QByteArray chooseFormatAndUrl(const QUrl &u,
96 const QMimeData *mimeData,
97 const QStringList &formats,
98 const QString &text,
99 const QString &suggestedFileName,
100 QWidget *widget,
101 bool clipboard,
102 QUrl *newUrl)
103{
104 QString dialogText(text);
105 if (dialogText.isEmpty()) {
106 dialogText = i18n("Filename for clipboard content:");
107 }
108
109 auto defaultFilename = suggestedFileName;
110 if (defaultFilename.isEmpty()) {
111 defaultFilename = i18nc("A default file name excluding extension for some pasted content", "pasted file");
112 }
113
114 KIO::PasteDialog dlg(QString(), dialogText, defaultFilename, formats, widget);
115
116 if (dlg.exec() != QDialog::Accepted) {
117 return QByteArray();
118 }
119
120 const QString chosenFormat = formats[dlg.comboItem()];
121 if (clipboard && !qApp->clipboard()->mimeData()->hasFormat(mimetype: chosenFormat)) {
122 KMessageBox::information(parent: widget,
123 i18n("The clipboard has changed since you used 'paste': "
124 "the chosen data format is no longer applicable. "
125 "Please copy again what you wanted to paste."));
126 return QByteArray();
127 }
128
129 const QString result = dlg.lineEditText();
130
131 // qDebug() << " result=" << result << " chosenFormat=" << chosenFormat;
132 *newUrl = u;
133 newUrl->setPath(path: Utils::concatPaths(path1: newUrl->path(), path2: result));
134
135 const QUrl destUrl = getDestinationUrl(srcUrl: u, destUrl: *newUrl, widget);
136 *newUrl = destUrl;
137
138 // In Qt3, the result of clipboard()->mimeData() only existed until the next
139 // event loop run (see dlg.exec() above), so we re-fetched it.
140 // TODO: This should not be necessary with Qt5; remove this conditional
141 // and test that it still works.
142 if (clipboard) {
143 mimeData = QApplication::clipboard()->mimeData();
144 }
145 const QByteArray ba = mimeData->data(mimetype: chosenFormat);
146 return ba;
147}
148
149static QStringList extractFormats(const QMimeData *mimeData)
150{
151 QStringList formats;
152 const QStringList allFormats = mimeData->formats();
153 for (const QString &format : allFormats) {
154 if (format == QLatin1String("application/x-qiconlist")) { // Q3IconView and kde4's libkonq
155 continue;
156 }
157 if (format == QLatin1String("application/x-kde-cutselection")) { // see isClipboardDataCut
158 continue;
159 }
160 if (format == QLatin1String("application/x-kde-suggestedfilename")) {
161 continue;
162 }
163 if (format == QLatin1String("application/x-kde-onlyReplaceEmpty")) { // Prevents emptying Klipper via selection
164 continue;
165 }
166 if (format.startsWith(s: QLatin1String("application/x-qt-"))) { // Qt-internal
167 continue;
168 }
169 if (format.startsWith(s: QLatin1String("x-kmail-drag/"))) { // app-internal
170 continue;
171 }
172 if (!format.contains(c: QLatin1Char('/'))) { // e.g. TARGETS, MULTIPLE, TIMESTAMP
173 continue;
174 }
175 formats.append(t: format);
176 }
177 return formats;
178}
179
180KIOWIDGETS_EXPORT bool KIO::canPasteMimeData(const QMimeData *data)
181{
182 return data->hasText() || !extractFormats(mimeData: data).isEmpty();
183}
184
185KIO::Job *pasteMimeDataImpl(const QMimeData *mimeData, const QUrl &destUrl, const QString &dialogText, QWidget *widget, bool clipboard)
186{
187 QByteArray ba;
188 const QString suggestedFilename = QString::fromUtf8(ba: mimeData->data(QStringLiteral("application/x-kde-suggestedfilename")));
189
190 // Now check for plain text
191 // We don't want to display a MIME type choice for a QTextDrag, those MIME type look ugly.
192 if (mimeData->hasText()) {
193 ba = mimeData->text().toLocal8Bit(); // encoding OK?
194 } else {
195 auto formats = extractFormats(mimeData);
196 const auto firstFormat = formats.value(i: 0);
197 // Remove formats that shouldn't be exposed to the user
198 erase_if(list&: formats, pred: [](const QString &string) -> bool {
199 return string.startsWith(s: u"application/x-kde-");
200 });
201 if (formats.isEmpty() && firstFormat.isEmpty()) {
202 return nullptr;
203 } else if (formats.size() > 1) {
204 QUrl newUrl;
205 ba = chooseFormatAndUrl(u: destUrl, mimeData, formats, text: dialogText, suggestedFileName: suggestedFilename, widget, clipboard, newUrl: &newUrl);
206 if (ba.isEmpty() || newUrl.isEmpty()) {
207 return nullptr;
208 }
209 return putDataAsyncTo(url: newUrl, data: ba, widget, flags: KIO::Overwrite);
210 }
211 ba = mimeData->data(mimetype: firstFormat);
212 }
213 if (ba.isEmpty()) {
214 return nullptr;
215 }
216
217 const QUrl newUrl = getNewFileName(u: destUrl, text: dialogText, suggestedFileName: suggestedFilename, widget);
218 if (newUrl.isEmpty()) {
219 return nullptr;
220 }
221
222 return putDataAsyncTo(url: newUrl, data: ba, widget, flags: KIO::Overwrite);
223}
224
225KIOWIDGETS_EXPORT QString KIO::pasteActionText(const QMimeData *mimeData, bool *enable, const KFileItem &destItem)
226{
227 bool canPasteData = false;
228 QList<QUrl> urls;
229
230 // mimeData can be 0 according to https://bugs.kde.org/show_bug.cgi?id=335053
231 if (mimeData) {
232 canPasteData = KIO::canPasteMimeData(data: mimeData);
233 urls = KUrlMimeData::urlsFromMimeData(mimeData);
234 } else {
235 qCWarning(KIO_WIDGETS) << "QApplication::clipboard()->mimeData() is 0!";
236 }
237
238 QString text;
239 if (!urls.isEmpty() || canPasteData) {
240 // disable the paste action if no writing is supported
241 if (!destItem.isNull()) {
242 if (destItem.url().isEmpty()) {
243 *enable = false;
244 } else {
245 *enable = destItem.isWritable();
246 }
247 } else {
248 *enable = false;
249 }
250
251 if (urls.count() == 1 && urls.first().isLocalFile()) {
252 const bool isDir = QFileInfo(urls.first().toLocalFile()).isDir();
253 text = isDir ? i18nc("@action:inmenu", "Paste One Folder") : i18nc("@action:inmenu", "Paste One File");
254 } else if (!urls.isEmpty()) {
255 text = i18ncp("@action:inmenu", "Paste One Item", "Paste %1 Items", urls.count());
256 } else {
257 text = i18nc("@action:inmenu", "Paste Clipboard Contents…");
258 }
259 } else {
260 *enable = false;
261 text = i18nc("@action:inmenu", "Paste");
262 }
263 return text;
264}
265
266KIOWIDGETS_EXPORT void KIO::setClipboardDataCut(QMimeData *mimeData, bool cut)
267{
268 const QByteArray cutSelectionData = cut ? "1" : "0";
269 mimeData->setData(QStringLiteral("application/x-kde-cutselection"), data: cutSelectionData);
270}
271
272KIOWIDGETS_EXPORT bool KIO::isClipboardDataCut(const QMimeData *mimeData)
273{
274 const QByteArray a = mimeData->data(QStringLiteral("application/x-kde-cutselection"));
275 return (!a.isEmpty() && a.at(i: 0) == '1');
276}
277

source code of kio/src/widgets/paste.cpp