KIO

applicationlauncherjob.cpp
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(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;
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
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
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}
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
91 : KJob(parent)
92 , d(new ApplicationLauncherJobPrivate(this, {}))
93{
94}
95
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
103{
104 d->m_urls = urls;
105}
106
108{
109 d->m_runFlags = runFlags;
110}
111
113{
114 d->m_suggestedFileName = suggestedFileName;
115}
116
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
130{
131 if (!d->m_service) {
132 d->showOpenWithDialogForMimeType();
133 return;
134 }
135
136 Q_EMIT description(this, i18nc("Launching application", "Launching %1", d->m_service->name()), {}, {});
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(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 *>(this);
153 if (!untrustedProgramHandler) {
154 emitUnauthorizedError();
155 return;
156 }
157 connect(untrustedProgramHandler, &KIO::UntrustedProgramHandlerInterface::result, this, [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
165 if (untrustedProgramHandler->makeServiceFileExecutable(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(this, 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(d->m_service, d->m_runFlags, 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(d->m_numProcessesPending);
200 for (int i = 1; i < d->m_urls.count(); ++i) {
201 auto *processRunner =
202 KProcessRunner::fromApplication(d->m_service, d->m_serviceEntryPath, {d->m_urls.at(i)}, d->m_actionName, d->m_runFlags, d->m_suggestedFileName, QByteArray{});
203 d->m_processRunners.push_back(processRunner);
204 connect(processRunner, &KProcessRunner::processStarted, this, [this](qint64 pid) {
205 d->slotStarted(pid);
206 });
207 }
208 d->m_urls = {d->m_urls.at(0)};
209 } else {
210 d->m_numProcessesPending = 1;
211 }
212
213 auto *processRunner =
214 KProcessRunner::fromApplication(d->m_service, d->m_serviceEntryPath, d->m_urls, d->m_actionName, d->m_runFlags, d->m_suggestedFileName, d->m_startupId);
215 d->m_processRunners.push_back(processRunner);
216 connect(processRunner, &KProcessRunner::error, this, [this](const QString &errorText) {
217 setError(KJob::UserDefinedError);
218 setErrorText(errorText);
219 emitResult();
220 });
221 connect(processRunner, &KProcessRunner::processStarted, this, [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(this, &KJob::result, this, [&](KJob *job) {
240 loop.exit(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(d->m_processRunners.cbegin(), d->m_processRunners.cend(), [](QPointer<KProcessRunner> r) {
249 return r.isNull() || r->waitForStarted();
250 });
251 for (const auto &r : std::as_const(d->m_processRunners)) {
252 if (!r.isNull()) {
253 qApp->sendPostedEvents(r); // so slotStarted gets called
254 }
255 }
256 return ret;
257}
258
260{
261 return d->m_pids.at(0);
262}
263
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(job, &KJob::result, q, [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
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(openWithHandler, &KIO::OpenWithHandlerInterface::canceled, q, [this]() {
305 q->setError(KIO::ERR_USER_CANCELED);
306 q->emitResult();
307 });
308
309 QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::serviceSelected, q, [this](const KService::Ptr &service) {
310 Q_ASSERT(service);
311 m_service = service;
312 q->start();
313 });
314
315 QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::handled, q, [this]() {
316 q->emitResult();
317 });
318
319 openWithHandler->promptUserForApplication(q, m_urls, m_mimeTypeName);
320}
static Q_INVOKABLE bool authorize(const QString &action)
static Q_INVOKABLE bool authorizeAction(const QString &action)
QString exec() const
QString name() const
static bool isAuthorizedDesktopFile(const QString &path)
void setSuggestedFileName(const QString &suggestedFileName)
Sets the file name to use in the case of downloading the file to a tempfile in order to give to a non...
~ApplicationLauncherJob() override
Destructor.
ApplicationLauncherJob(const KService::Ptr &service, QObject *parent=nullptr)
Creates an ApplicationLauncherJob.
void setRunFlags(RunFlags runFlags)
Specifies various flags.
QFlags< RunFlag > RunFlags
Stores a combination of RunFlag values.
void setStartupId(const QByteArray &startupId)
Sets the platform-specific startup id of the application launch.
void start() override
Starts the job.
void setUrls(const QList< QUrl > &urls)
Specifies the URLs to be passed to the application.
MimeTypeFinderJob finds out the MIME type of a URL.
void serviceSelected(const KService::Ptr &service)
Emitted by promptUserForApplication() once the user chooses an application.
void handled()
Emitted by promptUserForApplication() if it fully handled it including launching the app.
void canceled()
Emitted by promptUserForApplication() if the user canceled the application selection dialog.
void result(bool confirmed)
Implementations of this interface must emit result in showUntrustedProgramWarning.
void setErrorText(const QString &errorText)
void description(KJob *job, const QString &title, const QPair< QString, QString > &field1=QPair< QString, QString >(), const QPair< QString, QString > &field2=QPair< QString, QString >())
virtual QString errorString() const
void emitResult()
int error() const
void result(KJob *job)
void setError(int errorCode)
virtual Q_SCRIPTABLE void start()=0
KJob(QObject *parent=nullptr)
QString name() const
QString exec() const
QExplicitlySharedDataPointer< KService > Ptr
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
T delegateExtension(KJob *job)
Returns the child of the job's uiDelegate() that implements the given extension, or nullptr if none w...
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
int exec(ProcessEventsFlags flags)
void exit(int returnCode)
QString canonicalFilePath() const const
QObject(QObject *parent)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QObject * parent() const const
bool isEmpty() const const
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri May 2 2025 12:02:24 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.