KIO

openwith.cpp
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;
32 exec.remove(QLatin1String("-caption %c"));
33 exec.remove(QLatin1String("-caption \"%c\""));
34 exec.remove(QLatin1String("%i"));
35 exec.remove(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"), KConfig::NoGlobals, 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(qMimeType, QStringList(serviceId));
46
47 KConfigGroup addedApps(profile, QStringLiteral("Added Associations"));
48 QStringList apps = addedApps.readXdgListEntry(qMimeType);
49 apps.removeAll(serviceId);
50 apps.prepend(serviceId); // make it the preferred app
51 addedApps.writeXdgListEntry(qMimeType, 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"), KConfig::NoGlobals);
57 fileTypesConfig->group(QStringLiteral("EmbedSettings")).writeEntry(QStringLiteral("embed-") + qMimeType, 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("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(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;
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(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(typedExec);
135 qCDebug(KIO_CORE) << "binaryName=" << binaryName;
136 // Ensure that the typed binary name actually exists (#81190)
137 if (QStandardPaths::findExecutable(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(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(service->storageId(), 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(QLatin1String("%u"), Qt::CaseInsensitive) && !typedExec.contains(QLatin1String("%f"), Qt::CaseInsensitive)) {
168 int index = serviceExec.indexOf(QLatin1String("%u"), 0, Qt::CaseInsensitive);
169 if (index == -1) {
170 index = serviceExec.indexOf(QLatin1String("%f"), 0, Qt::CaseInsensitive);
171 }
172 if (index > -1) {
173 fullExec += QLatin1Char(' ') + QStringView(serviceExec).mid(index, 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(false /* ignored argument */, serviceName, &menuId);
196 // qDebug() << "Creating new service" << serviceName << "(" << newPath << ")" << "menuId=" << menuId;
197
198 KDesktopFile desktopFile(newPath);
199 KConfigGroup cg = desktopFile.desktopGroup();
200 cg.writeEntry("Type", "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".
208 auto view = QStringView{fullExec}.trimmed();
209 int index = view.indexOf(QLatin1Char(' '));
210 if (index > 0) {
211 name.append(view.mid(index));
212 }
213 cg.writeEntry("Name", simplifiedExecLineFromService(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(QLatin1String("x-scheme-handler/"))) {
217 if (!typedExec.contains(QLatin1String("%u"), Qt::CaseInsensitive) && !typedExec.contains(QLatin1String("%f"), Qt::CaseInsensitive)) {
218 fullExec += QStringLiteral(" %u");
219 }
220 }
221
222 cg.writeEntry("Exec", fullExec);
223 cg.writeEntry("NoDisplay", true); // don't make it appear in the K menu
224 if (openInTerminal) {
225 cg.writeEntry("Terminal", true);
226 // only add --noclose when we are sure it is konsole we're using
227 if (preferredTerminal == QLatin1String("konsole") && lingerTerminal) {
228 cg.writeEntry("TerminalOptions", "--noclose");
229 }
230 }
231 if (!mimeType.isEmpty()) {
232 cg.writeXdgListEntry("MimeType", QStringList() << mimeType);
233 }
234 cg.sync();
235
236 if (!mimeType.isEmpty()) {
237 addToMimeAppsList(menuId, 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
void writeEntry(const char *key, const char *value, WriteConfigFlags pFlags=Normal)
QString readPathEntry(const char *key, const QString &aDefault) const
void writeXdgListEntry(const char *key, const QStringList &value, WriteConfigFlags pFlags=Normal)
bool sync() override
KConfigGroup desktopGroup() const
static QString executablePath(const QString &execLine)
Given a full command line (e.g. the Exec= line from a .desktop file), extract the name of the executa...
static QString executableName(const QString &execLine)
Given a full command line (e.g. the Exec= line from a .desktop file), extract the name of the executa...
static AcceptResult accept(KService::Ptr &service, const QString &typedExec, bool remember, const QString &mimeType, bool openInTerminal, bool lingerTerminal, bool saveNewApps)
Accept an openwith request with the provided arguments as context.
Definition openwith.cpp:66
void setExec(const QString &exec)
QString storageId() const
void setTerminalOptions(const QString &options)
static QString newServicePath(bool showInMenu, const QString &suggestedName, QString *menuId=nullptr, const QStringList *reservedMenuIds=nullptr)
bool terminal() const
bool noDisplay() const
static Ptr serviceByDesktopName(const QString &_name)
QString exec() const
void setTerminal(bool b)
bool isApplication() const
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
QString name() const
QString entryPath() const
QString xi18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
A namespace for KIO globals.
bool exists() const const
QString fileName() const const
void prepend(parameter_type value)
qsizetype removeAll(const AT &t)
QString findExecutable(const QString &executableName, const QStringList &paths)
QString & append(QChar ch)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString number(double n, char format, int precision)
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QString simplified() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString trimmed() const const
QStringView mid(qsizetype start, qsizetype length) const const
CaseInsensitive
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 3 2024 11:49:40 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.