1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 1998, 1999 Torben Weis <weis@kde.org>
4 SPDX-FileCopyrightText: 2000-2005 David Faure <faure@kde.org>
5 SPDX-FileCopyrightText: 2001 Waldo Bastian <bastian@kde.org>
6
7 SPDX-License-Identifier: GPL-2.0-or-later
8*/
9
10#include "main.h"
11#include "filecopyjob.h"
12#include "kio_version.h"
13#include "kioexecdebug.h"
14#include "kioexecdinterface.h"
15#include "statjob.h"
16
17#include <QDir>
18#include <QFile>
19
20#include <KAboutData>
21#include <KDBusService>
22#include <KLocalizedString>
23#include <KMessageBox>
24#include <KService>
25#include <QApplication>
26#include <QDebug>
27#include <copyjob.h>
28#include <desktopexecparser.h>
29#include <job.h>
30
31#include <QCommandLineOption>
32#include <QCommandLineParser>
33#include <QFileInfo>
34#include <QStandardPaths>
35#include <QThread>
36
37#include <config-kioexec.h>
38
39#if HAVE_X11
40#include <KStartupInfo>
41#include <private/qtx11extras_p.h>
42#endif
43
44KIOExec::KIOExec(const QStringList &args, bool tempFiles, const QString &suggestedFileName)
45 : mExited(false)
46 , mTempFiles(tempFiles)
47 , mUseDaemon(false)
48 , mSuggestedFileName(suggestedFileName)
49 , expectedCounter(0)
50 , command(args.first())
51 , jobCounter(0)
52{
53 qCDebug(KIOEXEC) << "command=" << command << "args=" << args;
54
55 for (int i = 1; i < args.count(); i++) {
56 const QUrl urlArg = QUrl::fromUserInput(userInput: args.value(i));
57 if (!urlArg.isValid()) {
58 KMessageBox::error(parent: nullptr, i18n("Invalid URL: %1", args.value(i)));
59 exit(status: 1);
60 }
61 KIO::StatJob *mostlocal = KIO::mostLocalUrl(url: urlArg);
62 bool b = mostlocal->exec();
63 if (!b) {
64 KMessageBox::error(parent: nullptr, i18n("File not found: %1", urlArg.toDisplayString()));
65 exit(status: 1);
66 }
67 Q_ASSERT(b);
68 const QUrl url = mostlocal->mostLocalUrl();
69
70 // kDebug() << "url=" << url.url() << " filename=" << url.fileName();
71 // A local file, not an URL ?
72 // => It is not encoded and not shell escaped, too.
73 if (url.isLocalFile()) {
74 FileInfo file;
75 file.path = url.toLocalFile();
76 file.url = url;
77 fileList.append(t: file);
78 } else {
79 // It is an URL
80 if (!url.isValid()) {
81 KMessageBox::error(parent: nullptr, i18n("The URL %1\nis malformed", url.url()));
82 } else if (mTempFiles) {
83 KMessageBox::error(parent: nullptr, i18n("Remote URL %1\nnot allowed with --tempfiles switch", url.toDisplayString()));
84 } else {
85 // We must fetch the file
86 QString fileName = KIO::encodeFileName(str: url.fileName());
87 if (!suggestedFileName.isEmpty()) {
88 fileName = suggestedFileName;
89 }
90 if (fileName.isEmpty()) {
91 fileName = QStringLiteral("unnamed");
92 }
93 // Build the destination filename, in ~/.cache/kioexec/krun/
94 // Unlike KDE-1.1, we put the filename at the end so that the extension is kept
95 // (Some programs rely on it)
96 QString krun_writable = QStandardPaths::writableLocation(type: QStandardPaths::CacheLocation)
97 + QStringLiteral("/krun/%1_%2/").arg(a: QCoreApplication::applicationPid()).arg(a: jobCounter++);
98 QDir().mkpath(dirPath: krun_writable); // error handling will be done by the job
99 QString tmp = krun_writable + fileName;
100 FileInfo file;
101 file.path = tmp;
102 file.url = url;
103 fileList.append(t: file);
104
105 expectedCounter++;
106 const QUrl dest = QUrl::fromLocalFile(localfile: tmp);
107 qCDebug(KIOEXEC) << "Copying" << url << "to" << dest;
108 KIO::Job *job = KIO::file_copy(src: url, dest);
109 jobList.append(t: job);
110
111 connect(sender: job, signal: &KJob::result, context: this, slot: &KIOExec::slotResult);
112 }
113 }
114 }
115
116 if (mTempFiles) {
117 // delay call so QApplication::exit passes the exit code to exec()
118 QTimer::singleShot(interval: 0, receiver: this, slot: &KIOExec::slotRunApp);
119 return;
120 }
121
122 counter = 0;
123 if (counter == expectedCounter) {
124 slotResult(nullptr);
125 }
126}
127
128void KIOExec::slotResult(KJob *job)
129{
130 if (job) {
131 KIO::FileCopyJob *copyJob = static_cast<KIO::FileCopyJob *>(job);
132 const QString path = copyJob->destUrl().path();
133
134 if (job->error()) {
135 // That error dialog would be queued, i.e. not immediate...
136 // job->showErrorDialog();
137 if (job->error() != KIO::ERR_USER_CANCELED) {
138 KMessageBox::error(parent: nullptr, text: job->errorString());
139 }
140
141 auto it = std::find_if(first: fileList.begin(), last: fileList.end(), pred: [&path](const FileInfo &i) {
142 return i.path == path;
143 });
144 if (it != fileList.end()) {
145 fileList.erase(pos: it);
146 } else {
147 qCDebug(KIOEXEC) << path << "not found in list";
148 }
149 } else {
150 // Tell kioexecd to watch the file for changes.
151 const QString dest = copyJob->srcUrl().toString();
152 qCDebug(KIOEXEC) << "Telling kioexecd to watch path" << path << "dest" << dest;
153 OrgKdeKIOExecdInterface kioexecd(QStringLiteral("org.kde.kioexecd6"), QStringLiteral("/modules/kioexecd"), QDBusConnection::sessionBus());
154 kioexecd.watch(path, destUrl: dest);
155 mUseDaemon = !kioexecd.lastError().isValid();
156 if (!mUseDaemon) {
157 qCDebug(KIOEXEC) << "Not using kioexecd";
158 }
159 }
160 }
161
162 counter++;
163
164 if (counter < expectedCounter) {
165 return;
166 }
167
168 qCDebug(KIOEXEC) << "All files downloaded, will call slotRunApp shortly";
169 // We know we can run the app now - but let's finish the job properly first.
170 QTimer::singleShot(interval: 0, receiver: this, slot: &KIOExec::slotRunApp);
171
172 jobList.clear();
173}
174
175void KIOExec::slotRunApp()
176{
177 if (fileList.isEmpty()) {
178 qCDebug(KIOEXEC) << "No files downloaded -> exiting";
179 mExited = true;
180 QApplication::exit(retcode: 1);
181 return;
182 }
183
184 KService service(QStringLiteral("dummy"), command, QString());
185
186 QList<QUrl> list;
187 list.reserve(asize: fileList.size());
188 // Store modification times
189 QList<FileInfo>::Iterator it = fileList.begin();
190 for (; it != fileList.end(); ++it) {
191 QFileInfo info(it->path);
192 it->time = info.lastModified();
193 QUrl url = QUrl::fromLocalFile(localfile: it->path);
194 list << url;
195 }
196
197 KIO::DesktopExecParser execParser(service, list);
198 QStringList params = execParser.resultingArguments();
199 if (params.isEmpty()) {
200 qWarning() << execParser.errorMessage();
201 QApplication::exit(retcode: -1);
202 return;
203 }
204
205 qCDebug(KIOEXEC) << "EXEC" << params.join(sep: QLatin1Char(' '));
206
207#if HAVE_X11
208 // propagate the startup identification to the started process
209 KStartupInfoId id;
210 QByteArray startupId;
211 if (QX11Info::isPlatformX11()) {
212 startupId = QX11Info::nextStartupId();
213 }
214 id.initId(id: startupId);
215 id.setupStartupEnv();
216#endif
217
218 QString exe(params.takeFirst());
219 const int exit_code = QProcess::execute(program: exe, arguments: params);
220
221#if HAVE_X11
222 KStartupInfo::resetStartupEnv();
223#endif
224
225 qCDebug(KIOEXEC) << "EXEC done";
226
227 QStringList tempFilesToRemove;
228
229 // Test whether one of the files changed
230 for (it = fileList.begin(); it != fileList.end(); ++it) {
231 QString src = it->path;
232 const QUrl dest = it->url;
233 QFileInfo info(src);
234 const bool uploadChanges = !mUseDaemon && !dest.isLocalFile();
235 if (info.exists() && (it->time != info.lastModified())) {
236 if (mTempFiles) {
237 const auto result = KMessageBox::questionTwoActions(
238 parent: nullptr,
239 i18n("The supposedly temporary file\n%1\nhas been modified.\nDo you still want to delete it?", dest.toDisplayString(QUrl::PreferLocalFile)),
240 i18n("File Changed"),
241 primaryAction: KStandardGuiItem::del(),
242 secondaryAction: KGuiItem(i18n("Do Not Delete")));
243 if (result != KMessageBox::PrimaryAction) {
244 continue; // don't delete the temp file
245 }
246 } else if (uploadChanges) { // no upload when it's already a local file or kioexecd already did it.
247 const auto result =
248 KMessageBox::questionTwoActions(parent: nullptr,
249 i18n("The file\n%1\nhas been modified.\nDo you want to upload the changes?", dest.toDisplayString()),
250 i18n("File Changed"),
251 primaryAction: KGuiItem(i18n("Upload")),
252 secondaryAction: KGuiItem(i18n("Do Not Upload")));
253 if (result == KMessageBox::PrimaryAction) {
254 qCDebug(KIOEXEC) << "src=" << src << "dest=" << dest;
255 // Do it the synchronous way.
256 KIO::CopyJob *job = KIO::copy(src: QUrl::fromLocalFile(localfile: src), dest);
257 if (!job->exec()) {
258 KMessageBox::error(parent: nullptr, text: job->errorText());
259 continue; // don't delete the temp file
260 }
261 }
262 }
263 }
264
265 if ((uploadChanges || mTempFiles) && exit_code == 0) {
266 // Note that a temp file needs to be removed later
267 tempFilesToRemove.append(t: src);
268 }
269 }
270
271 if (!tempFilesToRemove.isEmpty()) {
272 // Wait for a reasonable time so that even if the application forks
273 // on startup (like OOo or amarok) it will have time to start up and
274 // read the file before it gets deleted. #130709.
275 const int sleepSecs = 180;
276 qCDebug(KIOEXEC) << "sleeping for" << sleepSecs << "seconds before deleting" << tempFilesToRemove.count() << "temp files...";
277 QThread::sleep(sleepSecs);
278 qCDebug(KIOEXEC) << sleepSecs << "seconds have passed, deleting temp files";
279
280 for (const QString &src : std::as_const(t&: tempFilesToRemove)) {
281 QFileInfo info(src);
282 const QString parentDir = info.path();
283 qCDebug(KIOEXEC) << "deleting" << info.filePath();
284 QFile(src).remove();
285 // NOTE: this is not necessarily a temporary directory.
286 if (QDir().rmdir(dirName: parentDir)) {
287 qCDebug(KIOEXEC) << "Removed empty parent directory" << parentDir;
288 }
289 }
290 }
291
292 mExited = true;
293 QApplication::exit(retcode: exit_code);
294}
295
296int main(int argc, char **argv)
297{
298 QApplication app(argc, argv);
299 KAboutData aboutData(QStringLiteral("kioexec"),
300 i18n("KIOExec"),
301 QStringLiteral(KIO_VERSION_STRING),
302 i18n("KIO Exec - Opens remote files, watches modifications, asks for upload"),
303 KAboutLicense::GPL,
304 i18n("(c) 1998-2000,2003 The KFM/Konqueror Developers"));
305 aboutData.addAuthor(i18n("David Faure"), task: QString(), QStringLiteral("faure@kde.org"));
306 aboutData.addAuthor(i18n("Stephan Kulow"), task: QString(), QStringLiteral("coolo@kde.org"));
307 aboutData.addAuthor(i18n("Bernhard Rosenkraenzer"), task: QString(), QStringLiteral("bero@arklinux.org"));
308 aboutData.addAuthor(i18n("Waldo Bastian"), task: QString(), QStringLiteral("bastian@kde.org"));
309 aboutData.addAuthor(i18n("Oswald Buddenhagen"), task: QString(), QStringLiteral("ossi@kde.org"));
310 KAboutData::setApplicationData(aboutData);
311 KDBusService service(KDBusService::Multiple);
312
313 QCommandLineParser parser;
314 parser.addOption(commandLineOption: QCommandLineOption(QStringList{QStringLiteral("tempfiles")}, i18n("Treat URLs as local files and delete them afterwards")));
315 parser.addOption(
316 commandLineOption: QCommandLineOption(QStringList{QStringLiteral("suggestedfilename")}, i18n("Suggested file name for the downloaded file"), QStringLiteral("filename")));
317 parser.addPositionalArgument(QStringLiteral("command"), i18n("Command to execute"));
318 parser.addPositionalArgument(QStringLiteral("urls"), i18n("URL(s) or local file(s) used for 'command'"));
319
320 app.setQuitOnLastWindowClosed(false);
321
322 aboutData.setupCommandLine(&parser);
323 parser.process(app);
324 aboutData.processCommandLine(parser: &parser);
325
326 if (parser.positionalArguments().count() < 1) {
327 parser.showHelp(exitCode: -1);
328 return -1;
329 }
330
331 const bool tempfiles = parser.isSet(QStringLiteral("tempfiles"));
332 const QString suggestedfilename = parser.value(QStringLiteral("suggestedfilename"));
333 KIOExec exec(parser.positionalArguments(), tempfiles, suggestedfilename);
334
335 // Don't go into the event loop if we already want to exit (#172197)
336 if (exec.exited()) {
337 return 0;
338 }
339
340 return app.exec();
341}
342
343#include "moc_main.cpp"
344

source code of kio/src/kioexec/main.cpp