1 | /* |
2 | This file is part of the KDE libraries |
3 | SPDX-FileCopyrightText: 1999 Pietro Iglio <iglio@kde.org> |
4 | SPDX-FileCopyrightText: 1999 Preston Brown <pbrown@kde.org> |
5 | |
6 | SPDX-License-Identifier: LGPL-2.0-or-later |
7 | */ |
8 | |
9 | #include "kdesktopfile.h" |
10 | |
11 | #include "kauthorized.h" |
12 | #include "kconfig_core_log_settings.h" |
13 | #include "kconfig_p.h" |
14 | #include "kconfiggroup.h" |
15 | #include "kconfigini_p.h" |
16 | #include "kdesktopfileaction.h" |
17 | |
18 | #include <QDir> |
19 | #include <QFileInfo> |
20 | #include <QStandardPaths> |
21 | #include <QUrl> |
22 | |
23 | #ifndef Q_OS_WIN |
24 | #include <unistd.h> |
25 | #endif |
26 | |
27 | #include <algorithm> |
28 | |
29 | class KDesktopFilePrivate : public KConfigPrivate |
30 | { |
31 | public: |
32 | KDesktopFilePrivate(QStandardPaths::StandardLocation resourceType, const QString &fileName) |
33 | : KConfigPrivate(KConfig::NoGlobals, resourceType) |
34 | { |
35 | changeFileName(fileName); |
36 | |
37 | // make sure the [Desktop Entry] group is always the first one, as required by the spec |
38 | mBackend.setPrimaryGroup(QStringLiteral("Desktop Entry" )); |
39 | } |
40 | KConfigGroup desktopGroup; |
41 | }; |
42 | |
43 | KDesktopFile::KDesktopFile(QStandardPaths::StandardLocation resourceType, const QString &fileName) |
44 | : KConfig(*new KDesktopFilePrivate(resourceType, fileName)) |
45 | { |
46 | Q_D(KDesktopFile); |
47 | reparseConfiguration(); |
48 | d->desktopGroup = KConfigGroup(this, QStringLiteral("Desktop Entry" )); |
49 | } |
50 | |
51 | KDesktopFile::KDesktopFile(const QString &fileName) |
52 | : KDesktopFile(QStandardPaths::ApplicationsLocation, fileName) |
53 | { |
54 | } |
55 | |
56 | KDesktopFile::~KDesktopFile() = default; |
57 | |
58 | KConfigGroup KDesktopFile::desktopGroup() const |
59 | { |
60 | Q_D(const KDesktopFile); |
61 | return d->desktopGroup; |
62 | } |
63 | |
64 | QString KDesktopFile::locateLocal(const QString &path) |
65 | { |
66 | static const QLatin1Char slash('/'); |
67 | |
68 | // Relative to config? (e.g. for autostart) |
69 | const QStringList genericConfig = QStandardPaths::standardLocations(type: QStandardPaths::GenericConfigLocation); |
70 | // Iterate from the last item since some items may be subfolders of others. |
71 | auto it = std::find_if(first: genericConfig.crbegin(), last: genericConfig.crend(), pred: [&path](const QString &dir) { |
72 | return path.startsWith(s: dir + slash); |
73 | }); |
74 | if (it != genericConfig.crend()) { |
75 | return QStandardPaths::writableLocation(type: QStandardPaths::GenericConfigLocation) + slash + QStringView(path).mid(pos: it->size() + 1); |
76 | } |
77 | |
78 | QString relativePath; |
79 | // Relative to xdg data dir? (much more common) |
80 | const QStringList lstGenericDataLocation = QStandardPaths::standardLocations(type: QStandardPaths::GenericDataLocation); |
81 | for (const QString &dir : lstGenericDataLocation) { |
82 | if (path.startsWith(s: dir + slash)) { |
83 | relativePath = path.mid(position: dir.length() + 1); |
84 | } |
85 | } |
86 | if (relativePath.isEmpty()) { |
87 | // What now? The desktop file doesn't come from XDG_DATA_DIRS. Use filename only and hope for the best. |
88 | relativePath = path.mid(position: path.lastIndexOf(c: slash) + 1); |
89 | } |
90 | return QStandardPaths::writableLocation(type: QStandardPaths::GenericDataLocation) + slash + relativePath; |
91 | } |
92 | |
93 | bool KDesktopFile::isDesktopFile(const QString &path) |
94 | { |
95 | return path.endsWith(s: QLatin1String(".desktop" )); |
96 | } |
97 | |
98 | bool KDesktopFile::isAuthorizedDesktopFile(const QString &path) |
99 | { |
100 | if (path.isEmpty()) { |
101 | return false; // Empty paths are not ok. |
102 | } |
103 | |
104 | if (QDir::isRelativePath(path)) { |
105 | return true; // Relative paths are ok. |
106 | } |
107 | |
108 | const QString realPath = QFileInfo(path).canonicalFilePath(); |
109 | if (realPath.isEmpty()) { |
110 | return false; // File doesn't exist. |
111 | } |
112 | |
113 | #ifndef Q_OS_WIN |
114 | static constexpr Qt::CaseSensitivity sensitivity = Qt::CaseSensitive; |
115 | #else |
116 | static constexpr Qt::CaseSensitivity sensitivity = Qt::CaseInsensitive; |
117 | #endif |
118 | |
119 | // Check if the .desktop file is installed as part of KDE or XDG. |
120 | const QStringList appsDirs = QStandardPaths::standardLocations(type: QStandardPaths::ApplicationsLocation); |
121 | auto it = std::find_if(first: appsDirs.cbegin(), last: appsDirs.cend(), pred: [&realPath, &path](const QString &prefix) { |
122 | QFileInfo info(prefix); |
123 | return info.exists() && info.isDir() && (realPath.startsWith(s: info.canonicalFilePath(), cs: sensitivity) || path.startsWith(s: info.canonicalFilePath())); |
124 | }); |
125 | if (it != appsDirs.cend()) { |
126 | return true; |
127 | } |
128 | |
129 | const QString autostartDir = QStringLiteral("autostart/" ); |
130 | const QStringList lstConfigPath = QStandardPaths::standardLocations(type: QStandardPaths::GenericConfigLocation); |
131 | auto configIt = std::find_if(first: lstConfigPath.cbegin(), last: lstConfigPath.cend(), pred: [&realPath, &autostartDir](const QString &xdgDataPrefix) { |
132 | QFileInfo info(xdgDataPrefix); |
133 | if (info.exists() && info.isDir()) { |
134 | const QString prefix = info.canonicalFilePath(); |
135 | return realPath.startsWith(s: prefix + QLatin1Char('/') + autostartDir, cs: sensitivity); |
136 | } |
137 | return false; |
138 | }); |
139 | if (configIt != lstConfigPath.cend()) { |
140 | return true; |
141 | } |
142 | |
143 | // Forbid desktop files outside of standard locations if kiosk is set so |
144 | if (!KAuthorized::authorize(QStringLiteral("run_desktop_files" ))) { |
145 | qCWarning(KCONFIG_CORE_LOG) << "Access to" << path << "denied because of 'run_desktop_files' restriction." ; |
146 | return false; |
147 | } |
148 | |
149 | // Not otherwise permitted, so only allow if the file is executable, or if |
150 | // owned by root (uid == 0) |
151 | QFileInfo entryInfo(path); |
152 | if (entryInfo.isExecutable() || entryInfo.ownerId() == 0) { |
153 | return true; |
154 | } |
155 | |
156 | qCInfo(KCONFIG_CORE_LOG) << "Access to" << path << "denied, not owned by root and executable flag not set." ; |
157 | return false; |
158 | } |
159 | |
160 | QString KDesktopFile::readType() const |
161 | { |
162 | Q_D(const KDesktopFile); |
163 | return d->desktopGroup.readEntry(key: "Type" , aDefault: QString()); |
164 | } |
165 | |
166 | QString KDesktopFile::readIcon() const |
167 | { |
168 | Q_D(const KDesktopFile); |
169 | return d->desktopGroup.readEntry(key: "Icon" , aDefault: QString()); |
170 | } |
171 | |
172 | QString KDesktopFile::readName() const |
173 | { |
174 | Q_D(const KDesktopFile); |
175 | return d->desktopGroup.readEntry(key: "Name" , aDefault: QString()); |
176 | } |
177 | |
178 | QString KDesktopFile::() const |
179 | { |
180 | Q_D(const KDesktopFile); |
181 | return d->desktopGroup.readEntry(key: "Comment" , aDefault: QString()); |
182 | } |
183 | |
184 | QString KDesktopFile::readGenericName() const |
185 | { |
186 | Q_D(const KDesktopFile); |
187 | return d->desktopGroup.readEntry(key: "GenericName" , aDefault: QString()); |
188 | } |
189 | |
190 | QString KDesktopFile::readPath() const |
191 | { |
192 | Q_D(const KDesktopFile); |
193 | // NOT readPathEntry, it is not XDG-compliant: it performs |
194 | // various expansions, like $HOME. Note that the expansion |
195 | // behaviour still happens if the "e" flag is set, maintaining |
196 | // backwards compatibility. |
197 | return d->desktopGroup.readEntry(key: "Path" , aDefault: QString()); |
198 | } |
199 | |
200 | QString KDesktopFile::readUrl() const |
201 | { |
202 | Q_D(const KDesktopFile); |
203 | if (hasDeviceType()) { |
204 | return d->desktopGroup.readEntry(key: "MountPoint" , aDefault: QString()); |
205 | } else { |
206 | // NOT readPathEntry (see readPath()) |
207 | QString url = d->desktopGroup.readEntry(key: "URL" , aDefault: QString()); |
208 | if (!url.isEmpty() && !QDir::isRelativePath(path: url)) { |
209 | // Handle absolute paths as such (i.e. we need to escape them) |
210 | return QUrl::fromLocalFile(localfile: url).toString(); |
211 | } |
212 | return url; |
213 | } |
214 | } |
215 | |
216 | QStringList KDesktopFile::readActions() const |
217 | { |
218 | Q_D(const KDesktopFile); |
219 | return d->desktopGroup.readXdgListEntry(key: "Actions" ); |
220 | } |
221 | |
222 | QStringList KDesktopFile::readMimeTypes() const |
223 | { |
224 | Q_D(const KDesktopFile); |
225 | return d->desktopGroup.readXdgListEntry(key: "MimeType" ); |
226 | } |
227 | |
228 | KConfigGroup KDesktopFile::actionGroup(const QString &group) |
229 | { |
230 | return KConfigGroup(this, QLatin1String("Desktop Action " ) + group); |
231 | } |
232 | |
233 | KConfigGroup KDesktopFile::actionGroup(const QString &group) const |
234 | { |
235 | return const_cast<KDesktopFile *>(this)->actionGroup(group); |
236 | } |
237 | |
238 | bool KDesktopFile::hasActionGroup(const QString &group) const |
239 | { |
240 | return hasGroup(group: QString(QLatin1String("Desktop Action " ) + group)); |
241 | } |
242 | |
243 | bool KDesktopFile::hasLinkType() const |
244 | { |
245 | return readType() == QLatin1String("Link" ); |
246 | } |
247 | |
248 | bool KDesktopFile::hasApplicationType() const |
249 | { |
250 | return readType() == QLatin1String("Application" ); |
251 | } |
252 | |
253 | bool KDesktopFile::hasDeviceType() const |
254 | { |
255 | return readType() == QLatin1String("FSDevice" ); |
256 | } |
257 | |
258 | bool KDesktopFile::tryExec() const |
259 | { |
260 | Q_D(const KDesktopFile); |
261 | // Test for TryExec and "X-KDE-AuthorizeAction" |
262 | // NOT readPathEntry (see readPath()) |
263 | const QString te = d->desktopGroup.readEntry(key: "TryExec" , aDefault: QString()); |
264 | if (!te.isEmpty() && QStandardPaths::findExecutable(executableName: te).isEmpty()) { |
265 | return false; |
266 | } |
267 | const QStringList list = d->desktopGroup.readEntry(key: "X-KDE-AuthorizeAction" , aDefault: QStringList()); |
268 | const auto isNotAuthorized = std::any_of(first: list.cbegin(), last: list.cend(), pred: [](const QString &action) { |
269 | return !KAuthorized::authorize(action: action.trimmed()); |
270 | }); |
271 | if (isNotAuthorized) { |
272 | return false; |
273 | } |
274 | |
275 | // See also KService::username() |
276 | if (d->desktopGroup.readEntry(key: "X-KDE-SubstituteUID" , defaultValue: false)) { |
277 | QString user = d->desktopGroup.readEntry(key: "X-KDE-Username" , aDefault: QString()); |
278 | if (user.isEmpty()) { |
279 | user = qEnvironmentVariable(varName: "ADMIN_ACCOUNT" ), QStringLiteral("root" ); |
280 | } |
281 | if (!KAuthorized::authorize(action: QLatin1String("user/" ) + user)) { |
282 | return false; |
283 | } |
284 | } |
285 | |
286 | return true; |
287 | } |
288 | |
289 | QString KDesktopFile::readDocPath() const |
290 | { |
291 | Q_D(const KDesktopFile); |
292 | return d->desktopGroup.readPathEntry(key: "X-DocPath" , aDefault: QString()); |
293 | } |
294 | |
295 | KDesktopFile *KDesktopFile::copyTo(const QString &file) const |
296 | { |
297 | KDesktopFile *config = new KDesktopFile(QString()); |
298 | this->KConfig::copyTo(file, config); |
299 | return config; |
300 | } |
301 | |
302 | QString KDesktopFile::fileName() const |
303 | { |
304 | return name(); |
305 | } |
306 | |
307 | bool KDesktopFile::noDisplay() const |
308 | { |
309 | Q_D(const KDesktopFile); |
310 | return d->desktopGroup.readEntry(key: "NoDisplay" , defaultValue: false); |
311 | } |
312 | |
313 | QList<KDesktopFileAction> KDesktopFile::actions() const |
314 | { |
315 | QList<KDesktopFileAction> desktopFileActions; |
316 | const QStringList actionKeys = readActions(); |
317 | for (const QString &actionKey : actionKeys) { |
318 | const KConfigGroup grp = actionGroup(group: actionKey); |
319 | desktopFileActions << KDesktopFileAction(actionKey, grp.readEntry(key: "Name" ), grp.readEntry(key: "Icon" ), grp.readEntry(key: "Exec" ), fileName()); |
320 | } |
321 | return desktopFileActions; |
322 | } |
323 | |