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_urls.size() == 1) {
129 const QUrl url = d->m_urls.first();
130 if (url.isLocalFile() && !QFile::exists(url.toLocalFile())) {
131 // Don't launch an application for a non-existing file (e.g. a broken symlink)
132 setError(KIO::ERR_DOES_NOT_EXIST);
133 setErrorText(url.toDisplayString());
134 emitResult();
135 return;
136 }
137 }
138
139 if (!d->m_service) {
140 d->showOpenWithDialogForMimeType();
141 return;
142 }
143
144 Q_EMIT description(this, i18nc("Launching application", "Launching %1", d->m_service->name()), {}, {});
145
146 // First, the security checks
147 if (!KAuthorized::authorize(QStringLiteral("run_desktop_files"))) {
148 // KIOSK restriction, cannot be circumvented
149 emitUnauthorizedError();
150 return;
151 }
152
153 if (!d->m_serviceEntryPath.isEmpty() && !KDesktopFile::isAuthorizedDesktopFile(d->m_serviceEntryPath)) {
154 // We can use QStandardPaths::findExecutable to resolve relative pathnames
155 // but that gets rid of the command line arguments.
156 QString program = QFileInfo(d->m_service->exec()).canonicalFilePath();
157 if (program.isEmpty()) { // e.g. due to command line arguments
158 program = d->m_service->exec();
159 }
160 auto *untrustedProgramHandler = KIO::delegateExtension<KIO::UntrustedProgramHandlerInterface *>(this);
161 if (!untrustedProgramHandler) {
162 emitUnauthorizedError();
163 return;
164 }
165 connect(untrustedProgramHandler, &KIO::UntrustedProgramHandlerInterface::result, this, [this, untrustedProgramHandler](bool result) {
166 if (result) {
167 // Assume that service is an absolute path since we're being called (relative paths
168 // would have been allowed unless Kiosk said no, therefore we already know where the
169 // .desktop file is. Now add a header to it if it doesn't already have one
170 // and add the +x bit.
171
172 QString errorString;
173 if (untrustedProgramHandler->makeServiceFileExecutable(d->m_serviceEntryPath, errorString)) {
174 proceedAfterSecurityChecks();
175 } else {
176 QString serviceName = d->m_service->name();
177 if (serviceName.isEmpty()) {
178 serviceName = d->m_service->genericName();
179 }
180 setError(KJob::UserDefinedError);
181 setErrorText(i18n("Unable to make the service %1 executable, aborting execution.\n%2.", serviceName, errorString));
182 emitResult();
183 }
184 } else {
185 setError(KIO::ERR_USER_CANCELED);
186 emitResult();
187 }
188 });
189 untrustedProgramHandler->showUntrustedProgramWarning(this, d->m_service->name());
190 return;
191 }
192 proceedAfterSecurityChecks();
193}
194
195void KIO::ApplicationLauncherJob::proceedAfterSecurityChecks()
196{
197 bool startNTimesCondition = d->m_urls.count() > 1 && !d->m_service->allowMultipleFiles();
198#ifdef WITH_QTDBUS
199 startNTimesCondition = startNTimesCondition && !DBusActivationRunner::activationPossible(d->m_service, d->m_runFlags, d->m_suggestedFileName);
200#endif
201 if (startNTimesCondition) {
202 // We need to launch the application N times.
203 // We ignore the result for application 2 to N.
204 // For the first file we launch the application in the
205 // usual way. The reported result is based on this application.
206 d->m_numProcessesPending = d->m_urls.count();
207 d->m_processRunners.reserve(d->m_numProcessesPending);
208 for (int i = 1; i < d->m_urls.count(); ++i) {
209 auto *processRunner =
210 KProcessRunner::fromApplication(d->m_service, d->m_serviceEntryPath, {d->m_urls.at(i)}, d->m_runFlags, d->m_suggestedFileName, QByteArray{});
211 d->m_processRunners.push_back(processRunner);
212 connect(processRunner, &KProcessRunner::processStarted, this, [this](qint64 pid) {
213 d->slotStarted(pid);
214 });
215 }
216 d->m_urls = {d->m_urls.at(0)};
217 } else {
218 d->m_numProcessesPending = 1;
219 }
220
221 auto *processRunner =
222 KProcessRunner::fromApplication(d->m_service, d->m_serviceEntryPath, d->m_urls, d->m_runFlags, d->m_suggestedFileName, d->m_startupId);
223 d->m_processRunners.push_back(processRunner);
224 connect(processRunner, &KProcessRunner::error, this, [this](const QString &errorText) {
225 setError(KJob::UserDefinedError);
226 setErrorText(errorText);
227 emitResult();
228 });
229 connect(processRunner, &KProcessRunner::processStarted, this, [this](qint64 pid) {
230 d->slotStarted(pid);
231 });
232}
233
234// For KRun
235bool KIO::ApplicationLauncherJob::waitForStarted()
236{
237 if (error() != KJob::NoError) {
238 return false;
239 }
240 if (d->m_processRunners.isEmpty()) {
241 // Maybe we're in the security prompt...
242 // Can't avoid the nested event loop
243 // This fork of KJob::exec doesn't set QEventLoop::ExcludeUserInputEvents
244 const bool wasAutoDelete = isAutoDelete();
245 setAutoDelete(false);
246 QEventLoop loop;
247 connect(this, &KJob::result, this, [&](KJob *job) {
248 loop.exit(job->error());
249 });
250 const int ret = loop.exec();
251 if (wasAutoDelete) {
252 deleteLater();
253 }
254 return ret != KJob::NoError;
255 }
256 const bool ret = std::all_of(d->m_processRunners.cbegin(), d->m_processRunners.cend(), [](QPointer<KProcessRunner> r) {
257 return r.isNull() || r->waitForStarted();
258 });
259 for (const auto &r : std::as_const(d->m_processRunners)) {
260 if (!r.isNull()) {
261 qApp->sendPostedEvents(r); // so slotStarted gets called
262 }
263 }
264 return ret;
265}
266
268{
269 return d->m_pids.at(0);
270}
271
273{
274 return d->m_pids;
275}
276
277void KIO::ApplicationLauncherJobPrivate::showOpenWithDialogForMimeType()
278{
279 if (m_urls.size() == 1) {
280 auto job = new KIO::MimeTypeFinderJob(m_urls[0], q);
281 job->setFollowRedirections(true);
282 job->setSuggestedFileName(m_suggestedFileName);
283 q->connect(job, &KJob::result, q, [this, job]() {
284 if (!job->error()) {
285 m_mimeTypeName = job->mimeType();
286 }
287 showOpenWithDialog();
288 });
289 job->start();
290 } else {
291 showOpenWithDialog();
292 }
293}
294
295void KIO::ApplicationLauncherJobPrivate::showOpenWithDialog()
296{
297 if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) {
298 q->setError(KJob::UserDefinedError);
299 q->setErrorText(i18n("You are not authorized to select an application to open this file."));
300 q->emitResult();
301 return;
302 }
303
305 if (!openWithHandler) {
306 q->setError(KJob::UserDefinedError);
307 q->setErrorText(i18n("Internal error: could not prompt the user for which application to start"));
308 q->emitResult();
309 return;
310 }
311
312 QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::canceled, q, [this]() {
313 q->setError(KIO::ERR_USER_CANCELED);
314 q->emitResult();
315 });
316
317 QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::serviceSelected, q, [this](const KService::Ptr &service) {
318 Q_ASSERT(service);
319 m_service = service;
320 q->start();
321 });
322
323 QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::handled, q, [this]() {
324 q->emitResult();
325 });
326
327 openWithHandler->promptUserForApplication(q, m_urls, m_mimeTypeName);
328}
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)
bool exists() const const
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)
bool isLocalFile() const const
QString toDisplayString(FormattingOptions options) const const
QString toLocalFile() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 16:58:45 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.