File indexing completed on 2025-01-19 03:41:33
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 }