1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6*/
7
8#include "applicationlauncherjob.h"
9#include "../core/global.h"
10#include "jobuidelegatefactory.h"
11#include "kiogui_debug.h"
12#include "kprocessrunner_p.h"
13#include "mimetypefinderjob.h"
14#include "openwithhandlerinterface.h"
15#include "untrustedprogramhandlerinterface.h"
16
17#ifdef WITH_QTDBUS
18#include "dbusactivationrunner_p.h"
19#endif
20
21#include <KAuthorized>
22#include <KDesktopFile>
23#include <KDesktopFileAction>
24#include <KLocalizedString>
25
26#include <QFileInfo>
27#include <QPointer>
28
29class KIO::ApplicationLauncherJobPrivate
30{
31public:
32 explicit ApplicationLauncherJobPrivate(KIO::ApplicationLauncherJob *job, const KService::Ptr &service)
33 : m_service(service)
34 , q(job)
35 {
36 }
37
38 void slotStarted(qint64 pid)
39 {
40 m_pids.append(t: pid);
41 if (--m_numProcessesPending == 0) {
42 q->emitResult();
43 }
44 }
45
46 void showOpenWithDialogForMimeType();
47 void showOpenWithDialog();
48
49 KService::Ptr m_service;
50 QString m_serviceEntryPath;
51 QList<QUrl> m_urls;
52 QString m_actionName;
53 KIO::ApplicationLauncherJob::RunFlags m_runFlags;
54 QString m_suggestedFileName;
55 QString m_mimeTypeName;
56 QByteArray m_startupId;
57 QList<qint64> m_pids;
58 QList<QPointer<KProcessRunner>> m_processRunners;
59 int m_numProcessesPending = 0;
60 KIO::ApplicationLauncherJob *q;
61};
62
63KIO::ApplicationLauncherJob::ApplicationLauncherJob(const KService::Ptr &service, QObject *parent)
64 : KJob(parent)
65 , d(new ApplicationLauncherJobPrivate(this, service))
66{
67 if (d->m_service) {
68 // Cache entryPath() because we may call KService::setExec() which will clear entryPath()
69 d->m_serviceEntryPath = d->m_service->entryPath();
70 }
71}
72
73KIO::ApplicationLauncherJob::ApplicationLauncherJob(const KServiceAction &serviceAction, QObject *parent)
74 : ApplicationLauncherJob(serviceAction.service(), parent)
75{
76 Q_ASSERT(d->m_service);
77 d->m_service.detach();
78 d->m_service->setExec(serviceAction.exec());
79 d->m_actionName = serviceAction.name();
80}
81KIO::ApplicationLauncherJob::ApplicationLauncherJob(const KDesktopFileAction &desktopFileAction, QObject *parent)
82 : ApplicationLauncherJob(KService::Ptr(new KService(desktopFileAction.desktopFilePath())), parent)
83{
84 Q_ASSERT(d->m_service);
85 d->m_service.detach();
86 d->m_service->setExec(desktopFileAction.exec());
87 d->m_actionName = desktopFileAction.name();
88}
89
90KIO::ApplicationLauncherJob::ApplicationLauncherJob(QObject *parent)
91 : KJob(parent)
92 , d(new ApplicationLauncherJobPrivate(this, {}))
93{
94}
95
96KIO::ApplicationLauncherJob::~ApplicationLauncherJob()
97{
98 // Do *NOT* delete the KProcessRunner instances here.
99 // We need it to keep running so it can terminate startup notification on process exit.
100}
101
102void KIO::ApplicationLauncherJob::setUrls(const QList<QUrl> &urls)
103{
104 d->m_urls = urls;
105}
106
107void KIO::ApplicationLauncherJob::setRunFlags(RunFlags runFlags)
108{
109 d->m_runFlags = runFlags;
110}
111
112void KIO::ApplicationLauncherJob::setSuggestedFileName(const QString &suggestedFileName)
113{
114 d->m_suggestedFileName = suggestedFileName;
115}
116
117void KIO::ApplicationLauncherJob::setStartupId(const QByteArray &startupId)
118{
119 d->m_startupId = startupId;
120}
121
122void KIO::ApplicationLauncherJob::emitUnauthorizedError()
123{
124 setError(KJob::UserDefinedError);
125 setErrorText(i18n("You are not authorized to execute this file."));
126 emitResult();
127}
128
129void KIO::ApplicationLauncherJob::start()
130{
131 if (!d->m_service) {
132 d->showOpenWithDialogForMimeType();
133 return;
134 }
135
136 Q_EMIT description(job: this, i18nc("Launching application", "Launching %1", d->m_service->name()), field1: {}, field2: {});
137
138 // First, the security checks
139 if (!KAuthorized::authorize(QStringLiteral("run_desktop_files"))) {
140 // KIOSK restriction, cannot be circumvented
141 emitUnauthorizedError();
142 return;
143 }
144
145 if (!d->m_serviceEntryPath.isEmpty() && !KDesktopFile::isAuthorizedDesktopFile(path: d->m_serviceEntryPath)) {
146 // We can use QStandardPaths::findExecutable to resolve relative pathnames
147 // but that gets rid of the command line arguments.
148 QString program = QFileInfo(d->m_service->exec()).canonicalFilePath();
149 if (program.isEmpty()) { // e.g. due to command line arguments
150 program = d->m_service->exec();
151 }
152 auto *untrustedProgramHandler = KIO::delegateExtension<KIO::UntrustedProgramHandlerInterface *>(job: this);
153 if (!untrustedProgramHandler) {
154 emitUnauthorizedError();
155 return;
156 }
157 connect(sender: untrustedProgramHandler, signal: &KIO::UntrustedProgramHandlerInterface::result, context: this, slot: [this, untrustedProgramHandler](bool result) {
158 if (result) {
159 // Assume that service is an absolute path since we're being called (relative paths
160 // would have been allowed unless Kiosk said no, therefore we already know where the
161 // .desktop file is. Now add a header to it if it doesn't already have one
162 // and add the +x bit.
163
164 QString errorString;
165 if (untrustedProgramHandler->makeServiceFileExecutable(fileName: d->m_serviceEntryPath, errorString)) {
166 proceedAfterSecurityChecks();
167 } else {
168 QString serviceName = d->m_service->name();
169 if (serviceName.isEmpty()) {
170 serviceName = d->m_service->genericName();
171 }
172 setError(KJob::UserDefinedError);
173 setErrorText(i18n("Unable to make the service %1 executable, aborting execution.\n%2.", serviceName, errorString));
174 emitResult();
175 }
176 } else {
177 setError(KIO::ERR_USER_CANCELED);
178 emitResult();
179 }
180 });
181 untrustedProgramHandler->showUntrustedProgramWarning(job: this, programName: d->m_service->name());
182 return;
183 }
184 proceedAfterSecurityChecks();
185}
186
187void KIO::ApplicationLauncherJob::proceedAfterSecurityChecks()
188{
189 bool startNTimesCondition = d->m_urls.count() > 1 && !d->m_service->allowMultipleFiles();
190#ifdef WITH_QTDBUS
191 startNTimesCondition = startNTimesCondition && !DBusActivationRunner::activationPossible(service: d->m_service, flags: d->m_runFlags, suggestedFileName: d->m_suggestedFileName);
192#endif
193 if (startNTimesCondition) {
194 // We need to launch the application N times.
195 // We ignore the result for application 2 to N.
196 // For the first file we launch the application in the
197 // usual way. The reported result is based on this application.
198 d->m_numProcessesPending = d->m_urls.count();
199 d->m_processRunners.reserve(asize: d->m_numProcessesPending);
200 for (int i = 1; i < d->m_urls.count(); ++i) {
201 auto *processRunner =
202 KProcessRunner::fromApplication(service: d->m_service, serviceEntryPath: d->m_serviceEntryPath, urls: {d->m_urls.at(i)}, actionName: d->m_actionName, flags: d->m_runFlags, suggestedFileName: d->m_suggestedFileName, asn: QByteArray{});
203 d->m_processRunners.push_back(t: processRunner);
204 connect(sender: processRunner, signal: &KProcessRunner::processStarted, context: this, slot: [this](qint64 pid) {
205 d->slotStarted(pid);
206 });
207 }
208 d->m_urls = {d->m_urls.at(i: 0)};
209 } else {
210 d->m_numProcessesPending = 1;
211 }
212
213 auto *processRunner =
214 KProcessRunner::fromApplication(service: d->m_service, serviceEntryPath: d->m_serviceEntryPath, urls: d->m_urls, actionName: d->m_actionName, flags: d->m_runFlags, suggestedFileName: d->m_suggestedFileName, asn: d->m_startupId);
215 d->m_processRunners.push_back(t: processRunner);
216 connect(sender: processRunner, signal: &KProcessRunner::error, context: this, slot: [this](const QString &errorText) {
217 setError(KJob::UserDefinedError);
218 setErrorText(errorText);
219 emitResult();
220 });
221 connect(sender: processRunner, signal: &KProcessRunner::processStarted, context: this, slot: [this](qint64 pid) {
222 d->slotStarted(pid);
223 });
224}
225
226// For KRun
227bool KIO::ApplicationLauncherJob::waitForStarted()
228{
229 if (error() != KJob::NoError) {
230 return false;
231 }
232 if (d->m_processRunners.isEmpty()) {
233 // Maybe we're in the security prompt...
234 // Can't avoid the nested event loop
235 // This fork of KJob::exec doesn't set QEventLoop::ExcludeUserInputEvents
236 const bool wasAutoDelete = isAutoDelete();
237 setAutoDelete(false);
238 QEventLoop loop;
239 connect(sender: this, signal: &KJob::result, context: this, slot: [&](KJob *job) {
240 loop.exit(returnCode: job->error());
241 });
242 const int ret = loop.exec();
243 if (wasAutoDelete) {
244 deleteLater();
245 }
246 return ret != KJob::NoError;
247 }
248 const bool ret = std::all_of(first: d->m_processRunners.cbegin(), last: d->m_processRunners.cend(), pred: [](QPointer<KProcessRunner> r) {
249 return r.isNull() || r->waitForStarted();
250 });
251 for (const auto &r : std::as_const(t&: d->m_processRunners)) {
252 if (!r.isNull()) {
253 qApp->sendPostedEvents(receiver: r); // so slotStarted gets called
254 }
255 }
256 return ret;
257}
258
259qint64 KIO::ApplicationLauncherJob::pid() const
260{
261 return d->m_pids.at(i: 0);
262}
263
264QList<qint64> KIO::ApplicationLauncherJob::pids() const
265{
266 return d->m_pids;
267}
268
269void KIO::ApplicationLauncherJobPrivate::showOpenWithDialogForMimeType()
270{
271 if (m_urls.size() == 1) {
272 auto job = new KIO::MimeTypeFinderJob(m_urls[0], q);
273 job->setFollowRedirections(true);
274 job->setSuggestedFileName(m_suggestedFileName);
275 q->connect(sender: job, signal: &KJob::result, context: q, slot: [this, job]() {
276 if (!job->error()) {
277 m_mimeTypeName = job->mimeType();
278 }
279 showOpenWithDialog();
280 });
281 job->start();
282 } else {
283 showOpenWithDialog();
284 }
285}
286
287void KIO::ApplicationLauncherJobPrivate::showOpenWithDialog()
288{
289 if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) {
290 q->setError(KJob::UserDefinedError);
291 q->setErrorText(i18n("You are not authorized to select an application to open this file."));
292 q->emitResult();
293 return;
294 }
295
296 auto *openWithHandler = KIO::delegateExtension<KIO::OpenWithHandlerInterface *>(job: q);
297 if (!openWithHandler) {
298 q->setError(KJob::UserDefinedError);
299 q->setErrorText(i18n("Internal error: could not prompt the user for which application to start"));
300 q->emitResult();
301 return;
302 }
303
304 QObject::connect(sender: openWithHandler, signal: &KIO::OpenWithHandlerInterface::canceled, context: q, slot: [this]() {
305 q->setError(KIO::ERR_USER_CANCELED);
306 q->emitResult();
307 });
308
309 QObject::connect(sender: openWithHandler, signal: &KIO::OpenWithHandlerInterface::serviceSelected, context: q, slot: [this](const KService::Ptr &service) {
310 Q_ASSERT(service);
311 m_service = service;
312 q->start();
313 });
314
315 QObject::connect(sender: openWithHandler, signal: &KIO::OpenWithHandlerInterface::handled, context: q, slot: [this]() {
316 q->emitResult();
317 });
318
319 openWithHandler->promptUserForApplication(job: q, urls: m_urls, mimeType: m_mimeTypeName);
320}
321

source code of kio/src/gui/applicationlauncherjob.cpp