File indexing completed on 2024-04-28 15:29:49

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 1997, 1998 Matthias Kalle Dalheimer <kalle@kde.org>
0004     SPDX-FileCopyrightText: 1999 Espen Sand <espen@kde.org>
0005     SPDX-FileCopyrightText: 2000-2004 Frerich Raabe <raabe@kde.org>
0006     SPDX-FileCopyrightText: 2003, 2004 Oswald Buddenhagen <ossi@kde.org>
0007     SPDX-FileCopyrightText: 2006 Thiago Macieira <thiago@kde.org>
0008     SPDX-FileCopyrightText: 2008 Aaron Seigo <aseigo@kde.org>
0009 
0010     SPDX-License-Identifier: LGPL-2.0-or-later
0011 */
0012 
0013 #include "ktoolinvocation.h"
0014 
0015 #include <KApplicationTrader>
0016 #include <KConfigGroup>
0017 #include <KSharedConfig>
0018 
0019 #include "kservice.h"
0020 #include <KConfig>
0021 #include <KLocalizedString>
0022 #include <KMacroExpander>
0023 #include <KMessage>
0024 #include <KShell>
0025 
0026 #include <QDebug>
0027 #include <QHash>
0028 #include <QStandardPaths>
0029 #include <QUrl>
0030 #include <QUrlQuery>
0031 
0032 static QStringList splitEmailAddressList(const QString &aStr)
0033 {
0034     // This is a copy of KPIM::splitEmailAddrList().
0035     // Features:
0036     // - always ignores quoted characters
0037     // - ignores everything (including parentheses and commas)
0038     //   inside quoted strings
0039     // - supports nested comments
0040     // - ignores everything (including double quotes and commas)
0041     //   inside comments
0042 
0043     QStringList list;
0044 
0045     if (aStr.isEmpty()) {
0046         return list;
0047     }
0048 
0049     QString addr;
0050     int addrstart = 0;
0051     int commentlevel = 0;
0052     bool insidequote = false;
0053 
0054     for (int index = 0; index < aStr.length(); index++) {
0055         // the following conversion to latin1 is o.k. because
0056         // we can safely ignore all non-latin1 characters
0057         switch (aStr[index].toLatin1()) {
0058         case '"': // start or end of quoted string
0059             if (commentlevel == 0) {
0060                 insidequote = !insidequote;
0061             }
0062             break;
0063         case '(': // start of comment
0064             if (!insidequote) {
0065                 commentlevel++;
0066             }
0067             break;
0068         case ')': // end of comment
0069             if (!insidequote) {
0070                 if (commentlevel > 0) {
0071                     commentlevel--;
0072                 } else {
0073                     // qDebug() << "Error in address splitting: Unmatched ')'"
0074                     //          << endl;
0075                     return list;
0076                 }
0077             }
0078             break;
0079         case '\\': // quoted character
0080             index++; // ignore the quoted character
0081             break;
0082         case ',':
0083             if (!insidequote && (commentlevel == 0)) {
0084                 addr = aStr.mid(addrstart, index - addrstart);
0085                 if (!addr.isEmpty()) {
0086                     list += addr.simplified();
0087                 }
0088                 addrstart = index + 1;
0089             }
0090             break;
0091         }
0092     }
0093     // append the last address to the list
0094     if (!insidequote && (commentlevel == 0)) {
0095         addr = aStr.mid(addrstart, aStr.length() - addrstart);
0096         if (!addr.isEmpty()) {
0097             list += addr.simplified();
0098         }
0099     }
0100     // else
0101     //  qDebug() << "Error in address splitting: "
0102     //            << "Unexpected end of address list"
0103     //            << endl;
0104 
0105     return list;
0106 }
0107 
0108 void KToolInvocation::invokeMailer(const QString &_to,
0109                                    const QString &_cc,
0110                                    const QString &_bcc,
0111                                    const QString &subject,
0112                                    const QString &body,
0113                                    const QString & /*messageFile TODO*/,
0114                                    const QStringList &attachURLs,
0115                                    const QByteArray &startup_id)
0116 {
0117     if (!isMainThreadActive()) {
0118         return;
0119     }
0120     KService::Ptr emailClient = KApplicationTrader::preferredService(QStringLiteral("x-scheme-handler/mailto"));
0121     auto command = emailClient->exec();
0122 
0123     QString to;
0124     QString cc;
0125     QString bcc;
0126     if (emailClient->storageId() == QStringLiteral("org.kde.kmail2.desktop")) {
0127         command = QStringLiteral("kmail --composer -s %s -c %c -b %b --body %B --attach %A -- %t");
0128         if (!_to.isEmpty()) {
0129             QUrl url;
0130             url.setScheme(QStringLiteral("mailto"));
0131             url.setPath(_to);
0132             to = QString::fromLatin1(url.toEncoded());
0133         }
0134         if (!_cc.isEmpty()) {
0135             QUrl url;
0136             url.setScheme(QStringLiteral("mailto"));
0137             url.setPath(_cc);
0138             cc = QString::fromLatin1(url.toEncoded());
0139         }
0140         if (!_bcc.isEmpty()) {
0141             QUrl url;
0142             url.setScheme(QStringLiteral("mailto"));
0143             url.setPath(_bcc);
0144             bcc = QString::fromLatin1(url.toEncoded());
0145         }
0146     } else {
0147         to = _to;
0148         cc = _cc;
0149         bcc = _bcc;
0150         if (!command.contains(QLatin1Char('%'))) {
0151             command += QLatin1String(" %u");
0152         }
0153     }
0154 
0155     if (emailClient->terminal()) {
0156         KConfigGroup confGroup(KSharedConfig::openConfig(), "General");
0157         QString preferredTerminal = confGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole"));
0158         command = preferredTerminal + QLatin1String(" -e ") + command;
0159     }
0160 
0161     QStringList cmdTokens = KShell::splitArgs(command);
0162     QString cmd = cmdTokens.takeFirst();
0163 
0164     QUrl url;
0165     QUrlQuery query;
0166     if (!to.isEmpty()) {
0167         QStringList tos = splitEmailAddressList(to);
0168         url.setPath(tos.first());
0169         tos.erase(tos.begin());
0170         for (QStringList::ConstIterator it = tos.constBegin(); it != tos.constEnd(); ++it) {
0171             query.addQueryItem(QStringLiteral("to"), *it);
0172         }
0173     }
0174     const QStringList ccs = splitEmailAddressList(cc);
0175     for (QStringList::ConstIterator it = ccs.constBegin(); it != ccs.constEnd(); ++it) {
0176         query.addQueryItem(QStringLiteral("cc"), *it);
0177     }
0178     const QStringList bccs = splitEmailAddressList(bcc);
0179     for (QStringList::ConstIterator it = bccs.constBegin(); it != bccs.constEnd(); ++it) {
0180         query.addQueryItem(QStringLiteral("bcc"), *it);
0181     }
0182     for (QStringList::ConstIterator it = attachURLs.constBegin(); it != attachURLs.constEnd(); ++it) {
0183         query.addQueryItem(QStringLiteral("attach"), *it);
0184     }
0185     if (!subject.isEmpty()) {
0186         query.addQueryItem(QStringLiteral("subject"), subject);
0187     }
0188     if (!body.isEmpty()) {
0189         query.addQueryItem(QStringLiteral("body"), body);
0190     }
0191 
0192     url.setQuery(query);
0193 
0194     if (!(to.isEmpty() && (!url.hasQuery()))) {
0195         url.setScheme(QStringLiteral("mailto"));
0196     }
0197 
0198     QHash<QChar, QString> keyMap;
0199     keyMap.insert(QLatin1Char('t'), to);
0200     keyMap.insert(QLatin1Char('s'), subject);
0201     keyMap.insert(QLatin1Char('c'), cc);
0202     keyMap.insert(QLatin1Char('b'), bcc);
0203     keyMap.insert(QLatin1Char('B'), body);
0204     keyMap.insert(QLatin1Char('u'), url.toString());
0205 
0206     QString attachlist = attachURLs.join(QLatin1Char(','));
0207     attachlist.prepend(QLatin1Char('\''));
0208     attachlist.append(QLatin1Char('\''));
0209     keyMap.insert(QLatin1Char('A'), attachlist);
0210     for (int i = 0; i < cmdTokens.count(); ++i) {
0211         if (cmdTokens.at(i) == QLatin1String("%A")) {
0212             if (attachURLs.isEmpty()) {
0213                 cmdTokens.removeAt(i);
0214             } else {
0215                 const QString previousStr = cmdTokens.at(i - 1);
0216                 cmdTokens.removeAt(i);
0217                 const int currentPos = i;
0218                 for (const QString &attachUrl : attachURLs) {
0219                     cmdTokens.insert(currentPos, previousStr);
0220                     cmdTokens.insert(currentPos, attachUrl);
0221                     i += 2;
0222                 }
0223             }
0224         } else {
0225             const QString str = KMacroExpander::expandMacros(cmdTokens.at(i), keyMap);
0226             cmdTokens[i] = str;
0227         }
0228     }
0229     QString error;
0230     // TODO this should check if cmd has a .desktop file, and use data from it, together
0231     // with sending more ASN data
0232     if (kdeinitExec(cmd, cmdTokens, &error, nullptr, startup_id)) {
0233         KMessage::message(KMessage::Error, //
0234                           i18n("Could not launch the mail client:\n\n%1", error),
0235                           i18n("Could not launch Mail Client"));
0236     }
0237 }
0238 
0239 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 0)
0240 void KToolInvocation::invokeBrowser(const QString &url, const QByteArray &startup_id)
0241 {
0242     if (!isMainThreadActive()) {
0243         return;
0244     }
0245 
0246     QStringList args;
0247     args << url;
0248     QString error;
0249 
0250     // This method should launch a webbrowser, preferably without doing a MIME type
0251     // check first, like KRun (i.e. kde-open) would do.
0252 
0253     // In a KDE session, honour BrowserApplication if set, otherwise use preferred app for text/html if any,
0254     // otherwise xdg-open, otherwise kde-open (which does a MIME type check first though).
0255 
0256     // Outside KDE, call xdg-open if present, otherwise fallback to the above logic.
0257 
0258     QString exe; // the binary we are going to launch.
0259 
0260     const QString xdg_open = QStandardPaths::findExecutable(QStringLiteral("xdg-open"));
0261     if (qEnvironmentVariableIsEmpty("KDE_FULL_SESSION")) {
0262         exe = xdg_open;
0263     }
0264 
0265     if (exe.isEmpty()) {
0266         // We're in a KDE session (or there's no xdg-open installed)
0267         KConfigGroup config(KSharedConfig::openConfig(), "General");
0268         const QString browserApp = config.readPathEntry("BrowserApplication", QString());
0269         if (!browserApp.isEmpty()) {
0270             exe = browserApp;
0271             if (exe.startsWith(QLatin1Char('!'))) {
0272                 exe.remove(0, 1); // Literal command
0273                 QStringList cmdTokens = KShell::splitArgs(exe);
0274                 exe = cmdTokens.takeFirst();
0275                 args = cmdTokens + args;
0276             } else {
0277                 // desktop file ID
0278                 KService::Ptr service = KService::serviceByStorageId(exe);
0279                 if (service) {
0280                     // qDebug() << "Starting service" << service->entryPath();
0281                     if (startServiceByDesktopPath(service->entryPath(), args, &error, nullptr, nullptr, startup_id)) {
0282                         KMessage::message(KMessage::Error,
0283                                           // TODO: i18n("Could not launch %1:\n\n%2", exe, error),
0284                                           i18n("Could not launch the browser:\n\n%1", error),
0285                                           i18n("Could not launch Browser"));
0286                     }
0287                     return;
0288                 }
0289             }
0290         } else {
0291             const KService::Ptr htmlApp = KApplicationTrader::preferredService(QStringLiteral("text/html"));
0292             if (htmlApp) {
0293                 // WORKAROUND: For bugs 264562 and 265474:
0294                 // In order to correctly handle non-HTML urls we change the service
0295                 // desktop file name to "kfmclient.desktop" whenever the above query
0296                 // returns "kfmclient_html.desktop".Otherwise, the hard coded mime-type
0297                 // "text/html" mime-type parameter in the kfmclient_html will cause all
0298                 // URLs to be treated as if they are HTML page.
0299                 QString entryPath = htmlApp->entryPath();
0300                 if (entryPath.endsWith(QLatin1String("kfmclient_html.desktop"))) {
0301                     entryPath.remove(entryPath.length() - 13, 5);
0302                 }
0303                 QString error;
0304                 int pid = 0;
0305                 int err = startServiceByDesktopPath(entryPath, url, &error, nullptr, &pid, startup_id);
0306                 if (err != 0) {
0307                     KMessage::message(KMessage::Error,
0308                                       // TODO: i18n("Could not launch %1:\n\n%2", htmlApp->exec(), error),
0309                                       i18n("Could not launch the browser:\n\n%1", error),
0310                                       i18n("Could not launch Browser"));
0311                 } else { // success
0312                     return;
0313                 }
0314             } else {
0315                 exe = xdg_open;
0316             }
0317         }
0318     }
0319 
0320     if (exe.isEmpty()) {
0321         exe = QStringLiteral("kde-open"); // it's from kdebase-runtime, it has to be there.
0322     }
0323 
0324     // qDebug() << "Using" << exe << "to open" << url;
0325     if (kdeinitExec(exe, args, &error, nullptr, startup_id)) {
0326         KMessage::message(KMessage::Error,
0327                           // TODO: i18n("Could not launch %1:\n\n%2", exe, error),
0328                           i18n("Could not launch the browser:\n\n%1", error),
0329                           i18n("Could not launch Browser"));
0330     }
0331 }
0332 #endif
0333 
0334 void KToolInvocation::invokeTerminal(const QString &command, const QStringList &envs, const QString &workdir, const QByteArray &startup_id)
0335 {
0336     if (!isMainThreadActive()) {
0337         return;
0338     }
0339 
0340     const KService::Ptr terminal = terminalApplication(command, workdir);
0341     if (!terminal) {
0342         KMessage::message(KMessage::Error, i18n("Unable to determine the default terminal"));
0343         return;
0344     }
0345 
0346     QStringList cmdTokens = KShell::splitArgs(terminal->exec());
0347     const QString cmd = cmdTokens.takeFirst();
0348 
0349     QString error;
0350     // clang-format off
0351     if (self()->startServiceInternal("kdeinit_exec_with_workdir",
0352                                      cmd, cmdTokens, &error, nullptr, nullptr, startup_id, false, workdir, envs)) {
0353         KMessage::message(KMessage::Error,
0354                           i18n("Could not launch the terminal client:\n\n%1", error),
0355                           i18n("Could not launch Terminal Client"));
0356     }
0357     // clang-format on
0358 }
0359 
0360 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 79)
0361 void KToolInvocation::invokeTerminal(const QString &command, const QString &workdir, const QByteArray &startup_id)
0362 {
0363     invokeTerminal(command, {}, workdir, startup_id);
0364 }
0365 #endif
0366 
0367 KServicePtr KToolInvocation::terminalApplication(const QString &command, const QString &workingDir)
0368 {
0369     const KConfigGroup confGroup(KSharedConfig::openConfig(), "General");
0370     const QString terminalService = confGroup.readEntry("TerminalService");
0371     const QString terminalExec = confGroup.readEntry("TerminalApplication");
0372     KServicePtr ptr;
0373     if (!terminalService.isEmpty()) {
0374         ptr = KService::serviceByStorageId(terminalService);
0375     } else if (!terminalExec.isEmpty()) {
0376         ptr = new KService(QStringLiteral("terminal"), terminalExec, QStringLiteral("utilities-terminal"));
0377     }
0378     if (!ptr) {
0379         ptr = KService::serviceByStorageId(QStringLiteral("org.kde.konsole"));
0380     }
0381     if (!ptr) {
0382         return KServicePtr();
0383     }
0384     QString exec = ptr->exec();
0385     if (!command.isEmpty()) {
0386         if (exec == QLatin1String("konsole")) {
0387             exec += QLatin1String(" --noclose");
0388         } else if (exec == QLatin1String("xterm")) {
0389             exec += QLatin1String(" -hold");
0390         }
0391         exec += QLatin1String(" -e ") + command;
0392     }
0393     if (ptr->exec() == QLatin1String("konsole") && !workingDir.isEmpty()) {
0394         exec += QStringLiteral(" --workdir %1").arg(KShell::quoteArg(workingDir));
0395     }
0396     ptr->setExec(exec);
0397     if (!workingDir.isEmpty()) {
0398         ptr->setWorkingDirectory(workingDir);
0399     }
0400     return ptr;
0401 }