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