1/*
2 SPDX-FileCopyrightText: 1997 Torben Weis <weis@stud.uni-frankfurt.de>
3 SPDX-FileCopyrightText: 1999 Dirk Mueller <mueller@kde.org>
4 Portions SPDX-FileCopyrightText: 1999 Preston Brown <pbrown@kde.org>
5 SPDX-FileCopyrightText: 2007 Pino Toscano <pino@kde.org>
6 SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org>
7
8 SPDX-License-Identifier: LGPL-2.0-or-later
9*/
10
11#include "openwith.h"
12
13#include <QFileInfo>
14#include <QStandardPaths>
15
16#include <KConfigGroup>
17#include <KDesktopFile>
18#include <KLocalizedString>
19#include <KSharedConfig>
20
21#include "desktopexecparser.h"
22#include "kiocoredebug.h"
23
24namespace
25{
26
27QString simplifiedExecLineFromService(const QString &fullExec)
28{
29 QString exec = fullExec;
30 exec.remove(s: QLatin1String("%u"), cs: Qt::CaseInsensitive);
31 exec.remove(s: QLatin1String("%f"), cs: Qt::CaseInsensitive);
32 exec.remove(s: QLatin1String("-caption %c"));
33 exec.remove(s: QLatin1String("-caption \"%c\""));
34 exec.remove(s: QLatin1String("%i"));
35 exec.remove(s: QLatin1String("%m"));
36 return exec.simplified();
37}
38
39void addToMimeAppsList(const QString &serviceId /*menu id or storage id*/, const QString &qMimeType)
40{
41 KSharedConfig::Ptr profile = KSharedConfig::openConfig(QStringLiteral("mimeapps.list"), mode: KConfig::NoGlobals, type: QStandardPaths::GenericConfigLocation);
42
43 // Save the default application according to mime-apps-spec 1.0
44 KConfigGroup defaultApp(profile, QStringLiteral("Default Applications"));
45 defaultApp.writeXdgListEntry(pKey: qMimeType, value: QStringList(serviceId));
46
47 KConfigGroup addedApps(profile, QStringLiteral("Added Associations"));
48 QStringList apps = addedApps.readXdgListEntry(pKey: qMimeType);
49 apps.removeAll(t: serviceId);
50 apps.prepend(t: serviceId); // make it the preferred app
51 addedApps.writeXdgListEntry(pKey: qMimeType, value: apps);
52
53 profile->sync();
54
55 // Also make sure the "auto embed" setting for this MIME type is off
56 KSharedConfig::Ptr fileTypesConfig = KSharedConfig::openConfig(QStringLiteral("filetypesrc"), mode: KConfig::NoGlobals);
57 fileTypesConfig->group(QStringLiteral("EmbedSettings")).writeEntry(QStringLiteral("embed-") + qMimeType, value: false);
58 fileTypesConfig->sync();
59}
60
61} // namespace
62
63namespace KIO
64{
65
66OpenWith::AcceptResult OpenWith::accept(KService::Ptr &service,
67 const QString &typedExec,
68 bool remember,
69 const QString &mimeType,
70 bool openInTerminal,
71 bool lingerTerminal,
72 bool saveNewApps)
73{
74 QString fullExec(typedExec);
75
76 KConfigGroup confGroup(KSharedConfig::openConfig(), QStringLiteral("General"));
77 const QString preferredTerminal = confGroup.readPathEntry(key: "TerminalApplication", QStringLiteral("konsole"));
78
79 QString serviceName;
80 QString initialServiceName;
81 QString configPath;
82 QString serviceExec;
83 bool rebuildSycoca = false;
84 if (!service) {
85 // No service selected - check the command line
86
87 // Find out the name of the service from the command line, removing args and paths
88 serviceName = KIO::DesktopExecParser::executableName(execLine: typedExec);
89 if (serviceName.isEmpty()) {
90 return {.accept = false, .error = i18n("Could not extract executable name from '%1', please type a valid program name.", serviceName)};
91 }
92 initialServiceName = serviceName;
93 // Also remember the executableName with a path, if any, for the
94 // check that the executable exists.
95 qCDebug(KIO_CORE) << "initialServiceName=" << initialServiceName;
96 int i = 1; // We have app, app-2, app-3... Looks better for the user.
97 bool ok = false;
98 // Check if there's already a service by that name, with the same Exec line
99 do {
100 qCDebug(KIO_CORE) << "looking for service" << serviceName;
101 KService::Ptr serv = KService::serviceByDesktopName(name: serviceName);
102 ok = !serv; // ok if no such service yet
103 // also ok if we find the exact same service (well, "kwrite" == "kwrite %U")
104 if (serv && !serv->noDisplay() /* #297720 */) {
105 if (serv->isApplication()) {
106 qCDebug(KIO_CORE) << "typedExec=" << typedExec << "serv->exec=" << serv->exec()
107 << "simplifiedExecLineFromService=" << simplifiedExecLineFromService(fullExec);
108 serviceExec = simplifiedExecLineFromService(fullExec: serv->exec());
109 if (typedExec == serviceExec) {
110 ok = true;
111 service = serv;
112 qCDebug(KIO_CORE) << "OK, found identical service: " << serv->entryPath();
113 } else {
114 qCDebug(KIO_CORE) << "Exec line differs, service says:" << serviceExec;
115 configPath = serv->entryPath();
116 serviceExec = serv->exec();
117 }
118 } else {
119 qCDebug(KIO_CORE) << "Found, but not an application:" << serv->entryPath();
120 }
121 }
122 if (!ok) { // service was found, but it was different -> keep looking
123 ++i;
124 serviceName = initialServiceName + QLatin1Char('-') + QString::number(i);
125 }
126 } while (!ok);
127 }
128 if (service) {
129 // Existing service selected
130 serviceName = service->name();
131 initialServiceName = serviceName;
132 fullExec = service->exec();
133 } else {
134 const QString binaryName = KIO::DesktopExecParser::executablePath(execLine: typedExec);
135 qCDebug(KIO_CORE) << "binaryName=" << binaryName;
136 // Ensure that the typed binary name actually exists (#81190)
137 if (QStandardPaths::findExecutable(executableName: binaryName).isEmpty()) {
138 // QStandardPaths::findExecutable does not find non-executable files.
139 // Give a better error message for the case of a existing but non-executable file.
140 // https://bugs.kde.org/show_bug.cgi?id=437880
141 const QString msg = QFileInfo::exists(file: binaryName)
142 ? xi18nc("@info", "<filename>%1</filename> does not appear to be an executable program.", binaryName)
143 : xi18nc("@info", "<filename>%1</filename> was not found; please enter a valid path to an executable program.", binaryName);
144 return {.accept = false, .error = msg};
145 }
146 }
147
148 if (service && openInTerminal != service->terminal()) {
149 service = nullptr; // It's not exactly this service we're running
150 }
151
152 qCDebug(KIO_CORE) << "bRemember=" << remember << "service found=" << service;
153 if (service) {
154 if (remember) {
155 // Associate this app with qMimeType in mimeapps.list
156 Q_ASSERT(!mimeType.isEmpty()); // we don't show the remember checkbox otherwise
157 addToMimeAppsList(serviceId: service->storageId(), qMimeType: mimeType);
158 rebuildSycoca = true;
159 }
160 } else {
161 const bool createDesktopFile = remember || saveNewApps;
162 if (!createDesktopFile) {
163 // Create temp service
164 if (configPath.isEmpty()) {
165 service = new KService(initialServiceName, fullExec, QString());
166 } else {
167 if (!typedExec.contains(s: QLatin1String("%u"), cs: Qt::CaseInsensitive) && !typedExec.contains(s: QLatin1String("%f"), cs: Qt::CaseInsensitive)) {
168 int index = serviceExec.indexOf(s: QLatin1String("%u"), from: 0, cs: Qt::CaseInsensitive);
169 if (index == -1) {
170 index = serviceExec.indexOf(s: QLatin1String("%f"), from: 0, cs: Qt::CaseInsensitive);
171 }
172 if (index > -1) {
173 fullExec += QLatin1Char(' ') + QStringView(serviceExec).mid(pos: index, n: 2);
174 }
175 }
176 // qDebug() << "Creating service with Exec=" << fullExec;
177 service = new KService(configPath);
178 service->setExec(fullExec);
179 }
180 if (openInTerminal) {
181 service->setTerminal(true);
182 // only add --noclose when we are sure it is konsole we're using
183 if (preferredTerminal == QLatin1String("konsole") && lingerTerminal) {
184 service->setTerminalOptions(QStringLiteral("--noclose"));
185 }
186 }
187 } else {
188 // If we got here, we can't seem to find a service for what they wanted. Create one.
189
190 QString menuId;
191#ifdef Q_OS_WIN32
192 // on windows, do not use the complete path, but only the default name.
193 serviceName = QFileInfo(serviceName).fileName();
194#endif
195 QString newPath = KService::newServicePath(showInMenu: false /* ignored argument */, suggestedName: serviceName, menuId: &menuId);
196 // qDebug() << "Creating new service" << serviceName << "(" << newPath << ")" << "menuId=" << menuId;
197
198 KDesktopFile desktopFile(newPath);
199 KConfigGroup cg = desktopFile.desktopGroup();
200 cg.writeEntry(key: "Type", value: "Application");
201
202 // For the user visible name, use the executable name with any
203 // arguments appended, but with desktop-file specific expansion
204 // arguments removed. This is done to more clearly communicate the
205 // actual command used to the user and makes it easier to
206 // distinguish things like "qdbus".
207 QString name = KIO::DesktopExecParser::executableName(execLine: fullExec);
208 auto view = QStringView{fullExec}.trimmed();
209 int index = view.indexOf(c: QLatin1Char(' '));
210 if (index > 0) {
211 name.append(v: view.mid(pos: index));
212 }
213 cg.writeEntry(key: "Name", value: simplifiedExecLineFromService(fullExec: name));
214
215 // if we select a binary for a scheme handler, then it's safe to assume it can handle URLs
216 if (mimeType.startsWith(s: QLatin1String("x-scheme-handler/"))) {
217 if (!typedExec.contains(s: QLatin1String("%u"), cs: Qt::CaseInsensitive) && !typedExec.contains(s: QLatin1String("%f"), cs: Qt::CaseInsensitive)) {
218 fullExec += QStringLiteral(" %u");
219 }
220 }
221
222 cg.writeEntry(key: "Exec", value: fullExec);
223 cg.writeEntry(key: "NoDisplay", value: true); // don't make it appear in the K menu
224 if (openInTerminal) {
225 cg.writeEntry(key: "Terminal", value: true);
226 // only add --noclose when we are sure it is konsole we're using
227 if (preferredTerminal == QLatin1String("konsole") && lingerTerminal) {
228 cg.writeEntry(key: "TerminalOptions", value: "--noclose");
229 }
230 }
231 if (!mimeType.isEmpty()) {
232 cg.writeXdgListEntry(key: "MimeType", value: QStringList() << mimeType);
233 }
234 cg.sync();
235
236 if (!mimeType.isEmpty()) {
237 addToMimeAppsList(serviceId: menuId, qMimeType: mimeType);
238 rebuildSycoca = true;
239 }
240 service = new KService(newPath);
241 }
242 }
243
244 return {.accept = true, .error = {}, .rebuildSycoca = rebuildSycoca};
245}
246
247} // namespace KIO
248

source code of kio/src/core/openwith.cpp