File indexing completed on 2024-04-21 03:55:01

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2000 Torben Weis <weis@kde.org>
0004     SPDX-FileCopyrightText: 2006-2013 David Faure <faure@kde.org>
0005     SPDX-FileCopyrightText: 2009 Michael Pyne <michael.pyne@kdemail.net>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include "desktopexecparser.h"
0011 #ifndef Q_OS_ANDROID
0012 #include "kiofuse_interface.h"
0013 #endif
0014 
0015 #include <KApplicationTrader>
0016 #include <KConfigGroup>
0017 #include <KDesktopFile>
0018 #include <KLocalizedString>
0019 #include <KMacroExpander>
0020 #include <KService>
0021 #include <KSharedConfig>
0022 #include <KShell>
0023 #include <kprotocolinfo.h> // KF6 TODO remove after moving hasSchemeHandler to OpenUrlJob
0024 
0025 #ifndef Q_OS_ANDROID
0026 #include <QDBusConnection>
0027 #include <QDBusReply>
0028 #endif
0029 #include <QDir>
0030 #include <QFile>
0031 #include <QProcessEnvironment>
0032 #include <QStandardPaths>
0033 #include <QUrl>
0034 
0035 #include <config-kiocore.h> // KDE_INSTALL_FULL_LIBEXECDIR_KF
0036 
0037 #include "kiocoredebug.h"
0038 
0039 class KRunMX1 : public KMacroExpanderBase
0040 {
0041 public:
0042     explicit KRunMX1(const KService &_service)
0043         : KMacroExpanderBase(QLatin1Char('%'))
0044         , hasUrls(false)
0045         , hasSpec(false)
0046         , service(_service)
0047     {
0048     }
0049 
0050     bool hasUrls;
0051     bool hasSpec;
0052 
0053 protected:
0054     int expandEscapedMacro(const QString &str, int pos, QStringList &ret) override;
0055 
0056 private:
0057     const KService &service;
0058 };
0059 
0060 int KRunMX1::expandEscapedMacro(const QString &str, int pos, QStringList &ret)
0061 {
0062     uint option = str[pos + 1].unicode();
0063     switch (option) {
0064     case 'c':
0065         ret << service.name().replace(QLatin1Char('%'), QLatin1String("%%"));
0066         break;
0067     case 'k':
0068         ret << service.entryPath().replace(QLatin1Char('%'), QLatin1String("%%"));
0069         break;
0070     case 'i':
0071         ret << QStringLiteral("--icon") << service.icon().replace(QLatin1Char('%'), QLatin1String("%%"));
0072         break;
0073     case 'm':
0074         //       ret << "-miniicon" << service.icon().replace( '%', "%%" );
0075         qCWarning(KIO_CORE) << "-miniicon isn't supported anymore (service" << service.name() << ')';
0076         break;
0077     case 'u':
0078     case 'U':
0079         hasUrls = true;
0080         Q_FALLTHROUGH();
0081     /* fallthrough */
0082     case 'f':
0083     case 'F':
0084     case 'n':
0085     case 'N':
0086     case 'd':
0087     case 'D':
0088     case 'v':
0089         hasSpec = true;
0090         Q_FALLTHROUGH();
0091     /* fallthrough */
0092     default:
0093         return -2; // subst with same and skip
0094     }
0095     return 2;
0096 }
0097 
0098 class KRunMX2 : public KMacroExpanderBase
0099 {
0100 public:
0101     explicit KRunMX2(const QList<QUrl> &_urls)
0102         : KMacroExpanderBase(QLatin1Char('%'))
0103         , ignFile(false)
0104         , urls(_urls)
0105     {
0106     }
0107 
0108     bool ignFile;
0109 
0110 protected:
0111     int expandEscapedMacro(const QString &str, int pos, QStringList &ret) override;
0112 
0113 private:
0114     void subst(int option, const QUrl &url, QStringList &ret);
0115 
0116     const QList<QUrl> &urls;
0117 };
0118 
0119 void KRunMX2::subst(int option, const QUrl &url, QStringList &ret)
0120 {
0121     switch (option) {
0122     case 'u':
0123         ret << ((url.isLocalFile() && url.fragment().isNull() && url.query().isNull()) ? QDir::toNativeSeparators(url.toLocalFile()) : url.toString());
0124         break;
0125     case 'd':
0126         ret << url.adjusted(QUrl::RemoveFilename).path();
0127         break;
0128     case 'f':
0129         ret << QDir::toNativeSeparators(url.toLocalFile());
0130         break;
0131     case 'n':
0132         ret << url.fileName();
0133         break;
0134     case 'v':
0135         if (url.isLocalFile() && QFile::exists(url.toLocalFile())) {
0136             ret << KDesktopFile(url.toLocalFile()).desktopGroup().readEntry("Dev");
0137         }
0138         break;
0139     }
0140     return;
0141 }
0142 
0143 int KRunMX2::expandEscapedMacro(const QString &str, int pos, QStringList &ret)
0144 {
0145     uint option = str[pos + 1].unicode();
0146     switch (option) {
0147     case 'f':
0148     case 'u':
0149     case 'n':
0150     case 'd':
0151     case 'v':
0152         if (urls.isEmpty()) {
0153             if (!ignFile) {
0154                 // qCDebug(KIO_CORE) << "No URLs supplied to single-URL service" << str;
0155             }
0156         } else if (urls.count() > 1) {
0157             qCWarning(KIO_CORE) << urls.count() << "URLs supplied to single-URL service" << str;
0158         } else {
0159             subst(option, urls.first(), ret);
0160         }
0161         break;
0162     case 'F':
0163     case 'U':
0164     case 'N':
0165     case 'D':
0166         option += 'a' - 'A';
0167         for (const QUrl &url : urls) {
0168             subst(option, url, ret);
0169         }
0170         break;
0171     case '%':
0172         ret = QStringList(QStringLiteral("%"));
0173         break;
0174     default:
0175         return -2; // subst with same and skip
0176     }
0177     return 2;
0178 }
0179 
0180 QStringList KIO::DesktopExecParser::supportedProtocols(const KService &service)
0181 {
0182     QStringList supportedProtocols = service.supportedProtocols();
0183 
0184     KRunMX1 mx1(service);
0185     QString exec = service.exec();
0186     if (mx1.expandMacrosShellQuote(exec) && !mx1.hasUrls) {
0187         if (!supportedProtocols.isEmpty()) {
0188             qCWarning(KIO_CORE) << service.entryPath() << "contains supported protocols but doesn't use %u or %U in its Exec line! This is inconsistent.";
0189         }
0190         return QStringList();
0191     } else {
0192         if (supportedProtocols.isEmpty()) {
0193             // compat mode: assume KIO if not set and it's a KDE app (or a KDE service)
0194             const QStringList categories = service.property<QStringList>(QStringLiteral("Categories"));
0195             if (categories.contains(QLatin1String("KDE")) || !service.isApplication() || service.entryPath().isEmpty() /*temp service*/) {
0196                 supportedProtocols.append(QStringLiteral("KIO"));
0197             } else { // if no KDE app, be a bit over-generic
0198                 supportedProtocols.append(QStringLiteral("http"));
0199                 supportedProtocols.append(QStringLiteral("https")); // #253294
0200                 supportedProtocols.append(QStringLiteral("ftp"));
0201             }
0202         }
0203     }
0204 
0205     // qCDebug(KIO_CORE) << "supportedProtocols:" << supportedProtocols;
0206     return supportedProtocols;
0207 }
0208 
0209 bool KIO::DesktopExecParser::isProtocolInSupportedList(const QUrl &url, const QStringList &supportedProtocols)
0210 {
0211     return url.isLocalFile() //
0212         || supportedProtocols.contains(QLatin1String("KIO")) //
0213         || supportedProtocols.contains(url.scheme(), Qt::CaseInsensitive);
0214 }
0215 
0216 // We have up to two sources of data, for protocols not handled by KIO workers (so called "helper") :
0217 // 1) the exec line of the .protocol file, if there's one
0218 // 2) the application associated with x-scheme-handler/<protocol> if there's one
0219 bool KIO::DesktopExecParser::hasSchemeHandler(const QUrl &url) // KF6 TODO move to OpenUrlJob
0220 {
0221     if (KProtocolInfo::isHelperProtocol(url)) {
0222         return true;
0223     }
0224     const KService::Ptr service = KApplicationTrader::preferredService(QLatin1String("x-scheme-handler/") + url.scheme());
0225     if (service) {
0226         qCDebug(KIO_CORE) << QLatin1String("preferred service for x-scheme-handler/") + url.scheme() << service->desktopEntryName();
0227     }
0228     return service;
0229 }
0230 
0231 class KIO::DesktopExecParserPrivate
0232 {
0233 public:
0234     DesktopExecParserPrivate(const KService &_service, const QList<QUrl> &_urls)
0235         : service(_service)
0236         , urls(_urls)
0237         , tempFiles(false)
0238     {
0239     }
0240 
0241     bool isUrlSupported(const QUrl &url, const QStringList &supportedProtocols);
0242 
0243     const KService &service;
0244     QList<QUrl> urls;
0245     bool tempFiles;
0246     QString suggestedFileName;
0247     QString m_errorString;
0248 };
0249 
0250 KIO::DesktopExecParser::DesktopExecParser(const KService &service, const QList<QUrl> &urls)
0251     : d(new DesktopExecParserPrivate(service, urls))
0252 {
0253 }
0254 
0255 KIO::DesktopExecParser::~DesktopExecParser()
0256 {
0257 }
0258 
0259 void KIO::DesktopExecParser::setUrlsAreTempFiles(bool tempFiles)
0260 {
0261     d->tempFiles = tempFiles;
0262 }
0263 
0264 void KIO::DesktopExecParser::setSuggestedFileName(const QString &suggestedFileName)
0265 {
0266     d->suggestedFileName = suggestedFileName;
0267 }
0268 
0269 static const QString kioexecPath()
0270 {
0271     QString kioexec = QCoreApplication::applicationDirPath() + QLatin1String("/kioexec");
0272     if (!QFileInfo::exists(kioexec)) {
0273         kioexec = QStringLiteral(KDE_INSTALL_FULL_LIBEXECDIR_KF "/kioexec");
0274     }
0275     Q_ASSERT(QFileInfo::exists(kioexec));
0276     return kioexec;
0277 }
0278 
0279 static QString findNonExecutableProgram(const QString &executable)
0280 {
0281     // Relative to current dir, or absolute path
0282     const QFileInfo fi(executable);
0283     if (fi.exists() && !fi.isExecutable()) {
0284         return executable;
0285     }
0286 
0287 #ifdef Q_OS_UNIX
0288     // This is a *very* simplified version of QStandardPaths::findExecutable
0289     const QStringList searchPaths = QString::fromLocal8Bit(qgetenv("PATH")).split(QDir::listSeparator(), Qt::SkipEmptyParts);
0290     for (const QString &searchPath : searchPaths) {
0291         const QString candidate = searchPath + QLatin1Char('/') + executable;
0292         const QFileInfo fileInfo(candidate);
0293         if (fileInfo.exists()) {
0294             if (fileInfo.isExecutable()) {
0295                 qWarning() << "Internal program error. QStandardPaths::findExecutable couldn't find" << executable << "but our own logic found it at"
0296                            << candidate << ". Please report a bug at https://bugs.kde.org";
0297             } else {
0298                 return candidate;
0299             }
0300         }
0301     }
0302 #endif
0303     return QString();
0304 }
0305 
0306 bool KIO::DesktopExecParserPrivate::isUrlSupported(const QUrl &url, const QStringList &protocols)
0307 {
0308     if (KIO::DesktopExecParser::isProtocolInSupportedList(url, protocols)) {
0309         return true;
0310     }
0311 
0312     // supportedProtocols() only checks whether the .desktop file has MimeType=x-scheme-handler/xxx
0313     // We also want to check whether the app has been set as default/associated in mimeapps.list
0314     const auto handlers = KApplicationTrader::queryByMimeType(QLatin1String("x-scheme-handler/") + url.scheme());
0315     for (const KService::Ptr &handler : handlers) {
0316         if (handler->desktopEntryName() == service.desktopEntryName()) {
0317             return true;
0318         }
0319     }
0320 
0321     return false;
0322 }
0323 
0324 QStringList KIO::DesktopExecParser::resultingArguments() const
0325 {
0326     QString exec = d->service.exec();
0327     if (exec.isEmpty()) {
0328         d->m_errorString = i18n("No Exec field in %1", d->service.entryPath());
0329         qCWarning(KIO_CORE) << "No Exec field in" << d->service.entryPath();
0330         return QStringList();
0331     }
0332 
0333     // Extract the name of the binary to execute from the full Exec line, to see if it exists
0334     const QString binary = executablePath(exec);
0335     QString executableFullPath;
0336     if (!binary.isEmpty()) { // skip all this if the Exec line is a complex shell command
0337         if (QDir::isRelativePath(binary)) {
0338             // Resolve the executable to ensure that helpers in libexec are found.
0339             // Too bad for commands that need a shell - they must reside in $PATH.
0340             executableFullPath = QStandardPaths::findExecutable(binary);
0341             if (executableFullPath.isEmpty()) {
0342                 executableFullPath = QFile::decodeName(KDE_INSTALL_FULL_LIBEXECDIR_KF "/") + binary;
0343             }
0344         } else {
0345             executableFullPath = binary;
0346         }
0347 
0348         // Now check that the binary exists and has the executable flag
0349         if (!QFileInfo(executableFullPath).isExecutable()) {
0350             // Does it really not exist, or is it non-executable (on Unix)? (bug #415567)
0351             const QString nonExecutable = findNonExecutableProgram(binary);
0352             if (nonExecutable.isEmpty()) {
0353                 d->m_errorString = i18n("Could not find the program '%1'", binary);
0354             } else {
0355                 if (QDir::isRelativePath(binary)) {
0356                     d->m_errorString = i18n("The program '%1' was found at '%2' but it is missing executable permissions.", binary, nonExecutable);
0357                 } else {
0358                     d->m_errorString = i18n("The program '%1' is missing executable permissions.", nonExecutable);
0359                 }
0360             }
0361             return QStringList();
0362         }
0363     }
0364 
0365     QStringList result;
0366     bool appHasTempFileOption;
0367 
0368     KRunMX1 mx1(d->service);
0369     KRunMX2 mx2(d->urls);
0370 
0371     if (!mx1.expandMacrosShellQuote(exec)) { // Error in shell syntax
0372         d->m_errorString = i18n("Syntax error in command %1 coming from %2", exec, d->service.entryPath());
0373         qCWarning(KIO_CORE) << "Syntax error in command" << d->service.exec() << ", service" << d->service.name();
0374         return QStringList();
0375     }
0376 
0377     // FIXME: the current way of invoking kioexec disables term and su use
0378 
0379     // Check if we need "tempexec" (kioexec in fact)
0380     appHasTempFileOption = d->tempFiles && d->service.property<bool>(QStringLiteral("X-KDE-HasTempFileOption"));
0381     if (d->tempFiles && !appHasTempFileOption && d->urls.size()) {
0382         result << kioexecPath() << QStringLiteral("--tempfiles") << exec;
0383         if (!d->suggestedFileName.isEmpty()) {
0384             result << QStringLiteral("--suggestedfilename");
0385             result << d->suggestedFileName;
0386         }
0387         result += QUrl::toStringList(d->urls);
0388         return result;
0389     }
0390 
0391     // Return true for non-KIO desktop files with explicit X-KDE-Protocols list, like vlc, for the special case below
0392     auto isNonKIO = [this]() {
0393         const QStringList protocols = d->service.property<QStringList>(QStringLiteral("X-KDE-Protocols"));
0394         return !protocols.isEmpty() && !protocols.contains(QLatin1String("KIO"));
0395     };
0396 
0397     // Check if we need kioexec, or KIOFuse
0398     bool useKioexec = false;
0399 #ifndef Q_OS_ANDROID
0400     org::kde::KIOFuse::VFS kiofuse_iface(QStringLiteral("org.kde.KIOFuse"), QStringLiteral("/org/kde/KIOFuse"), QDBusConnection::sessionBus());
0401     struct MountRequest {
0402         QDBusPendingReply<QString> reply;
0403         int urlIndex;
0404     };
0405     QList<MountRequest> requests;
0406     requests.reserve(d->urls.count());
0407 
0408     const QStringList appSupportedProtocols = supportedProtocols(d->service);
0409     for (int i = 0; i < d->urls.count(); ++i) {
0410         const QUrl url = d->urls.at(i);
0411         const bool supported = mx1.hasUrls ? d->isUrlSupported(url, appSupportedProtocols) : url.isLocalFile();
0412         if (!supported) {
0413             // If FUSE fails, and there is no scheme handler, we'll have to fallback to kioexec
0414             useKioexec = true;
0415         }
0416 
0417         // NOTE: Some non-KIO apps may support the URLs (e.g. VLC supports smb://)
0418         // but will not have the password if they are not in the URL itself.
0419         // Hence convert URL to KIOFuse equivalent in case there is a password.
0420         // @see https://pointieststick.com/2018/01/17/videos-on-samba-shares/
0421         // @see https://bugs.kde.org/show_bug.cgi?id=330192
0422         if (!supported || (!url.userName().isEmpty() && url.password().isEmpty() && isNonKIO())) {
0423             requests.push_back({kiofuse_iface.mountUrl(url.toString()), i});
0424         }
0425     }
0426 
0427     for (auto &request : requests) {
0428         request.reply.waitForFinished();
0429     }
0430     const bool fuseError = std::any_of(requests.cbegin(), requests.cend(), [](const MountRequest &request) {
0431         return request.reply.isError();
0432     });
0433 
0434     if (fuseError && useKioexec) {
0435         // We need to run the app through kioexec
0436         result << kioexecPath();
0437         if (d->tempFiles) {
0438             result << QStringLiteral("--tempfiles");
0439         }
0440         if (!d->suggestedFileName.isEmpty()) {
0441             result << QStringLiteral("--suggestedfilename");
0442             result << d->suggestedFileName;
0443         }
0444         result << exec;
0445         result += QUrl::toStringList(d->urls);
0446         return result;
0447     }
0448 
0449     // At this point we know we're not using kioexec, so feel free to replace
0450     // KIO URLs with their KIOFuse local path.
0451     for (const auto &request : std::as_const(requests)) {
0452         if (!request.reply.isError()) {
0453             d->urls[request.urlIndex] = QUrl::fromLocalFile(request.reply.value());
0454         }
0455     }
0456 #endif
0457 
0458     if (appHasTempFileOption) {
0459         exec += QLatin1String(" --tempfile");
0460     }
0461 
0462     // Did the user forget to append something like '%f'?
0463     // If so, then assume that '%f' is the right choice => the application
0464     // accepts only local files.
0465     if (!mx1.hasSpec) {
0466         exec += QLatin1String(" %f");
0467         mx2.ignFile = true;
0468     }
0469 
0470     mx2.expandMacrosShellQuote(exec); // syntax was already checked, so don't check return value
0471 
0472     /*
0473      1 = need_shell, 2 = terminal, 4 = su
0474 
0475      0                                                           << split(cmd)
0476      1                                                           << "sh" << "-c" << cmd
0477      2 << split(term) << "-e"                                    << split(cmd)
0478      3 << split(term) << "-e"                                    << "sh" << "-c" << cmd
0479 
0480      4                        << "kdesu" << "-u" << user << "-c" << cmd
0481      5                        << "kdesu" << "-u" << user << "-c" << ("sh -c " + quote(cmd))
0482      6 << split(term) << "-e" << "su"            << user << "-c" << cmd
0483      7 << split(term) << "-e" << "su"            << user << "-c" << ("sh -c " + quote(cmd))
0484 
0485      "sh -c" is needed in the "su" case, too, as su uses the user's login shell, not sh.
0486      this could be optimized with the -s switch of some su versions (e.g., debian linux).
0487     */
0488 
0489     if (d->service.terminal()) {
0490         KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("General"));
0491         QString terminal = cg.readPathEntry("TerminalApplication", QStringLiteral("konsole"));
0492 
0493         const bool isKonsole = (terminal == QLatin1String("konsole"));
0494         QStringList terminalParts = KShell::splitArgs(terminal);
0495         QString terminalPath;
0496         if (!terminalParts.isEmpty()) {
0497             terminalPath = QStandardPaths::findExecutable(terminalParts.at(0));
0498         }
0499 
0500         if (terminalPath.isEmpty()) {
0501             d->m_errorString = i18n("Terminal %1 not found while trying to run %2", terminal, d->service.entryPath());
0502             qCWarning(KIO_CORE) << "Terminal" << terminal << "not found, service" << d->service.name();
0503             return QStringList();
0504         }
0505         terminalParts[0] = terminalPath;
0506         terminal = KShell::joinArgs(terminalParts);
0507         if (isKonsole) {
0508             if (!d->service.workingDirectory().isEmpty()) {
0509                 terminal += QLatin1String(" --workdir ") + KShell::quoteArg(d->service.workingDirectory());
0510             }
0511             terminal += QLatin1String(" -qwindowtitle '%c'");
0512             if (!d->service.icon().isEmpty()) {
0513                 terminal += QLatin1String(" -qwindowicon ") + KShell::quoteArg(d->service.icon().replace(QLatin1Char('%'), QLatin1String("%%")));
0514             }
0515         }
0516         terminal += QLatin1Char(' ') + d->service.terminalOptions();
0517         if (!mx1.expandMacrosShellQuote(terminal)) {
0518             d->m_errorString = i18n("Syntax error in command %1 while trying to run %2", terminal, d->service.entryPath());
0519             qCWarning(KIO_CORE) << "Syntax error in command" << terminal << ", service" << d->service.name();
0520             return QStringList();
0521         }
0522         mx2.expandMacrosShellQuote(terminal);
0523         result = KShell::splitArgs(terminal); // assuming that the term spec never needs a shell!
0524         result << QStringLiteral("-e");
0525     }
0526 
0527     KShell::Errors err;
0528     QStringList execlist = KShell::splitArgs(exec, KShell::AbortOnMeta | KShell::TildeExpand, &err);
0529     if (!executableFullPath.isEmpty()) {
0530         execlist[0] = executableFullPath;
0531     }
0532 
0533     if (d->service.substituteUid()) {
0534         if (d->service.terminal()) {
0535             result << QStringLiteral("su");
0536         } else {
0537             QString kdesu = QFile::decodeName(KDE_INSTALL_FULL_LIBEXECDIR_KF "/kdesu");
0538             if (!QFile::exists(kdesu)) {
0539                 kdesu = QStandardPaths::findExecutable(QStringLiteral("kdesu"));
0540             }
0541             if (!QFile::exists(kdesu)) {
0542                 // Insert kdesu as string so we show a nice warning: 'Could not launch kdesu'
0543                 result << QStringLiteral("kdesu");
0544                 return result;
0545             } else {
0546                 result << kdesu << QStringLiteral("-u");
0547             }
0548         }
0549 
0550         result << d->service.username() << QStringLiteral("-c");
0551         if (err == KShell::FoundMeta) {
0552             exec = QLatin1String("/bin/sh -c ") + KShell::quoteArg(exec);
0553         } else {
0554             exec = KShell::joinArgs(execlist);
0555         }
0556         result << exec;
0557     } else {
0558         if (err == KShell::FoundMeta) {
0559             result << QStringLiteral("/bin/sh") << QStringLiteral("-c") << exec;
0560         } else {
0561             result += execlist;
0562         }
0563     }
0564 
0565     return result;
0566 }
0567 
0568 QString KIO::DesktopExecParser::errorMessage() const
0569 {
0570     return d->m_errorString;
0571 }
0572 
0573 // static
0574 QString KIO::DesktopExecParser::executableName(const QString &execLine)
0575 {
0576     const QString bin = executablePath(execLine);
0577     return bin.mid(bin.lastIndexOf(QLatin1Char('/')) + 1);
0578 }
0579 
0580 // static
0581 QString KIO::DesktopExecParser::executablePath(const QString &execLine)
0582 {
0583     // Remove parameters and/or trailing spaces.
0584     const QStringList args = KShell::splitArgs(execLine, KShell::AbortOnMeta | KShell::TildeExpand);
0585     auto it = std::find_if(args.cbegin(), args.cend(), [](const QString &arg) {
0586         return !arg.contains(QLatin1Char('='));
0587     });
0588     return it != args.cend() ? *it : QString{};
0589 }