KIO

applicationlauncherjob.cpp
1 /*
2  This file is part of the KDE libraries
3  SPDX-FileCopyrightText: 2020 David Faure <[email protected]>
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 "kprocessrunner_p.h"
10 #include "untrustedprogramhandlerinterface.h"
11 #include "kiogui_debug.h"
12 #include "openwithhandlerinterface.h"
13 #include "../core/global.h"
14 
15 #include <KAuthorized>
16 #include <KDesktopFile>
17 #include <KLocalizedString>
18 #include <QFileInfo>
19 
20 // KF6 TODO: Remove
21 static KIO::UntrustedProgramHandlerInterface *s_untrustedProgramHandler = nullptr;
22 
23 extern KIO::OpenWithHandlerInterface *s_openWithHandler; // defined in openurljob.cpp
24 
25 namespace KIO {
26 // Hidden API because in KF6 we'll just check if the job's uiDelegate implements UntrustedProgramHandlerInterface.
27 KIOGUI_EXPORT void setDefaultUntrustedProgramHandler(KIO::UntrustedProgramHandlerInterface *iface) { s_untrustedProgramHandler = iface; }
28 // For OpenUrlJob
29 KIO::UntrustedProgramHandlerInterface *defaultUntrustedProgramHandler() { return s_untrustedProgramHandler; }
30 }
31 
32 #include <KLocalizedString>
33 
34 class KIO::ApplicationLauncherJobPrivate
35 {
36 public:
37  explicit ApplicationLauncherJobPrivate(KIO::ApplicationLauncherJob *job, const KService::Ptr &service)
38  : m_service(service), q(job) {}
39 
40  void slotStarted(qint64 pid) {
41  m_pids.append(pid);
42  if (--m_numProcessesPending == 0) {
43  q->emitResult();
44  }
45  }
46 
47  void showOpenWithDialog();
48 
49  KService::Ptr m_service;
50  QList<QUrl> m_urls;
52  QString m_suggestedFileName;
53  QByteArray m_startupId;
54  QVector<qint64> m_pids;
55  QVector<QPointer<KProcessRunner>> m_processRunners;
56  int m_numProcessesPending = 0;
58 };
59 
61  : KJob(parent), d(new ApplicationLauncherJobPrivate(this, service))
62 {
63 }
64 
66  : ApplicationLauncherJob(serviceAction.service(), parent)
67 {
68  Q_ASSERT(d->m_service);
69  d->m_service.detach();
70  d->m_service->setExec(serviceAction.exec());
71 }
72 
74  : KJob(parent), d(new ApplicationLauncherJobPrivate(this, {}))
75 {
76 }
77 
79 {
80  // Do *NOT* delete the KProcessRunner instances here.
81  // We need it to keep running so it can terminate startup notification on process exit.
82 }
83 
85 {
86  d->m_urls = urls;
87 }
88 
90 {
91  d->m_runFlags = runFlags;
92 }
93 
95 {
96  d->m_suggestedFileName = suggestedFileName;
97 }
98 
100 {
101  d->m_startupId = startupId;
102 }
103 
104 void KIO::ApplicationLauncherJob::emitUnauthorizedError()
105 {
106  setError(KJob::UserDefinedError);
107  setErrorText(i18n("You are not authorized to execute this file."));
108  emitResult();
109 }
110 
112 {
113  if (!d->m_service) {
114  d->showOpenWithDialog();
115  return;
116  }
117  emit description(this, i18nc("Launching application", "Launching %1", d->m_service->name()), {}, {});
118 
119  // First, the security checks
120  if (!KAuthorized::authorize(QStringLiteral("run_desktop_files"))) {
121  // KIOSK restriction, cannot be circumvented
122  emitUnauthorizedError();
123  return;
124  }
125  if (!d->m_service->entryPath().isEmpty() &&
126  !KDesktopFile::isAuthorizedDesktopFile(d->m_service->entryPath())) {
127  // We can use QStandardPaths::findExecutable to resolve relative pathnames
128  // but that gets rid of the command line arguments.
129  QString program = QFileInfo(d->m_service->exec()).canonicalFilePath();
130  if (program.isEmpty()) { // e.g. due to command line arguments
131  program = d->m_service->exec();
132  }
133  if (!s_untrustedProgramHandler) {
134  emitUnauthorizedError();
135  return;
136  }
137  connect(s_untrustedProgramHandler, &KIO::UntrustedProgramHandlerInterface::result, this, [this](bool result) {
138  if (result) {
139  // Assume that service is an absolute path since we're being called (relative paths
140  // would have been allowed unless Kiosk said no, therefore we already know where the
141  // .desktop file is. Now add a header to it if it doesn't already have one
142  // and add the +x bit.
143 
145  if (s_untrustedProgramHandler->makeServiceFileExecutable(d->m_service->entryPath(), errorString)) {
146  proceedAfterSecurityChecks();
147  } else {
148  QString serviceName = d->m_service->name();
149  if (serviceName.isEmpty()) {
150  serviceName = d->m_service->genericName();
151  }
152  setError(KJob::UserDefinedError);
153  setErrorText(i18n("Unable to make the service %1 executable, aborting execution.\n%2.",
154  serviceName, errorString));
155  emitResult();
156  }
157  } else {
158  setError(KIO::ERR_USER_CANCELED);
159  emitResult();
160  }
161  });
162  s_untrustedProgramHandler->showUntrustedProgramWarning(this, d->m_service->name());
163  return;
164  }
165  proceedAfterSecurityChecks();
166 }
167 
168 void KIO::ApplicationLauncherJob::proceedAfterSecurityChecks()
169 {
170  if (d->m_urls.count() > 1 && !d->m_service->allowMultipleFiles()) {
171  // We need to launch the application N times.
172  // We ignore the result for application 2 to N.
173  // For the first file we launch the application in the
174  // usual way. The reported result is based on this application.
175  d->m_numProcessesPending = d->m_urls.count();
176  d->m_processRunners.reserve(d->m_numProcessesPending);
177  for (int i = 1; i < d->m_urls.count(); ++i) {
178  auto *processRunner = KProcessRunner::fromApplication(d->m_service, { d->m_urls.at(i) },
179  d->m_runFlags, d->m_suggestedFileName, QByteArray());
180  d->m_processRunners.push_back(processRunner);
181  connect(processRunner, &KProcessRunner::processStarted, this, [this](qint64 pid) {
182  d->slotStarted(pid);
183  });
184  }
185  d->m_urls = { d->m_urls.at(0) };
186  } else {
187  d->m_numProcessesPending = 1;
188  }
189 
190  auto *processRunner = KProcessRunner::fromApplication(d->m_service, d->m_urls,
191  d->m_runFlags, d->m_suggestedFileName, d->m_startupId);
192  d->m_processRunners.push_back(processRunner);
193  connect(processRunner, &KProcessRunner::error, this, [this](const QString &errorText) {
194  setError(KJob::UserDefinedError);
195  setErrorText(errorText);
196  emitResult();
197  });
198  connect(processRunner, &KProcessRunner::processStarted, this, [this](qint64 pid) {
199  d->slotStarted(pid);
200  });
201 }
202 
203 // For KRun
204 bool KIO::ApplicationLauncherJob::waitForStarted()
205 {
206  if (error() != KJob::NoError) {
207  return false;
208  }
209  if (d->m_processRunners.isEmpty()) {
210  // Maybe we're in the security prompt...
211  // Can't avoid the nested event loop
212  // This fork of KJob::exec doesn't set QEventLoop::ExcludeUserInputEvents
213  const bool wasAutoDelete = isAutoDelete();
214  setAutoDelete(false);
215  QEventLoop loop;
216  connect(this, &KJob::result, this, [&](KJob *job) {
217  loop.exit(job->error());
218  });
219  const int ret = loop.exec();
220  if (wasAutoDelete) {
221  deleteLater();
222  }
223  return ret != KJob::NoError;
224  }
225  const bool ret = std::all_of(d->m_processRunners.cbegin(),
226  d->m_processRunners.cend(),
227  [](QPointer<KProcessRunner> r) { return r.isNull() || r->waitForStarted(); });
228  for (auto r : qAsConst(d->m_processRunners)) {
229  if (!r.isNull()) {
230  qApp->sendPostedEvents(r); // so slotStarted gets called
231  }
232  }
233  return ret;
234 }
235 
237 {
238  return d->m_pids.at(0);
239 }
240 
242 {
243  return d->m_pids;
244 }
245 
246 void KIO::ApplicationLauncherJobPrivate::showOpenWithDialog()
247 {
248  if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) {
249  q->setError(KJob::UserDefinedError);
250  q->setErrorText(i18n("You are not authorized to select an application to open this file."));
251  q->emitResult();
252  return;
253  }
254  if (!s_openWithHandler) {
255  q->setError(KJob::UserDefinedError);
256  q->setErrorText(i18n("Internal error: could not prompt the user for which application to start"));
257  q->emitResult();
258  return;
259  }
260 
261  QObject::connect(s_openWithHandler, &KIO::OpenWithHandlerInterface::canceled, q, [this]() {
262  q->setError(KIO::ERR_USER_CANCELED);
263  q->emitResult();
264  });
265 
266  QObject::connect(s_openWithHandler, &KIO::OpenWithHandlerInterface::serviceSelected, q, [this](const KService::Ptr &service) {
267  Q_ASSERT(service);
268  m_service = service;
269  q->start();
270  });
271 
272  QObject::connect(s_openWithHandler, &KIO::OpenWithHandlerInterface::handled, q, [this]() {
273  q->emitResult();
274  });
275 
276  s_openWithHandler->promptUserForApplication(q, m_urls, QString() /* mimetype name unknown */);
277 }
void handled()
Emitted by promptUserForApplication() if it fully handled it including launching the app...
void emitResult()
A namespace for KIO globals.
Definition: authinfo.h:21
virtual QString errorString() const
void setError(int errorCode)
bool isAutoDelete() const
void description(KJob *job, const QString &title, const QPair< QString, QString > &field1=QPair< QString, QString >(), const QPair< QString, QString > &field2=QPair< QString, QString >())
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...
The UntrustedProgramHandlerInterface class allows ApplicationLauncherJob to prompt the user about an ...
void start() override
Starts the job.
virtual void showUntrustedProgramWarning(KJob *job, const QString &programName)
Show a warning to the user about the program not being trusted for execution.
static bool isAuthorizedDesktopFile(const QString &path)
void setErrorText(const QString &errorText)
virtual void promptUserForApplication(KJob *job, const QList< QUrl > &urls, const QString &mimeType)
Show the "Open With" dialog.
void setStartupId(const QByteArray &startupId)
Sets the startup notification id of the application launch.
int exec(QEventLoop::ProcessEventsFlags flags)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
void exit(int returnCode)
bool makeServiceFileExecutable(const QString &fileName, QString &errorString)
Helper function that attempts to make a desktop file executable.
bool isEmpty() const const
ApplicationLauncherJob(const KService::Ptr &service, QObject *parent=nullptr)
Creates an ApplicationLauncherJob.
void deleteLater()
void setRunFlags(RunFlags runFlags)
Specifies various flags.
The OpenWithHandlerInterface class allows OpenUrlJob to prompt the user about which application to us...
QString i18n(const char *text, const TYPE &arg...)
bool isNull() const const
QVector< qint64 > pids() const
void setUrls(const QList< QUrl > &urls)
Specifies the URLs to be passed to the application.
QString exec() const
~ApplicationLauncherJob() override
Destructor.
void result(bool confirmed)
Implementations of this interface must emit result in showUntrustedProgramWarning.
void result(KJob *job)
void serviceSelected(const KService::Ptr &service)
Emitted by promptUserForApplication() once the user chooses an application.
ApplicationLauncherJob runs an application and watches it while running.
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
void setAutoDelete(bool autodelete)
QObject * parent() const const
void canceled()
Emitted by promptUserForApplication() if the user canceled the application selection dialog...
QString errorText() const
int error() const
This file is part of the KDE documentation.
Documentation copyright © 1996-2020 The KDE developers.
Generated on Mon Nov 30 2020 23:01:44 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.