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

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