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 "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(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;
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;
57};
58
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
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}
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
85 : KJob(parent)
86 , d(new ApplicationLauncherJobPrivate(this, {}))
87{
88}
89
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
97{
98 d->m_urls = urls;
99}
100
102{
103 d->m_runFlags = runFlags;
104}
105
107{
108 d->m_suggestedFileName = suggestedFileName;
109}
110
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
124{
125 if (!d->m_service) {
126 d->showOpenWithDialogForMimeType();
127 return;
128 }
129
130 Q_EMIT description(this, i18nc("Launching application", "Launching %1", d->m_service->name()), {}, {});
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(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 *>(this);
147 if (!untrustedProgramHandler) {
148 emitUnauthorizedError();
149 return;
150 }
151 connect(untrustedProgramHandler, &KIO::UntrustedProgramHandlerInterface::result, this, [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(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(this, 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(d->m_service, d->m_runFlags, 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(d->m_numProcessesPending);
191 for (int i = 1; i < d->m_urls.count(); ++i) {
192 auto *processRunner =
193 KProcessRunner::fromApplication(d->m_service, d->m_serviceEntryPath, {d->m_urls.at(i)}, d->m_runFlags, d->m_suggestedFileName, QByteArray{});
194 d->m_processRunners.push_back(processRunner);
195 connect(processRunner, &KProcessRunner::processStarted, this, [this](qint64 pid) {
196 d->slotStarted(pid);
197 });
198 }
199 d->m_urls = {d->m_urls.at(0)};
200 } else {
201 d->m_numProcessesPending = 1;
202 }
203
204 auto *processRunner =
205 KProcessRunner::fromApplication(d->m_service, d->m_serviceEntryPath, d->m_urls, d->m_runFlags, d->m_suggestedFileName, d->m_startupId);
206 d->m_processRunners.push_back(processRunner);
207 connect(processRunner, &KProcessRunner::error, this, [this](const QString &errorText) {
208 setError(KJob::UserDefinedError);
209 setErrorText(errorText);
210 emitResult();
211 });
212 connect(processRunner, &KProcessRunner::processStarted, this, [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(this, &KJob::result, this, [&](KJob *job) {
231 loop.exit(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(d->m_processRunners.cbegin(), d->m_processRunners.cend(), [](QPointer<KProcessRunner> r) {
240 return r.isNull() || r->waitForStarted();
241 });
242 for (const auto &r : std::as_const(d->m_processRunners)) {
243 if (!r.isNull()) {
244 qApp->sendPostedEvents(r); // so slotStarted gets called
245 }
246 }
247 return ret;
248}
249
251{
252 return d->m_pids.at(0);
253}
254
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(job, &KJob::result, q, [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 *>(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(openWithHandler, &KIO::OpenWithHandlerInterface::canceled, q, [this]() {
296 q->setError(KIO::ERR_USER_CANCELED);
297 q->emitResult();
298 });
299
300 QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::serviceSelected, q, [this](const KService::Ptr &service) {
301 Q_ASSERT(service);
302 m_service = service;
303 q->start();
304 });
305
306 QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::handled, q, [this]() {
307 q->emitResult();
308 });
309
310 openWithHandler->promptUserForApplication(q, m_urls, m_mimeTypeName);
311}
static Q_INVOKABLE bool authorize(const QString &action)
static Q_INVOKABLE bool authorizeAction(const QString &action)
QString exec() const
static bool isAuthorizedDesktopFile(const QString &path)
ApplicationLauncherJob runs an application and watches it while running.
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.
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 emitResult()
int error() const
void result(KJob *job)
virtual Q_SCRIPTABLE void start()=0
QString exec() const
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
int exec(QEventLoop::ProcessEventsFlags flags)
void exit(int returnCode)
QString canonicalFilePath() const const
void append(const T &value)
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
bool isEmpty() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sun Feb 25 2024 18:45:23 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.