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

KDE's Doxygen guidelines are available online.