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 | |
26 | class KIO::ApplicationLauncherJobPrivate |
27 | { |
28 | public: |
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 | |
59 | KIO::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 | |
69 | KIO::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 | } |
76 | KIO::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 | |
84 | KIO::ApplicationLauncherJob::ApplicationLauncherJob(QObject *parent) |
85 | : KJob(parent) |
86 | , d(new ApplicationLauncherJobPrivate(this, {})) |
87 | { |
88 | } |
89 | |
90 | KIO::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 | |
96 | void KIO::ApplicationLauncherJob::setUrls(const QList<QUrl> &urls) |
97 | { |
98 | d->m_urls = urls; |
99 | } |
100 | |
101 | void KIO::ApplicationLauncherJob::setRunFlags(RunFlags runFlags) |
102 | { |
103 | d->m_runFlags = runFlags; |
104 | } |
105 | |
106 | void KIO::ApplicationLauncherJob::setSuggestedFileName(const QString &suggestedFileName) |
107 | { |
108 | d->m_suggestedFileName = suggestedFileName; |
109 | } |
110 | |
111 | void KIO::ApplicationLauncherJob::setStartupId(const QByteArray &startupId) |
112 | { |
113 | d->m_startupId = startupId; |
114 | } |
115 | |
116 | void KIO::ApplicationLauncherJob::emitUnauthorizedError() |
117 | { |
118 | setError(KJob::UserDefinedError); |
119 | setErrorText(i18n("You are not authorized to execute this file." )); |
120 | emitResult(); |
121 | } |
122 | |
123 | void 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 | |
181 | void 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 |
218 | bool 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 | |
250 | qint64 KIO::ApplicationLauncherJob::pid() const |
251 | { |
252 | return d->m_pids.at(i: 0); |
253 | } |
254 | |
255 | QList<qint64> KIO::ApplicationLauncherJob::pids() const |
256 | { |
257 | return d->m_pids; |
258 | } |
259 | |
260 | void 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 | |
278 | void 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 | |