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 }