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

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