File indexing completed on 2024-09-08 03:38:46
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 }