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