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 | |
24 | namespace |
25 | { |
26 | |
27 | QString 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 | |
39 | void 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 | |
63 | namespace KIO |
64 | { |
65 | |
66 | OpenWith::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 ; |
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 | |