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 "../core/global.h"
10 #include "jobuidelegatefactory.h"
11 #include "kiogui_debug.h"
12 #include "kprocessrunner_p.h"
13 #include "openwithhandlerinterface.h"
14 #include "untrustedprogramhandlerinterface.h"
15 
16 #include <KAuthorized>
17 #include <KDesktopFile>
18 #include <KLocalizedString>
19 #include <QFileInfo>
20 
21 class KIO::ApplicationLauncherJobPrivate
22 {
23 public:
24  explicit ApplicationLauncherJobPrivate(KIO::ApplicationLauncherJob *job, const KService::Ptr &service)
25  : m_service(service)
26  , q(job)
27  {
28  }
29 
30  void slotStarted(qint64 pid)
31  {
32  m_pids.append(pid);
33  if (--m_numProcessesPending == 0) {
34  q->emitResult();
35  }
36  }
37 
38  void showOpenWithDialog();
39 
40  KService::Ptr m_service;
41  QString m_serviceEntryPath;
42  QList<QUrl> m_urls;
44  QString m_suggestedFileName;
45  QByteArray m_startupId;
46  QVector<qint64> m_pids;
47  QVector<QPointer<KProcessRunner>> m_processRunners;
48  int m_numProcessesPending = 0;
50 };
51 
53  : KJob(parent)
54  , d(new ApplicationLauncherJobPrivate(this, service))
55 {
56  if (d->m_service) {
57  // Cache entryPath() because we may call KService::setExec() which will clear entryPath()
58  d->m_serviceEntryPath = d->m_service->entryPath();
59  }
60 }
61 
63  : ApplicationLauncherJob(serviceAction.service(), parent)
64 {
65  Q_ASSERT(d->m_service);
66  d->m_service.detach();
67  d->m_service->setExec(serviceAction.exec());
68 }
69 
71  : KJob(parent)
72  , d(new ApplicationLauncherJobPrivate(this, {}))
73 {
74 }
75 
77 {
78  // Do *NOT* delete the KProcessRunner instances here.
79  // We need it to keep running so it can terminate startup notification on process exit.
80 }
81 
83 {
84  d->m_urls = urls;
85 }
86 
88 {
89  d->m_runFlags = runFlags;
90 }
91 
93 {
94  d->m_suggestedFileName = suggestedFileName;
95 }
96 
98 {
99  d->m_startupId = startupId;
100 }
101 
102 void KIO::ApplicationLauncherJob::emitUnauthorizedError()
103 {
104  setError(KJob::UserDefinedError);
105  setErrorText(i18n("You are not authorized to execute this file."));
106  emitResult();
107 }
108 
110 {
111  if (!d->m_service) {
112  d->showOpenWithDialog();
113  return;
114  }
115 
116  Q_EMIT description(this, i18nc("Launching application", "Launching %1", d->m_service->name()), {}, {});
117 
118  // First, the security checks
119  if (!KAuthorized::authorize(QStringLiteral("run_desktop_files"))) {
120  // KIOSK restriction, cannot be circumvented
121  emitUnauthorizedError();
122  return;
123  }
124 
125  if (!d->m_serviceEntryPath.isEmpty() && !KDesktopFile::isAuthorizedDesktopFile(d->m_serviceEntryPath)) {
126  // We can use QStandardPaths::findExecutable to resolve relative pathnames
127  // but that gets rid of the command line arguments.
128  QString program = QFileInfo(d->m_service->exec()).canonicalFilePath();
129  if (program.isEmpty()) { // e.g. due to command line arguments
130  program = d->m_service->exec();
131  }
132  auto *untrustedProgramHandler = KIO::delegateExtension<KIO::UntrustedProgramHandlerInterface *>(this);
133  if (!untrustedProgramHandler) {
134  emitUnauthorizedError();
135  return;
136  }
137  connect(untrustedProgramHandler, &KIO::UntrustedProgramHandlerInterface::result, this, [this, untrustedProgramHandler](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 (untrustedProgramHandler->makeServiceFileExecutable(d->m_serviceEntryPath, 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.", serviceName, errorString));
154  emitResult();
155  }
156  } else {
157  setError(KIO::ERR_USER_CANCELED);
158  emitResult();
159  }
160  });
161  untrustedProgramHandler->showUntrustedProgramWarning(this, d->m_service->name());
162  return;
163  }
164  proceedAfterSecurityChecks();
165 }
166 
167 void KIO::ApplicationLauncherJob::proceedAfterSecurityChecks()
168 {
169  if (d->m_urls.count() > 1 && !d->m_service->allowMultipleFiles()) {
170  // We need to launch the application N times.
171  // We ignore the result for application 2 to N.
172  // For the first file we launch the application in the
173  // usual way. The reported result is based on this application.
174  d->m_numProcessesPending = d->m_urls.count();
175  d->m_processRunners.reserve(d->m_numProcessesPending);
176  for (int i = 1; i < d->m_urls.count(); ++i) {
177  auto *processRunner =
178  KProcessRunner::fromApplication(d->m_service, d->m_serviceEntryPath, {d->m_urls.at(i)}, d->m_runFlags, d->m_suggestedFileName, QByteArray{});
179  d->m_processRunners.push_back(processRunner);
180  connect(processRunner, &KProcessRunner::processStarted, this, [this](qint64 pid) {
181  d->slotStarted(pid);
182  });
183  }
184  d->m_urls = {d->m_urls.at(0)};
185  } else {
186  d->m_numProcessesPending = 1;
187  }
188 
189  auto *processRunner =
190  KProcessRunner::fromApplication(d->m_service, d->m_serviceEntryPath, d->m_urls, d->m_runFlags, d->m_suggestedFileName, d->m_startupId);
191  d->m_processRunners.push_back(processRunner);
192  connect(processRunner, &KProcessRunner::error, this, [this](const QString &errorText) {
193  setError(KJob::UserDefinedError);
194  setErrorText(errorText);
195  emitResult();
196  });
197  connect(processRunner, &KProcessRunner::processStarted, this, [this](qint64 pid) {
198  d->slotStarted(pid);
199  });
200 }
201 
202 // For KRun
203 bool KIO::ApplicationLauncherJob::waitForStarted()
204 {
205  if (error() != KJob::NoError) {
206  return false;
207  }
208  if (d->m_processRunners.isEmpty()) {
209  // Maybe we're in the security prompt...
210  // Can't avoid the nested event loop
211  // This fork of KJob::exec doesn't set QEventLoop::ExcludeUserInputEvents
212  const bool wasAutoDelete = isAutoDelete();
213  setAutoDelete(false);
214  QEventLoop loop;
215  connect(this, &KJob::result, this, [&](KJob *job) {
216  loop.exit(job->error());
217  });
218  const int ret = loop.exec();
219  if (wasAutoDelete) {
220  deleteLater();
221  }
222  return ret != KJob::NoError;
223  }
224  const bool ret = std::all_of(d->m_processRunners.cbegin(), d->m_processRunners.cend(), [](QPointer<KProcessRunner> r) {
225  return r.isNull() || r->waitForStarted();
226  });
227  for (const auto &r : qAsConst(d->m_processRunners)) {
228  if (!r.isNull()) {
229  qApp->sendPostedEvents(r); // so slotStarted gets called
230  }
231  }
232  return ret;
233 }
234 
236 {
237  return d->m_pids.at(0);
238 }
239 
241 {
242  return d->m_pids;
243 }
244 
245 void KIO::ApplicationLauncherJobPrivate::showOpenWithDialog()
246 {
247  if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) {
248  q->setError(KJob::UserDefinedError);
249  q->setErrorText(i18n("You are not authorized to select an application to open this file."));
250  q->emitResult();
251  return;
252  }
253 
254  auto *openWithHandler = KIO::delegateExtension<KIO::OpenWithHandlerInterface *>(q);
255  if (!openWithHandler) {
256  q->setError(KJob::UserDefinedError);
257  q->setErrorText(i18n("Internal error: could not prompt the user for which application to start"));
258  q->emitResult();
259  return;
260  }
261 
262  QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::canceled, q, [this]() {
263  q->setError(KIO::ERR_USER_CANCELED);
264  q->emitResult();
265  });
266 
267  QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::serviceSelected, q, [this](const KService::Ptr &service) {
268  Q_ASSERT(service);
269  m_service = service;
270  q->start();
271  });
272 
273  QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::handled, q, [this]() {
274  q->emitResult();
275  });
276 
277  openWithHandler->promptUserForApplication(q, m_urls, QString() /* MIME type name unknown */);
278 }
void handled()
Emitted by promptUserForApplication() if it fully handled it including launching the app...
void emitResult()
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...
void start() override
Starts the job.
static bool isAuthorizedDesktopFile(const QString &path)
void setErrorText(const QString &errorText)
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 isEmpty() const const
ApplicationLauncherJob(const KService::Ptr &service, QObject *parent=nullptr)
Creates an ApplicationLauncherJob.
void deleteLater()
void setRunFlags(RunFlags runFlags)
Specifies various flags.
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
Q_EMITQ_EMIT
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-2021 The KDE developers.
Generated on Sun Jun 20 2021 23:00:30 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.