| 1 | /* |
| 2 | This file is part of the KDE libraries |
| 3 | SPDX-FileCopyrightText: 2000 Torben Weis <weis@kde.org> |
| 4 | SPDX-FileCopyrightText: 2006-2013 David Faure <faure@kde.org> |
| 5 | SPDX-FileCopyrightText: 2009 Michael Pyne <michael.pyne@kdemail.net> |
| 6 | |
| 7 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 8 | */ |
| 9 | |
| 10 | #include "desktopexecparser.h" |
| 11 | |
| 12 | #ifdef WITH_QTDBUS |
| 13 | #include "kiofuse_interface.h" |
| 14 | #endif |
| 15 | |
| 16 | #include <KApplicationTrader> |
| 17 | #include <KConfigGroup> |
| 18 | #include <KDesktopFile> |
| 19 | #include <KLocalizedString> |
| 20 | #include <KMacroExpander> |
| 21 | #include <KService> |
| 22 | #include <KSharedConfig> |
| 23 | #include <KShell> |
| 24 | #include <kprotocolinfo.h> // KF6 TODO remove after moving hasSchemeHandler to OpenUrlJob |
| 25 | |
| 26 | #ifdef WITH_QTDBUS |
| 27 | #include <QDBusConnection> |
| 28 | #include <QDBusReply> |
| 29 | #endif |
| 30 | #include <QDir> |
| 31 | #include <QFile> |
| 32 | #include <QProcessEnvironment> |
| 33 | #include <QStandardPaths> |
| 34 | #include <QUrl> |
| 35 | |
| 36 | #include <config-kiocore.h> // KDE_INSTALL_FULL_LIBEXECDIR_KF |
| 37 | |
| 38 | #include "kiocoredebug.h" |
| 39 | |
| 40 | class KRunMX1 : public KMacroExpanderBase |
| 41 | { |
| 42 | public: |
| 43 | explicit KRunMX1(const KService &_service) |
| 44 | : KMacroExpanderBase(QLatin1Char('%')) |
| 45 | , service(_service) |
| 46 | { |
| 47 | } |
| 48 | |
| 49 | bool hasUrls = false; |
| 50 | bool hasSpec = false; |
| 51 | bool hasError = false; |
| 52 | |
| 53 | protected: |
| 54 | int expandEscapedMacro(const QString &str, int pos, QStringList &ret) override; |
| 55 | |
| 56 | private: |
| 57 | const KService &service; |
| 58 | }; |
| 59 | |
| 60 | int KRunMX1::expandEscapedMacro(const QString &str, int pos, QStringList &ret) |
| 61 | { |
| 62 | if (str.length() == pos + 1) { |
| 63 | // Internally, the stack of KMacroExpanderBase is empty and thus it thinks everything is successfully parsed |
| 64 | // This could be the case if the escape char "%" in this case, is at the end of the string |
| 65 | // See BUG: 495606 |
| 66 | hasError = true; |
| 67 | return 0; |
| 68 | } |
| 69 | uint option = str[pos + 1].unicode(); |
| 70 | switch (option) { |
| 71 | case 'c': |
| 72 | ret << service.name().replace(c: QLatin1Char('%'), after: QLatin1String("%%" )); |
| 73 | break; |
| 74 | case 'k': |
| 75 | ret << service.entryPath().replace(c: QLatin1Char('%'), after: QLatin1String("%%" )); |
| 76 | break; |
| 77 | case 'i': |
| 78 | ret << QStringLiteral("--icon" ) << service.icon().replace(c: QLatin1Char('%'), after: QLatin1String("%%" )); |
| 79 | break; |
| 80 | case 'm': |
| 81 | // ret << "-miniicon" << service.icon().replace( '%', "%%" ); |
| 82 | qCWarning(KIO_CORE) << "-miniicon isn't supported anymore (service" << service.name() << ')'; |
| 83 | break; |
| 84 | case 'u': |
| 85 | case 'U': |
| 86 | hasUrls = true; |
| 87 | Q_FALLTHROUGH(); |
| 88 | /* fallthrough */ |
| 89 | case 'f': |
| 90 | case 'F': |
| 91 | case 'n': |
| 92 | case 'N': |
| 93 | case 'd': |
| 94 | case 'D': |
| 95 | case 'v': |
| 96 | hasSpec = true; |
| 97 | Q_FALLTHROUGH(); |
| 98 | /* fallthrough */ |
| 99 | default: |
| 100 | return -2; // subst with same and skip |
| 101 | } |
| 102 | return 2; |
| 103 | } |
| 104 | |
| 105 | class KRunMX2 : public KMacroExpanderBase |
| 106 | { |
| 107 | public: |
| 108 | explicit KRunMX2(const QList<QUrl> &_urls) |
| 109 | : KMacroExpanderBase(QLatin1Char('%')) |
| 110 | , ignFile(false) |
| 111 | , urls(_urls) |
| 112 | { |
| 113 | } |
| 114 | |
| 115 | bool ignFile; |
| 116 | |
| 117 | protected: |
| 118 | int expandEscapedMacro(const QString &str, int pos, QStringList &ret) override; |
| 119 | |
| 120 | private: |
| 121 | void subst(int option, const QUrl &url, QStringList &ret); |
| 122 | |
| 123 | const QList<QUrl> &urls; |
| 124 | }; |
| 125 | |
| 126 | void KRunMX2::subst(int option, const QUrl &url, QStringList &ret) |
| 127 | { |
| 128 | switch (option) { |
| 129 | case 'u': |
| 130 | ret << ((url.isLocalFile() && url.fragment().isNull() && url.query().isNull()) ? QDir::toNativeSeparators(pathName: url.toLocalFile()) : url.toString()); |
| 131 | break; |
| 132 | case 'd': |
| 133 | ret << url.adjusted(options: QUrl::RemoveFilename).path(); |
| 134 | break; |
| 135 | case 'f': |
| 136 | ret << QDir::toNativeSeparators(pathName: url.toLocalFile()); |
| 137 | break; |
| 138 | case 'n': |
| 139 | ret << url.fileName(); |
| 140 | break; |
| 141 | case 'v': |
| 142 | if (url.isLocalFile() && QFile::exists(fileName: url.toLocalFile())) { |
| 143 | ret << KDesktopFile(url.toLocalFile()).desktopGroup().readEntry(key: "Dev" ); |
| 144 | } |
| 145 | break; |
| 146 | } |
| 147 | return; |
| 148 | } |
| 149 | |
| 150 | int KRunMX2::expandEscapedMacro(const QString &str, int pos, QStringList &ret) |
| 151 | { |
| 152 | uint option = str[pos + 1].unicode(); |
| 153 | switch (option) { |
| 154 | case 'f': |
| 155 | case 'u': |
| 156 | case 'n': |
| 157 | case 'd': |
| 158 | case 'v': |
| 159 | if (urls.isEmpty()) { |
| 160 | if (!ignFile) { |
| 161 | // qCDebug(KIO_CORE) << "No URLs supplied to single-URL service" << str; |
| 162 | } |
| 163 | } else if (urls.count() > 1) { |
| 164 | qCWarning(KIO_CORE) << urls.count() << "URLs supplied to single-URL service" << str; |
| 165 | } else { |
| 166 | subst(option, url: urls.first(), ret); |
| 167 | } |
| 168 | break; |
| 169 | case 'F': |
| 170 | case 'U': |
| 171 | case 'N': |
| 172 | case 'D': |
| 173 | option += 'a' - 'A'; |
| 174 | for (const QUrl &url : urls) { |
| 175 | subst(option, url, ret); |
| 176 | } |
| 177 | break; |
| 178 | case '%': |
| 179 | ret = QStringList(QStringLiteral("%" )); |
| 180 | break; |
| 181 | default: |
| 182 | return -2; // subst with same and skip |
| 183 | } |
| 184 | return 2; |
| 185 | } |
| 186 | |
| 187 | QStringList KIO::DesktopExecParser::supportedProtocols(const KService &service) |
| 188 | { |
| 189 | QStringList supportedProtocols = service.supportedProtocols(); |
| 190 | |
| 191 | KRunMX1 mx1(service); |
| 192 | QString exec = service.exec(); |
| 193 | if (mx1.expandMacrosShellQuote(str&: exec) && !mx1.hasUrls) { |
| 194 | if (!supportedProtocols.isEmpty()) { |
| 195 | qCWarning(KIO_CORE) << service.entryPath() << "contains supported protocols but doesn't use %u or %U in its Exec line! This is inconsistent." ; |
| 196 | } |
| 197 | return QStringList(); |
| 198 | } else { |
| 199 | if (supportedProtocols.isEmpty()) { |
| 200 | // compat mode: assume KIO if not set and it's a KDE app (or a KDE service) |
| 201 | const QStringList categories = service.property<QStringList>(QStringLiteral("Categories" )); |
| 202 | if (categories.contains(str: QLatin1String("KDE" )) || !service.isApplication() || service.entryPath().isEmpty() /*temp service*/) { |
| 203 | supportedProtocols.append(QStringLiteral("KIO" )); |
| 204 | } else { // if no KDE app, be a bit over-generic |
| 205 | supportedProtocols.append(QStringLiteral("http" )); |
| 206 | supportedProtocols.append(QStringLiteral("https" )); // #253294 |
| 207 | supportedProtocols.append(QStringLiteral("ftp" )); |
| 208 | } |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | // qCDebug(KIO_CORE) << "supportedProtocols:" << supportedProtocols; |
| 213 | return supportedProtocols; |
| 214 | } |
| 215 | |
| 216 | bool KIO::DesktopExecParser::isProtocolInSupportedList(const QUrl &url, const QStringList &supportedProtocols) |
| 217 | { |
| 218 | return url.isLocalFile() // |
| 219 | || supportedProtocols.contains(str: QLatin1String("KIO" )) // |
| 220 | || supportedProtocols.contains(str: url.scheme(), cs: Qt::CaseInsensitive); |
| 221 | } |
| 222 | |
| 223 | // We have up to two sources of data, for protocols not handled by KIO workers (so called "helper") : |
| 224 | // 1) the exec line of the .protocol file, if there's one |
| 225 | // 2) the application associated with x-scheme-handler/<protocol> if there's one |
| 226 | bool KIO::DesktopExecParser::hasSchemeHandler(const QUrl &url) // KF6 TODO move to OpenUrlJob |
| 227 | { |
| 228 | if (KProtocolInfo::isHelperProtocol(url)) { |
| 229 | return true; |
| 230 | } |
| 231 | const KService::Ptr service = KApplicationTrader::preferredService(mimeType: QLatin1String("x-scheme-handler/" ) + url.scheme()); |
| 232 | if (service) { |
| 233 | qCDebug(KIO_CORE) << QLatin1String("preferred service for x-scheme-handler/" ) + url.scheme() << service->desktopEntryName(); |
| 234 | } |
| 235 | return service; |
| 236 | } |
| 237 | |
| 238 | class KIO::DesktopExecParserPrivate |
| 239 | { |
| 240 | public: |
| 241 | DesktopExecParserPrivate(const KService &_service, const QList<QUrl> &_urls) |
| 242 | : service(_service) |
| 243 | , urls(_urls) |
| 244 | , tempFiles(false) |
| 245 | { |
| 246 | } |
| 247 | |
| 248 | bool isUrlSupported(const QUrl &url, const QStringList &supportedProtocols); |
| 249 | |
| 250 | const KService &service; |
| 251 | QList<QUrl> urls; |
| 252 | bool tempFiles; |
| 253 | QString suggestedFileName; |
| 254 | QString m_errorString; |
| 255 | }; |
| 256 | |
| 257 | KIO::DesktopExecParser::DesktopExecParser(const KService &service, const QList<QUrl> &urls) |
| 258 | : d(new DesktopExecParserPrivate(service, urls)) |
| 259 | { |
| 260 | } |
| 261 | |
| 262 | KIO::DesktopExecParser::~DesktopExecParser() |
| 263 | { |
| 264 | } |
| 265 | |
| 266 | void KIO::DesktopExecParser::setUrlsAreTempFiles(bool tempFiles) |
| 267 | { |
| 268 | d->tempFiles = tempFiles; |
| 269 | } |
| 270 | |
| 271 | void KIO::DesktopExecParser::setSuggestedFileName(const QString &suggestedFileName) |
| 272 | { |
| 273 | d->suggestedFileName = suggestedFileName; |
| 274 | } |
| 275 | |
| 276 | static const QString kioexecPath() |
| 277 | { |
| 278 | QString kioexec = QCoreApplication::applicationDirPath() + QLatin1String("/kioexec" ); |
| 279 | if (!QFileInfo::exists(file: kioexec)) { |
| 280 | kioexec = QStringLiteral(KDE_INSTALL_FULL_LIBEXECDIR_KF "/kioexec" ); |
| 281 | } |
| 282 | Q_ASSERT(QFileInfo::exists(kioexec)); |
| 283 | return kioexec; |
| 284 | } |
| 285 | |
| 286 | static QString findNonExecutableProgram(const QString &executable) |
| 287 | { |
| 288 | // Relative to current dir, or absolute path |
| 289 | const QFileInfo fi(executable); |
| 290 | if (fi.exists() && !fi.isExecutable()) { |
| 291 | return executable; |
| 292 | } |
| 293 | |
| 294 | #ifdef Q_OS_UNIX |
| 295 | // This is a *very* simplified version of QStandardPaths::findExecutable |
| 296 | const QStringList searchPaths = QString::fromLocal8Bit(ba: qgetenv(varName: "PATH" )).split(sep: QDir::listSeparator(), behavior: Qt::SkipEmptyParts); |
| 297 | for (const QString &searchPath : searchPaths) { |
| 298 | const QString candidate = searchPath + QLatin1Char('/') + executable; |
| 299 | const QFileInfo fileInfo(candidate); |
| 300 | if (fileInfo.exists()) { |
| 301 | if (fileInfo.isExecutable()) { |
| 302 | qWarning() << "Internal program error. QStandardPaths::findExecutable couldn't find" << executable << "but our own logic found it at" |
| 303 | << candidate << ". Please report a bug at https://bugs.kde.org" ; |
| 304 | } else { |
| 305 | return candidate; |
| 306 | } |
| 307 | } |
| 308 | } |
| 309 | #endif |
| 310 | return QString(); |
| 311 | } |
| 312 | |
| 313 | bool KIO::DesktopExecParserPrivate::isUrlSupported(const QUrl &url, const QStringList &protocols) |
| 314 | { |
| 315 | if (KIO::DesktopExecParser::isProtocolInSupportedList(url, supportedProtocols: protocols)) { |
| 316 | return true; |
| 317 | } |
| 318 | |
| 319 | // supportedProtocols() only checks whether the .desktop file has MimeType=x-scheme-handler/xxx |
| 320 | // We also want to check whether the app has been set as default/associated in mimeapps.list |
| 321 | const auto handlers = KApplicationTrader::queryByMimeType(mimeType: QLatin1String("x-scheme-handler/" ) + url.scheme()); |
| 322 | for (const KService::Ptr &handler : handlers) { |
| 323 | if (handler->desktopEntryName() == service.desktopEntryName()) { |
| 324 | return true; |
| 325 | } |
| 326 | } |
| 327 | |
| 328 | return false; |
| 329 | } |
| 330 | |
| 331 | QStringList KIO::DesktopExecParser::resultingArguments() const |
| 332 | { |
| 333 | QString exec = d->service.exec(); |
| 334 | if (exec.isEmpty()) { |
| 335 | d->m_errorString = i18n("No Exec field in %1" , d->service.entryPath()); |
| 336 | qCWarning(KIO_CORE) << "No Exec field in" << d->service.entryPath(); |
| 337 | return QStringList(); |
| 338 | } |
| 339 | |
| 340 | // Extract the name of the binary to execute from the full Exec line, to see if it exists |
| 341 | const QString binary = executablePath(execLine: exec); |
| 342 | QString executableFullPath; |
| 343 | if (!binary.isEmpty()) { // skip all this if the Exec line is a complex shell command |
| 344 | if (QDir::isRelativePath(path: binary)) { |
| 345 | // Resolve the executable to ensure that helpers in libexec are found. |
| 346 | // Too bad for commands that need a shell - they must reside in $PATH. |
| 347 | executableFullPath = QStandardPaths::findExecutable(executableName: binary); |
| 348 | if (executableFullPath.isEmpty()) { |
| 349 | executableFullPath = QFile::decodeName(KDE_INSTALL_FULL_LIBEXECDIR_KF "/" ) + binary; |
| 350 | } |
| 351 | } else { |
| 352 | executableFullPath = binary; |
| 353 | } |
| 354 | |
| 355 | // Now check that the binary exists and has the executable flag |
| 356 | if (!QFileInfo(executableFullPath).isExecutable()) { |
| 357 | // Does it really not exist, or is it non-executable (on Unix)? (bug #415567) |
| 358 | const QString nonExecutable = findNonExecutableProgram(executable: binary); |
| 359 | if (nonExecutable.isEmpty()) { |
| 360 | d->m_errorString = i18n("Could not find the program '%1'" , binary); |
| 361 | } else { |
| 362 | if (QDir::isRelativePath(path: binary)) { |
| 363 | d->m_errorString = i18n("The program '%1' was found at '%2' but it is missing executable permissions." , binary, nonExecutable); |
| 364 | } else { |
| 365 | d->m_errorString = i18n("The program '%1' is missing executable permissions." , nonExecutable); |
| 366 | } |
| 367 | } |
| 368 | return QStringList(); |
| 369 | } |
| 370 | } |
| 371 | |
| 372 | QStringList result; |
| 373 | bool appHasTempFileOption; |
| 374 | |
| 375 | KRunMX1 mx1(d->service); |
| 376 | KRunMX2 mx2(d->urls); |
| 377 | |
| 378 | if (!mx1.expandMacrosShellQuote(str&: exec) || mx1.hasError) { // Error in shell syntax |
| 379 | d->m_errorString = i18n("Syntax error in command %1 coming from %2" , exec, d->service.entryPath()); |
| 380 | qCWarning(KIO_CORE) << "Syntax error in command" << d->service.exec() << ", service" << d->service.name(); |
| 381 | return QStringList(); |
| 382 | } |
| 383 | |
| 384 | // FIXME: the current way of invoking kioexec disables term and su use |
| 385 | |
| 386 | // Check if we need "tempexec" (kioexec in fact) |
| 387 | appHasTempFileOption = d->tempFiles && d->service.property<bool>(QStringLiteral("X-KDE-HasTempFileOption" )); |
| 388 | if (d->tempFiles && !appHasTempFileOption && d->urls.size()) { |
| 389 | result << kioexecPath() << QStringLiteral("--tempfiles" ) << exec; |
| 390 | if (!d->suggestedFileName.isEmpty()) { |
| 391 | result << QStringLiteral("--suggestedfilename" ); |
| 392 | result << d->suggestedFileName; |
| 393 | } |
| 394 | result += QUrl::toStringList(uris: d->urls); |
| 395 | return result; |
| 396 | } |
| 397 | |
| 398 | #ifdef WITH_QTDBUS |
| 399 | // Return true for non-KIO desktop files with explicit X-KDE-Protocols list, like vlc, for the special case below |
| 400 | auto isNonKIO = [this]() { |
| 401 | const QStringList protocols = d->service.property<QStringList>(QStringLiteral("X-KDE-Protocols" )); |
| 402 | return !protocols.isEmpty() && !protocols.contains(str: QLatin1String("KIO" )); |
| 403 | }; |
| 404 | |
| 405 | // Check if we need kioexec, or KIOFuse |
| 406 | bool useKioexec = false; |
| 407 | |
| 408 | org::kde::KIOFuse::VFS kiofuse_iface(QStringLiteral("org.kde.KIOFuse" ), QStringLiteral("/org/kde/KIOFuse" ), QDBusConnection::sessionBus()); |
| 409 | struct MountRequest { |
| 410 | QDBusPendingReply<QString> reply; |
| 411 | int urlIndex; |
| 412 | }; |
| 413 | QList<MountRequest> requests; |
| 414 | requests.reserve(asize: d->urls.count()); |
| 415 | |
| 416 | const QStringList appSupportedProtocols = supportedProtocols(service: d->service); |
| 417 | for (int i = 0; i < d->urls.count(); ++i) { |
| 418 | const QUrl url = d->urls.at(i); |
| 419 | const bool supported = mx1.hasUrls ? d->isUrlSupported(url, protocols: appSupportedProtocols) : url.isLocalFile(); |
| 420 | if (!supported) { |
| 421 | // If FUSE fails, and there is no scheme handler, we'll have to fallback to kioexec |
| 422 | useKioexec = true; |
| 423 | } |
| 424 | |
| 425 | // NOTE: Some non-KIO apps may support the URLs (e.g. VLC supports smb://) |
| 426 | // but will not have the password if they are not in the URL itself. |
| 427 | // Hence convert URL to KIOFuse equivalent in case there is a password. |
| 428 | // \sa https://pointieststick.com/2018/01/17/videos-on-samba-shares/ |
| 429 | // \sa https://bugs.kde.org/show_bug.cgi?id=330192 |
| 430 | if (!supported || (!url.userName().isEmpty() && url.password().isEmpty() && isNonKIO())) { |
| 431 | requests.push_back(t: {.reply: kiofuse_iface.mountUrl(remoteUrl: url.toString()), .urlIndex: i}); |
| 432 | } |
| 433 | } |
| 434 | |
| 435 | for (auto &request : requests) { |
| 436 | request.reply.waitForFinished(); |
| 437 | } |
| 438 | const bool fuseError = std::any_of(first: requests.cbegin(), last: requests.cend(), pred: [](const MountRequest &request) { |
| 439 | return request.reply.isError(); |
| 440 | }); |
| 441 | |
| 442 | if (fuseError && useKioexec) { |
| 443 | // We need to run the app through kioexec |
| 444 | result << kioexecPath(); |
| 445 | if (d->tempFiles) { |
| 446 | result << QStringLiteral("--tempfiles" ); |
| 447 | } |
| 448 | if (!d->suggestedFileName.isEmpty()) { |
| 449 | result << QStringLiteral("--suggestedfilename" ); |
| 450 | result << d->suggestedFileName; |
| 451 | } |
| 452 | result << exec; |
| 453 | result += QUrl::toStringList(uris: d->urls); |
| 454 | return result; |
| 455 | } |
| 456 | |
| 457 | // At this point we know we're not using kioexec, so feel free to replace |
| 458 | // KIO URLs with their KIOFuse local path. |
| 459 | for (const auto &request : std::as_const(t&: requests)) { |
| 460 | if (!request.reply.isError()) { |
| 461 | d->urls[request.urlIndex] = QUrl::fromLocalFile(localfile: request.reply.value()); |
| 462 | } |
| 463 | } |
| 464 | #endif |
| 465 | |
| 466 | if (appHasTempFileOption) { |
| 467 | exec += QLatin1String(" --tempfile" ); |
| 468 | } |
| 469 | |
| 470 | // Did the user forget to append something like '%f'? |
| 471 | // If so, then assume that '%f' is the right choice => the application |
| 472 | // accepts only local files. |
| 473 | if (!mx1.hasSpec) { |
| 474 | exec += QLatin1String(" %f" ); |
| 475 | mx2.ignFile = true; |
| 476 | } |
| 477 | |
| 478 | mx2.expandMacrosShellQuote(str&: exec); // syntax was already checked, so don't check return value |
| 479 | |
| 480 | /* |
| 481 | 1 = need_shell, 2 = terminal, 4 = su |
| 482 | |
| 483 | 0 << split(cmd) |
| 484 | 1 << "sh" << "-c" << cmd |
| 485 | 2 << split(term) << "-e" << split(cmd) |
| 486 | 3 << split(term) << "-e" << "sh" << "-c" << cmd |
| 487 | |
| 488 | 4 << "kdesu" << "-u" << user << "-c" << cmd |
| 489 | 5 << "kdesu" << "-u" << user << "-c" << ("sh -c " + quote(cmd)) |
| 490 | 6 << split(term) << "-e" << "su" << user << "-c" << cmd |
| 491 | 7 << split(term) << "-e" << "su" << user << "-c" << ("sh -c " + quote(cmd)) |
| 492 | |
| 493 | "sh -c" is needed in the "su" case, too, as su uses the user's login shell, not sh. |
| 494 | this could be optimized with the -s switch of some su versions (e.g., debian linux). |
| 495 | */ |
| 496 | |
| 497 | if (d->service.terminal()) { |
| 498 | KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("General" )); |
| 499 | QString terminal = cg.readPathEntry(key: "TerminalApplication" , QStringLiteral("konsole" )); |
| 500 | |
| 501 | const bool isKonsole = (terminal == QLatin1String("konsole" )); |
| 502 | QStringList terminalParts = KShell::splitArgs(cmd: terminal); |
| 503 | QString terminalPath; |
| 504 | if (!terminalParts.isEmpty()) { |
| 505 | terminalPath = QStandardPaths::findExecutable(executableName: terminalParts.at(i: 0)); |
| 506 | } |
| 507 | |
| 508 | if (terminalPath.isEmpty()) { |
| 509 | d->m_errorString = i18n("Terminal %1 not found while trying to run %2" , terminal, d->service.entryPath()); |
| 510 | qCWarning(KIO_CORE) << "Terminal" << terminal << "not found, service" << d->service.name(); |
| 511 | return QStringList(); |
| 512 | } |
| 513 | terminalParts[0] = terminalPath; |
| 514 | terminal = KShell::joinArgs(args: terminalParts); |
| 515 | if (isKonsole) { |
| 516 | if (!d->service.workingDirectory().isEmpty()) { |
| 517 | terminal += QLatin1String(" --workdir " ) + KShell::quoteArg(arg: d->service.workingDirectory()); |
| 518 | } |
| 519 | terminal += QLatin1String(" -qwindowtitle '%c'" ); |
| 520 | if (!d->service.icon().isEmpty()) { |
| 521 | terminal += QLatin1String(" -qwindowicon " ) + KShell::quoteArg(arg: d->service.icon().replace(c: QLatin1Char('%'), after: QLatin1String("%%" ))); |
| 522 | } |
| 523 | } |
| 524 | terminal += QLatin1Char(' ') + d->service.terminalOptions(); |
| 525 | if (!mx1.expandMacrosShellQuote(str&: terminal) || mx1.hasError) { |
| 526 | d->m_errorString = i18n("Syntax error in command %1 while trying to run %2" , terminal, d->service.entryPath()); |
| 527 | qCWarning(KIO_CORE) << "Syntax error in command" << terminal << ", service" << d->service.name(); |
| 528 | return QStringList(); |
| 529 | } |
| 530 | mx2.expandMacrosShellQuote(str&: terminal); |
| 531 | result = KShell::splitArgs(cmd: terminal); // assuming that the term spec never needs a shell! |
| 532 | result << QStringLiteral("-e" ); |
| 533 | } |
| 534 | |
| 535 | KShell::Errors err; |
| 536 | QStringList execlist = KShell::splitArgs(cmd: exec, flags: KShell::AbortOnMeta | KShell::TildeExpand, err: &err); |
| 537 | if (!executableFullPath.isEmpty()) { |
| 538 | execlist[0] = executableFullPath; |
| 539 | } |
| 540 | |
| 541 | if (d->service.substituteUid()) { |
| 542 | if (d->service.terminal()) { |
| 543 | result << QStringLiteral("su" ); |
| 544 | } else { |
| 545 | QString kdesu = QFile::decodeName(KDE_INSTALL_FULL_LIBEXECDIR_KF "/kdesu" ); |
| 546 | if (!QFile::exists(fileName: kdesu)) { |
| 547 | kdesu = QStandardPaths::findExecutable(QStringLiteral("kdesu" )); |
| 548 | } |
| 549 | if (!QFile::exists(fileName: kdesu)) { |
| 550 | // Insert kdesu as string so we show a nice warning: 'Could not launch kdesu' |
| 551 | result << QStringLiteral("kdesu" ); |
| 552 | return result; |
| 553 | } else { |
| 554 | result << kdesu << QStringLiteral("-u" ); |
| 555 | } |
| 556 | } |
| 557 | |
| 558 | result << d->service.username() << QStringLiteral("-c" ); |
| 559 | if (err == KShell::FoundMeta) { |
| 560 | exec = QLatin1String("/bin/sh -c " ) + KShell::quoteArg(arg: exec); |
| 561 | } else { |
| 562 | exec = KShell::joinArgs(args: execlist); |
| 563 | } |
| 564 | result << exec; |
| 565 | } else { |
| 566 | if (err == KShell::FoundMeta) { |
| 567 | result << QStringLiteral("/bin/sh" ) << QStringLiteral("-c" ) << exec; |
| 568 | } else { |
| 569 | result += execlist; |
| 570 | } |
| 571 | } |
| 572 | |
| 573 | return result; |
| 574 | } |
| 575 | |
| 576 | QString KIO::DesktopExecParser::errorMessage() const |
| 577 | { |
| 578 | return d->m_errorString; |
| 579 | } |
| 580 | |
| 581 | // static |
| 582 | QString KIO::DesktopExecParser::executableName(const QString &execLine) |
| 583 | { |
| 584 | const QString bin = executablePath(execLine); |
| 585 | return bin.mid(position: bin.lastIndexOf(c: QLatin1Char('/')) + 1); |
| 586 | } |
| 587 | |
| 588 | // static |
| 589 | QString KIO::DesktopExecParser::executablePath(const QString &execLine) |
| 590 | { |
| 591 | // Remove parameters and/or trailing spaces. |
| 592 | const QStringList args = KShell::splitArgs(cmd: execLine, flags: KShell::AbortOnMeta | KShell::TildeExpand); |
| 593 | auto it = std::find_if(first: args.cbegin(), last: args.cend(), pred: [](const QString &arg) { |
| 594 | return !arg.contains(c: QLatin1Char('=')); |
| 595 | }); |
| 596 | return it != args.cend() ? *it : QString{}; |
| 597 | } |
| 598 | |