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

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.property(QStringLiteral("X-KDE-Protocols")).toStringList();
0183     KRunMX1 mx1(service);
0184     QString exec = service.exec();
0185     if (mx1.expandMacrosShellQuote(exec) && !mx1.hasUrls) {
0186         if (!supportedProtocols.isEmpty()) {
0187             qCWarning(KIO_CORE) << service.entryPath() << "contains a X-KDE-Protocols line but doesn't use %u or %U in its Exec line! This is inconsistent.";
0188         }
0189         return QStringList();
0190     } else {
0191         if (supportedProtocols.isEmpty()) {
0192             // compat mode: assume KIO if not set and it's a KDE app (or a KDE service)
0193             const QStringList categories = service.property(QStringLiteral("Categories")).toStringList();
0194             if (categories.contains(QLatin1String("KDE")) || !service.isApplication() || service.entryPath().isEmpty() /*temp service*/) {
0195                 supportedProtocols.append(QStringLiteral("KIO"));
0196             } else { // if no KDE app, be a bit over-generic
0197                 supportedProtocols.append(QStringLiteral("http"));
0198                 supportedProtocols.append(QStringLiteral("https")); // #253294
0199                 supportedProtocols.append(QStringLiteral("ftp"));
0200             }
0201         }
0202     }
0203 
0204     // add x-scheme-handler/<protocol>
0205     const QLatin1String xScheme("x-scheme-handler/");
0206     const auto servicesTypes = service.serviceTypes();
0207     for (const auto &mimeType : servicesTypes) {
0208         if (mimeType.startsWith(xScheme)) {
0209             supportedProtocols << mimeType.mid(xScheme.size());
0210         }
0211     }
0212 
0213     // qCDebug(KIO_CORE) << "supportedProtocols:" << supportedProtocols;
0214     return supportedProtocols;
0215 }
0216 
0217 bool KIO::DesktopExecParser::isProtocolInSupportedList(const QUrl &url, const QStringList &supportedProtocols)
0218 {
0219     return url.isLocalFile() //
0220         || supportedProtocols.contains(QLatin1String("KIO")) //
0221         || supportedProtocols.contains(url.scheme(), Qt::CaseInsensitive);
0222 }
0223 
0224 // We have up to two sources of data, for protocols not handled by KIO workers (so called "helper") :
0225 // 1) the exec line of the .protocol file, if there's one
0226 // 2) the application associated with x-scheme-handler/<protocol> if there's one
0227 bool KIO::DesktopExecParser::hasSchemeHandler(const QUrl &url) // KF6 TODO move to OpenUrlJob
0228 {
0229     if (KProtocolInfo::isHelperProtocol(url)) {
0230         return true;
0231     }
0232     const KService::Ptr service = KApplicationTrader::preferredService(QLatin1String("x-scheme-handler/") + url.scheme());
0233     if (service) {
0234         qCDebug(KIO_CORE) << QLatin1String("preferred service for x-scheme-handler/") + url.scheme() << service->desktopEntryName();
0235     }
0236     return service;
0237 }
0238 
0239 class KIO::DesktopExecParserPrivate
0240 {
0241 public:
0242     DesktopExecParserPrivate(const KService &_service, const QList<QUrl> &_urls)
0243         : service(_service)
0244         , urls(_urls)
0245         , tempFiles(false)
0246     {
0247     }
0248 
0249     bool isUrlSupported(const QUrl &url, const QStringList &supportedProtocols);
0250 
0251     const KService &service;
0252     QList<QUrl> urls;
0253     bool tempFiles;
0254     QString suggestedFileName;
0255     QString m_errorString;
0256 };
0257 
0258 KIO::DesktopExecParser::DesktopExecParser(const KService &service, const QList<QUrl> &urls)
0259     : d(new DesktopExecParserPrivate(service, urls))
0260 {
0261 }
0262 
0263 KIO::DesktopExecParser::~DesktopExecParser()
0264 {
0265 }
0266 
0267 void KIO::DesktopExecParser::setUrlsAreTempFiles(bool tempFiles)
0268 {
0269     d->tempFiles = tempFiles;
0270 }
0271 
0272 void KIO::DesktopExecParser::setSuggestedFileName(const QString &suggestedFileName)
0273 {
0274     d->suggestedFileName = suggestedFileName;
0275 }
0276 
0277 static const QString kioexecPath()
0278 {
0279     QString kioexec = QCoreApplication::applicationDirPath() + QLatin1String("/kioexec");
0280     if (!QFileInfo::exists(kioexec)) {
0281         kioexec = QStringLiteral(KDE_INSTALL_FULL_LIBEXECDIR_KF "/kioexec");
0282     }
0283     Q_ASSERT(QFileInfo::exists(kioexec));
0284     return kioexec;
0285 }
0286 
0287 static QString findNonExecutableProgram(const QString &executable)
0288 {
0289     // Relative to current dir, or absolute path
0290     const QFileInfo fi(executable);
0291     if (fi.exists() && !fi.isExecutable()) {
0292         return executable;
0293     }
0294 
0295 #ifdef Q_OS_UNIX
0296     // This is a *very* simplified version of QStandardPaths::findExecutable
0297     const QStringList searchPaths = QString::fromLocal8Bit(qgetenv("PATH")).split(QDir::listSeparator(), Qt::SkipEmptyParts);
0298     for (const QString &searchPath : searchPaths) {
0299         const QString candidate = searchPath + QLatin1Char('/') + executable;
0300         const QFileInfo fileInfo(candidate);
0301         if (fileInfo.exists()) {
0302             if (fileInfo.isExecutable()) {
0303                 qWarning() << "Internal program error. QStandardPaths::findExecutable couldn't find" << executable << "but our own logic found it at"
0304                            << candidate << ". Please report a bug at https://bugs.kde.org";
0305             } else {
0306                 return candidate;
0307             }
0308         }
0309     }
0310 #endif
0311     return QString();
0312 }
0313 
0314 bool KIO::DesktopExecParserPrivate::isUrlSupported(const QUrl &url, const QStringList &protocols)
0315 {
0316     if (KIO::DesktopExecParser::isProtocolInSupportedList(url, protocols)) {
0317         return true;
0318     }
0319 
0320     // supportedProtocols() only checks whether the .desktop file has MimeType=x-scheme-handler/xxx
0321     // We also want to check whether the app has been set as default/associated in mimeapps.list
0322     const auto handlers = KApplicationTrader::queryByMimeType(QLatin1String("x-scheme-handler/") + url.scheme());
0323     for (const KService::Ptr &handler : handlers) {
0324         if (handler->desktopEntryName() == service.desktopEntryName()) {
0325             return true;
0326         }
0327     }
0328 
0329     return false;
0330 }
0331 
0332 QStringList KIO::DesktopExecParser::resultingArguments() const
0333 {
0334     QString exec = d->service.exec();
0335     if (exec.isEmpty()) {
0336         d->m_errorString = i18n("No Exec field in %1", d->service.entryPath());
0337         qCWarning(KIO_CORE) << "No Exec field in" << d->service.entryPath();
0338         return QStringList();
0339     }
0340 
0341     // Extract the name of the binary to execute from the full Exec line, to see if it exists
0342     const QString binary = executablePath(exec);
0343     QString executableFullPath;
0344     if (!binary.isEmpty()) { // skip all this if the Exec line is a complex shell command
0345         if (QDir::isRelativePath(binary)) {
0346             // Resolve the executable to ensure that helpers in libexec are found.
0347             // Too bad for commands that need a shell - they must reside in $PATH.
0348             executableFullPath = QStandardPaths::findExecutable(binary);
0349             if (executableFullPath.isEmpty()) {
0350                 executableFullPath = QFile::decodeName(KDE_INSTALL_FULL_LIBEXECDIR_KF "/") + binary;
0351             }
0352         } else {
0353             executableFullPath = binary;
0354         }
0355 
0356         // Now check that the binary exists and has the executable flag
0357         if (!QFileInfo(executableFullPath).isExecutable()) {
0358             // Does it really not exist, or is it non-executable (on Unix)? (bug #415567)
0359             const QString nonExecutable = findNonExecutableProgram(binary);
0360             if (nonExecutable.isEmpty()) {
0361                 d->m_errorString = i18n("Could not find the program '%1'", binary);
0362             } else {
0363                 if (QDir::isRelativePath(binary)) {
0364                     d->m_errorString = i18n("The program '%1' was found at '%2' but it is missing executable permissions.", binary, nonExecutable);
0365                 } else {
0366                     d->m_errorString = i18n("The program '%1' is missing executable permissions.", nonExecutable);
0367                 }
0368             }
0369             return QStringList();
0370         }
0371     }
0372 
0373     QStringList result;
0374     bool appHasTempFileOption;
0375 
0376     KRunMX1 mx1(d->service);
0377     KRunMX2 mx2(d->urls);
0378 
0379     if (!mx1.expandMacrosShellQuote(exec)) { // Error in shell syntax
0380         d->m_errorString = i18n("Syntax error in command %1 coming from %2", exec, d->service.entryPath());
0381         qCWarning(KIO_CORE) << "Syntax error in command" << d->service.exec() << ", service" << d->service.name();
0382         return QStringList();
0383     }
0384 
0385     // FIXME: the current way of invoking kioexec disables term and su use
0386 
0387     // Check if we need "tempexec" (kioexec in fact)
0388     appHasTempFileOption = d->tempFiles && d->service.property(QStringLiteral("X-KDE-HasTempFileOption")).toBool();
0389     if (d->tempFiles && !appHasTempFileOption && d->urls.size()) {
0390         result << kioexecPath() << QStringLiteral("--tempfiles") << exec;
0391         if (!d->suggestedFileName.isEmpty()) {
0392             result << QStringLiteral("--suggestedfilename");
0393             result << d->suggestedFileName;
0394         }
0395         result += QUrl::toStringList(d->urls);
0396         return result;
0397     }
0398 
0399     // Return true for non-KIO desktop files with explicit X-KDE-Protocols list, like vlc, for the special case below
0400     auto isNonKIO = [this]() {
0401         const QStringList protocols = d->service.property(QStringLiteral("X-KDE-Protocols")).toStringList();
0402         return !protocols.isEmpty() && !protocols.contains(QLatin1String("KIO"));
0403     };
0404 
0405     // Check if we need kioexec, or KIOFuse
0406     bool useKioexec = false;
0407 #ifndef Q_OS_ANDROID
0408     org::kde::KIOFuse::VFS kiofuse_iface(QStringLiteral("org.kde.KIOFuse"), QStringLiteral("/org/kde/KIOFuse"), QDBusConnection::sessionBus());
0409     struct MountRequest {
0410         QDBusPendingReply<QString> reply;
0411         int urlIndex;
0412     };
0413     QVector<MountRequest> requests;
0414     requests.reserve(d->urls.count());
0415 
0416     const QStringList appSupportedProtocols = supportedProtocols(d->service);
0417     for (int i = 0; i < d->urls.count(); ++i) {
0418         const QUrl url = d->urls.at(i);
0419         const bool supported = mx1.hasUrls ? d->isUrlSupported(url, appSupportedProtocols) : url.isLocalFile();
0420         if (!supported) {
0421             // If FUSE fails, and there is no scheme handler, we'll have to fallback to kioexec
0422             useKioexec = true;
0423         }
0424 
0425         // NOTE: Some non-KIO apps may support the URLs (e.g. VLC supports smb://)
0426         // but will not have the password if they are not in the URL itself.
0427         // Hence convert URL to KIOFuse equivalent in case there is a password.
0428         // @see https://pointieststick.com/2018/01/17/videos-on-samba-shares/
0429         // @see https://bugs.kde.org/show_bug.cgi?id=330192
0430         if (!supported || (!url.userName().isEmpty() && url.password().isEmpty() && isNonKIO())) {
0431             requests.push_back({kiofuse_iface.mountUrl(url.toString()), i});
0432         }
0433     }
0434 
0435     for (auto &request : requests) {
0436         request.reply.waitForFinished();
0437     }
0438     const bool fuseError = std::any_of(requests.cbegin(), requests.cend(), [](const MountRequest &request) {
0439         return request.reply.isError();
0440     });
0441 
0442     if (fuseError && useKioexec) {
0443         // We need to run the app through kioexec
0444         result << kioexecPath();
0445         if (d->tempFiles) {
0446             result << QStringLiteral("--tempfiles");
0447         }
0448         if (!d->suggestedFileName.isEmpty()) {
0449             result << QStringLiteral("--suggestedfilename");
0450             result << d->suggestedFileName;
0451         }
0452         result << exec;
0453         result += QUrl::toStringList(d->urls);
0454         return result;
0455     }
0456 
0457     // At this point we know we're not using kioexec, so feel free to replace
0458     // KIO URLs with their KIOFuse local path.
0459     for (const auto &request : std::as_const(requests)) {
0460         if (!request.reply.isError()) {
0461             d->urls[request.urlIndex] = QUrl::fromLocalFile(request.reply.value());
0462         }
0463     }
0464 #endif
0465 
0466     if (appHasTempFileOption) {
0467         exec += QLatin1String(" --tempfile");
0468     }
0469 
0470     // Did the user forget to append something like '%f'?
0471     // If so, then assume that '%f' is the right choice => the application
0472     // accepts only local files.
0473     if (!mx1.hasSpec) {
0474         exec += QLatin1String(" %f");
0475         mx2.ignFile = true;
0476     }
0477 
0478     mx2.expandMacrosShellQuote(exec); // syntax was already checked, so don't check return value
0479 
0480     /*
0481      1 = need_shell, 2 = terminal, 4 = su
0482 
0483      0                                                           << split(cmd)
0484      1                                                           << "sh" << "-c" << cmd
0485      2 << split(term) << "-e"                                    << split(cmd)
0486      3 << split(term) << "-e"                                    << "sh" << "-c" << cmd
0487 
0488      4                        << "kdesu" << "-u" << user << "-c" << cmd
0489      5                        << "kdesu" << "-u" << user << "-c" << ("sh -c " + quote(cmd))
0490      6 << split(term) << "-e" << "su"            << user << "-c" << cmd
0491      7 << split(term) << "-e" << "su"            << user << "-c" << ("sh -c " + quote(cmd))
0492 
0493      "sh -c" is needed in the "su" case, too, as su uses the user's login shell, not sh.
0494      this could be optimized with the -s switch of some su versions (e.g., debian linux).
0495     */
0496 
0497     if (d->service.terminal()) {
0498         KConfigGroup cg(KSharedConfig::openConfig(), "General");
0499         QString terminal = cg.readPathEntry("TerminalApplication", QStringLiteral("konsole"));
0500 
0501         const bool isKonsole = (terminal == QLatin1String("konsole"));
0502         QStringList terminalParts = KShell::splitArgs(terminal);
0503         QString terminalPath;
0504         if (!terminalParts.isEmpty()) {
0505             terminalPath = QStandardPaths::findExecutable(terminalParts.at(0));
0506         }
0507 
0508         if (terminalPath.isEmpty()) {
0509             d->m_errorString = i18n("Terminal %1 not found while trying to run %2", terminal, d->service.entryPath());
0510             qCWarning(KIO_CORE) << "Terminal" << terminal << "not found, service" << d->service.name();
0511             return QStringList();
0512         }
0513         terminalParts[0] = terminalPath;
0514         terminal = KShell::joinArgs(terminalParts);
0515         if (isKonsole) {
0516             if (!d->service.workingDirectory().isEmpty()) {
0517                 terminal += QLatin1String(" --workdir ") + KShell::quoteArg(d->service.workingDirectory());
0518             }
0519             terminal += QLatin1String(" -qwindowtitle '%c'");
0520             if (!d->service.icon().isEmpty()) {
0521                 terminal += QLatin1String(" -qwindowicon ") + KShell::quoteArg(d->service.icon().replace(QLatin1Char('%'), QLatin1String("%%")));
0522             }
0523         }
0524         terminal += QLatin1Char(' ') + d->service.terminalOptions();
0525         if (!mx1.expandMacrosShellQuote(terminal)) {
0526             d->m_errorString = i18n("Syntax error in command %1 while trying to run %2", terminal, d->service.entryPath());
0527             qCWarning(KIO_CORE) << "Syntax error in command" << terminal << ", service" << d->service.name();
0528             return QStringList();
0529         }
0530         mx2.expandMacrosShellQuote(terminal);
0531         result = KShell::splitArgs(terminal); // assuming that the term spec never needs a shell!
0532         result << QStringLiteral("-e");
0533     }
0534 
0535     KShell::Errors err;
0536     QStringList execlist = KShell::splitArgs(exec, KShell::AbortOnMeta | KShell::TildeExpand, &err);
0537     if (!executableFullPath.isEmpty()) {
0538         execlist[0] = executableFullPath;
0539     }
0540 
0541     if (d->service.substituteUid()) {
0542         if (d->service.terminal()) {
0543             result << QStringLiteral("su");
0544         } else {
0545             QString kdesu = QFile::decodeName(KDE_INSTALL_FULL_LIBEXECDIR_KF "/kdesu");
0546             if (!QFile::exists(kdesu)) {
0547                 kdesu = QStandardPaths::findExecutable(QStringLiteral("kdesu"));
0548             }
0549             if (!QFile::exists(kdesu)) {
0550                 // Insert kdesu as string so we show a nice warning: 'Could not launch kdesu'
0551                 result << QStringLiteral("kdesu");
0552                 return result;
0553             } else {
0554                 result << kdesu << QStringLiteral("-u");
0555             }
0556         }
0557 
0558         result << d->service.username() << QStringLiteral("-c");
0559         if (err == KShell::FoundMeta) {
0560             exec = QLatin1String("/bin/sh -c ") + KShell::quoteArg(exec);
0561         } else {
0562             exec = KShell::joinArgs(execlist);
0563         }
0564         result << exec;
0565     } else {
0566         if (err == KShell::FoundMeta) {
0567             result << QStringLiteral("/bin/sh") << QStringLiteral("-c") << exec;
0568         } else {
0569             result += execlist;
0570         }
0571     }
0572 
0573     return result;
0574 }
0575 
0576 QString KIO::DesktopExecParser::errorMessage() const
0577 {
0578     return d->m_errorString;
0579 }
0580 
0581 // static
0582 QString KIO::DesktopExecParser::executableName(const QString &execLine)
0583 {
0584     const QString bin = executablePath(execLine);
0585     return bin.mid(bin.lastIndexOf(QLatin1Char('/')) + 1);
0586 }
0587 
0588 // static
0589 QString KIO::DesktopExecParser::executablePath(const QString &execLine)
0590 {
0591     // Remove parameters and/or trailing spaces.
0592     const QStringList args = KShell::splitArgs(execLine, KShell::AbortOnMeta | KShell::TildeExpand);
0593     auto it = std::find_if(args.cbegin(), args.cend(), [](const QString &arg) {
0594         return !arg.contains(QLatin1Char('='));
0595     });
0596     return it != args.cend() ? *it : QString{};
0597 }