File indexing completed on 2024-04-14 03:53:16

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006 */
0007 
0008 #include "applicationlauncherjob.h"
0009 #include "../core/global.h"
0010 #include "dbusactivationrunner_p.h"
0011 #include "jobuidelegatefactory.h"
0012 #include "kiogui_debug.h"
0013 #include "kprocessrunner_p.h"
0014 #include "mimetypefinderjob.h"
0015 #include "openwithhandlerinterface.h"
0016 #include "untrustedprogramhandlerinterface.h"
0017 
0018 #include <KAuthorized>
0019 #include <KDesktopFile>
0020 #include <KDesktopFileAction>
0021 #include <KLocalizedString>
0022 
0023 #include <QFileInfo>
0024 #include <QPointer>
0025 
0026 class KIO::ApplicationLauncherJobPrivate
0027 {
0028 public:
0029     explicit ApplicationLauncherJobPrivate(KIO::ApplicationLauncherJob *job, const KService::Ptr &service)
0030         : m_service(service)
0031         , q(job)
0032     {
0033     }
0034 
0035     void slotStarted(qint64 pid)
0036     {
0037         m_pids.append(pid);
0038         if (--m_numProcessesPending == 0) {
0039             q->emitResult();
0040         }
0041     }
0042 
0043     void showOpenWithDialogForMimeType();
0044     void showOpenWithDialog();
0045 
0046     KService::Ptr m_service;
0047     QString m_serviceEntryPath;
0048     QList<QUrl> m_urls;
0049     KIO::ApplicationLauncherJob::RunFlags m_runFlags;
0050     QString m_suggestedFileName;
0051     QString m_mimeTypeName;
0052     QByteArray m_startupId;
0053     QList<qint64> m_pids;
0054     QList<QPointer<KProcessRunner>> m_processRunners;
0055     int m_numProcessesPending = 0;
0056     KIO::ApplicationLauncherJob *q;
0057 };
0058 
0059 KIO::ApplicationLauncherJob::ApplicationLauncherJob(const KService::Ptr &service, QObject *parent)
0060     : KJob(parent)
0061     , d(new ApplicationLauncherJobPrivate(this, service))
0062 {
0063     if (d->m_service) {
0064         // Cache entryPath() because we may call KService::setExec() which will clear entryPath()
0065         d->m_serviceEntryPath = d->m_service->entryPath();
0066     }
0067 }
0068 
0069 KIO::ApplicationLauncherJob::ApplicationLauncherJob(const KServiceAction &serviceAction, QObject *parent)
0070     : ApplicationLauncherJob(serviceAction.service(), parent)
0071 {
0072     Q_ASSERT(d->m_service);
0073     d->m_service.detach();
0074     d->m_service->setExec(serviceAction.exec());
0075 }
0076 KIO::ApplicationLauncherJob::ApplicationLauncherJob(const KDesktopFileAction &desktopFileAction, QObject *parent)
0077     : ApplicationLauncherJob(KService::Ptr(new KService(desktopFileAction.desktopFilePath())), parent)
0078 {
0079     Q_ASSERT(d->m_service);
0080     d->m_service.detach();
0081     d->m_service->setExec(desktopFileAction.exec());
0082 }
0083 
0084 KIO::ApplicationLauncherJob::ApplicationLauncherJob(QObject *parent)
0085     : KJob(parent)
0086     , d(new ApplicationLauncherJobPrivate(this, {}))
0087 {
0088 }
0089 
0090 KIO::ApplicationLauncherJob::~ApplicationLauncherJob()
0091 {
0092     // Do *NOT* delete the KProcessRunner instances here.
0093     // We need it to keep running so it can terminate startup notification on process exit.
0094 }
0095 
0096 void KIO::ApplicationLauncherJob::setUrls(const QList<QUrl> &urls)
0097 {
0098     d->m_urls = urls;
0099 }
0100 
0101 void KIO::ApplicationLauncherJob::setRunFlags(RunFlags runFlags)
0102 {
0103     d->m_runFlags = runFlags;
0104 }
0105 
0106 void KIO::ApplicationLauncherJob::setSuggestedFileName(const QString &suggestedFileName)
0107 {
0108     d->m_suggestedFileName = suggestedFileName;
0109 }
0110 
0111 void KIO::ApplicationLauncherJob::setStartupId(const QByteArray &startupId)
0112 {
0113     d->m_startupId = startupId;
0114 }
0115 
0116 void KIO::ApplicationLauncherJob::emitUnauthorizedError()
0117 {
0118     setError(KJob::UserDefinedError);
0119     setErrorText(i18n("You are not authorized to execute this file."));
0120     emitResult();
0121 }
0122 
0123 void KIO::ApplicationLauncherJob::start()
0124 {
0125     if (!d->m_service) {
0126         d->showOpenWithDialogForMimeType();
0127         return;
0128     }
0129 
0130     Q_EMIT description(this, i18nc("Launching application", "Launching %1", d->m_service->name()), {}, {});
0131 
0132     // First, the security checks
0133     if (!KAuthorized::authorize(QStringLiteral("run_desktop_files"))) {
0134         // KIOSK restriction, cannot be circumvented
0135         emitUnauthorizedError();
0136         return;
0137     }
0138 
0139     if (!d->m_serviceEntryPath.isEmpty() && !KDesktopFile::isAuthorizedDesktopFile(d->m_serviceEntryPath)) {
0140         // We can use QStandardPaths::findExecutable to resolve relative pathnames
0141         // but that gets rid of the command line arguments.
0142         QString program = QFileInfo(d->m_service->exec()).canonicalFilePath();
0143         if (program.isEmpty()) { // e.g. due to command line arguments
0144             program = d->m_service->exec();
0145         }
0146         auto *untrustedProgramHandler = KIO::delegateExtension<KIO::UntrustedProgramHandlerInterface *>(this);
0147         if (!untrustedProgramHandler) {
0148             emitUnauthorizedError();
0149             return;
0150         }
0151         connect(untrustedProgramHandler, &KIO::UntrustedProgramHandlerInterface::result, this, [this, untrustedProgramHandler](bool result) {
0152             if (result) {
0153                 // Assume that service is an absolute path since we're being called (relative paths
0154                 // would have been allowed unless Kiosk said no, therefore we already know where the
0155                 // .desktop file is.  Now add a header to it if it doesn't already have one
0156                 // and add the +x bit.
0157 
0158                 QString errorString;
0159                 if (untrustedProgramHandler->makeServiceFileExecutable(d->m_serviceEntryPath, errorString)) {
0160                     proceedAfterSecurityChecks();
0161                 } else {
0162                     QString serviceName = d->m_service->name();
0163                     if (serviceName.isEmpty()) {
0164                         serviceName = d->m_service->genericName();
0165                     }
0166                     setError(KJob::UserDefinedError);
0167                     setErrorText(i18n("Unable to make the service %1 executable, aborting execution.\n%2.", serviceName, errorString));
0168                     emitResult();
0169                 }
0170             } else {
0171                 setError(KIO::ERR_USER_CANCELED);
0172                 emitResult();
0173             }
0174         });
0175         untrustedProgramHandler->showUntrustedProgramWarning(this, d->m_service->name());
0176         return;
0177     }
0178     proceedAfterSecurityChecks();
0179 }
0180 
0181 void KIO::ApplicationLauncherJob::proceedAfterSecurityChecks()
0182 {
0183     if (d->m_urls.count() > 1 && !DBusActivationRunner::activationPossible(d->m_service, d->m_runFlags, d->m_suggestedFileName)
0184         && !d->m_service->allowMultipleFiles()) {
0185         // We need to launch the application N times.
0186         // We ignore the result for application 2 to N.
0187         // For the first file we launch the application in the
0188         // usual way. The reported result is based on this application.
0189         d->m_numProcessesPending = d->m_urls.count();
0190         d->m_processRunners.reserve(d->m_numProcessesPending);
0191         for (int i = 1; i < d->m_urls.count(); ++i) {
0192             auto *processRunner =
0193                 KProcessRunner::fromApplication(d->m_service, d->m_serviceEntryPath, {d->m_urls.at(i)}, d->m_runFlags, d->m_suggestedFileName, QByteArray{});
0194             d->m_processRunners.push_back(processRunner);
0195             connect(processRunner, &KProcessRunner::processStarted, this, [this](qint64 pid) {
0196                 d->slotStarted(pid);
0197             });
0198         }
0199         d->m_urls = {d->m_urls.at(0)};
0200     } else {
0201         d->m_numProcessesPending = 1;
0202     }
0203 
0204     auto *processRunner =
0205         KProcessRunner::fromApplication(d->m_service, d->m_serviceEntryPath, d->m_urls, d->m_runFlags, d->m_suggestedFileName, d->m_startupId);
0206     d->m_processRunners.push_back(processRunner);
0207     connect(processRunner, &KProcessRunner::error, this, [this](const QString &errorText) {
0208         setError(KJob::UserDefinedError);
0209         setErrorText(errorText);
0210         emitResult();
0211     });
0212     connect(processRunner, &KProcessRunner::processStarted, this, [this](qint64 pid) {
0213         d->slotStarted(pid);
0214     });
0215 }
0216 
0217 // For KRun
0218 bool KIO::ApplicationLauncherJob::waitForStarted()
0219 {
0220     if (error() != KJob::NoError) {
0221         return false;
0222     }
0223     if (d->m_processRunners.isEmpty()) {
0224         // Maybe we're in the security prompt...
0225         // Can't avoid the nested event loop
0226         // This fork of KJob::exec doesn't set QEventLoop::ExcludeUserInputEvents
0227         const bool wasAutoDelete = isAutoDelete();
0228         setAutoDelete(false);
0229         QEventLoop loop;
0230         connect(this, &KJob::result, this, [&](KJob *job) {
0231             loop.exit(job->error());
0232         });
0233         const int ret = loop.exec();
0234         if (wasAutoDelete) {
0235             deleteLater();
0236         }
0237         return ret != KJob::NoError;
0238     }
0239     const bool ret = std::all_of(d->m_processRunners.cbegin(), d->m_processRunners.cend(), [](QPointer<KProcessRunner> r) {
0240         return r.isNull() || r->waitForStarted();
0241     });
0242     for (const auto &r : std::as_const(d->m_processRunners)) {
0243         if (!r.isNull()) {
0244             qApp->sendPostedEvents(r); // so slotStarted gets called
0245         }
0246     }
0247     return ret;
0248 }
0249 
0250 qint64 KIO::ApplicationLauncherJob::pid() const
0251 {
0252     return d->m_pids.at(0);
0253 }
0254 
0255 QList<qint64> KIO::ApplicationLauncherJob::pids() const
0256 {
0257     return d->m_pids;
0258 }
0259 
0260 void KIO::ApplicationLauncherJobPrivate::showOpenWithDialogForMimeType()
0261 {
0262     if (m_urls.size() == 1) {
0263         auto job = new KIO::MimeTypeFinderJob(m_urls[0], q);
0264         job->setFollowRedirections(true);
0265         job->setSuggestedFileName(m_suggestedFileName);
0266         q->connect(job, &KJob::result, q, [this, job]() {
0267             if (!job->error()) {
0268                 m_mimeTypeName = job->mimeType();
0269             }
0270             showOpenWithDialog();
0271         });
0272         job->start();
0273     } else {
0274         showOpenWithDialog();
0275     }
0276 }
0277 
0278 void KIO::ApplicationLauncherJobPrivate::showOpenWithDialog()
0279 {
0280     if (!KAuthorized::authorizeAction(QStringLiteral("openwith"))) {
0281         q->setError(KJob::UserDefinedError);
0282         q->setErrorText(i18n("You are not authorized to select an application to open this file."));
0283         q->emitResult();
0284         return;
0285     }
0286 
0287     auto *openWithHandler = KIO::delegateExtension<KIO::OpenWithHandlerInterface *>(q);
0288     if (!openWithHandler) {
0289         q->setError(KJob::UserDefinedError);
0290         q->setErrorText(i18n("Internal error: could not prompt the user for which application to start"));
0291         q->emitResult();
0292         return;
0293     }
0294 
0295     QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::canceled, q, [this]() {
0296         q->setError(KIO::ERR_USER_CANCELED);
0297         q->emitResult();
0298     });
0299 
0300     QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::serviceSelected, q, [this](const KService::Ptr &service) {
0301         Q_ASSERT(service);
0302         m_service = service;
0303         q->start();
0304     });
0305 
0306     QObject::connect(openWithHandler, &KIO::OpenWithHandlerInterface::handled, q, [this]() {
0307         q->emitResult();
0308     });
0309 
0310     openWithHandler->promptUserForApplication(q, m_urls, m_mimeTypeName);
0311 }