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 | |
44 | KIOExec::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 | |
128 | void 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 | |
175 | void 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 | |
296 | int 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 | |