File indexing completed on 2024-04-28 15:26:51

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