KIO

desktopexecparser.cpp
1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2000 Torben Weis <weis@kde.org>
4 SPDX-FileCopyrightText: 2006-2013 David Faure <faure@kde.org>
5 SPDX-FileCopyrightText: 2009 Michael Pyne <michael.pyne@kdemail.net>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10#include "desktopexecparser.h"
11
12#ifdef WITH_QTDBUS
13#include "kiofuse_interface.h"
14#endif
15
16#include <KApplicationTrader>
17#include <KConfigGroup>
18#include <KDesktopFile>
19#include <KLocalizedString>
20#include <KMacroExpander>
21#include <KService>
22#include <KSharedConfig>
23#include <KShell>
24#include <kprotocolinfo.h> // KF6 TODO remove after moving hasSchemeHandler to OpenUrlJob
25
26#ifdef WITH_QTDBUS
27#include <QDBusConnection>
28#include <QDBusReply>
29#endif
30#include <QDir>
31#include <QFile>
32#include <QProcessEnvironment>
33#include <QStandardPaths>
34#include <QUrl>
35
36#include <config-kiocore.h> // KDE_INSTALL_FULL_LIBEXECDIR_KF
37
38#include "kiocoredebug.h"
39
40class KRunMX1 : public KMacroExpanderBase
41{
42public:
43 explicit KRunMX1(const KService &_service)
45 , hasUrls(false)
46 , hasSpec(false)
47 , service(_service)
48 {
49 }
50
51 bool hasUrls;
52 bool hasSpec;
53
54protected:
55 int expandEscapedMacro(const QString &str, int pos, QStringList &ret) override;
56
57private:
58 const KService &service;
59};
60
61int KRunMX1::expandEscapedMacro(const QString &str, int pos, QStringList &ret)
62{
63 uint option = str[pos + 1].unicode();
64 switch (option) {
65 case 'c':
66 ret << service.name().replace(QLatin1Char('%'), QLatin1String("%%"));
67 break;
68 case 'k':
69 ret << service.entryPath().replace(QLatin1Char('%'), QLatin1String("%%"));
70 break;
71 case 'i':
72 ret << QStringLiteral("--icon") << service.icon().replace(QLatin1Char('%'), QLatin1String("%%"));
73 break;
74 case 'm':
75 // ret << "-miniicon" << service.icon().replace( '%', "%%" );
76 qCWarning(KIO_CORE) << "-miniicon isn't supported anymore (service" << service.name() << ')';
77 break;
78 case 'u':
79 case 'U':
80 hasUrls = true;
81 Q_FALLTHROUGH();
82 /* fallthrough */
83 case 'f':
84 case 'F':
85 case 'n':
86 case 'N':
87 case 'd':
88 case 'D':
89 case 'v':
90 hasSpec = true;
91 Q_FALLTHROUGH();
92 /* fallthrough */
93 default:
94 return -2; // subst with same and skip
95 }
96 return 2;
97}
98
99class KRunMX2 : public KMacroExpanderBase
100{
101public:
102 explicit KRunMX2(const QList<QUrl> &_urls)
104 , ignFile(false)
105 , urls(_urls)
106 {
107 }
108
109 bool ignFile;
110
111protected:
112 int expandEscapedMacro(const QString &str, int pos, QStringList &ret) override;
113
114private:
115 void subst(int option, const QUrl &url, QStringList &ret);
116
117 const QList<QUrl> &urls;
118};
119
120void KRunMX2::subst(int option, const QUrl &url, QStringList &ret)
121{
122 switch (option) {
123 case 'u':
124 ret << ((url.isLocalFile() && url.fragment().isNull() && url.query().isNull()) ? QDir::toNativeSeparators(url.toLocalFile()) : url.toString());
125 break;
126 case 'd':
127 ret << url.adjusted(QUrl::RemoveFilename).path();
128 break;
129 case 'f':
131 break;
132 case 'n':
133 ret << url.fileName();
134 break;
135 case 'v':
136 if (url.isLocalFile() && QFile::exists(url.toLocalFile())) {
137 ret << KDesktopFile(url.toLocalFile()).desktopGroup().readEntry("Dev");
138 }
139 break;
140 }
141 return;
142}
143
144int KRunMX2::expandEscapedMacro(const QString &str, int pos, QStringList &ret)
145{
146 uint option = str[pos + 1].unicode();
147 switch (option) {
148 case 'f':
149 case 'u':
150 case 'n':
151 case 'd':
152 case 'v':
153 if (urls.isEmpty()) {
154 if (!ignFile) {
155 // qCDebug(KIO_CORE) << "No URLs supplied to single-URL service" << str;
156 }
157 } else if (urls.count() > 1) {
158 qCWarning(KIO_CORE) << urls.count() << "URLs supplied to single-URL service" << str;
159 } else {
160 subst(option, urls.first(), ret);
161 }
162 break;
163 case 'F':
164 case 'U':
165 case 'N':
166 case 'D':
167 option += 'a' - 'A';
168 for (const QUrl &url : urls) {
169 subst(option, url, ret);
170 }
171 break;
172 case '%':
173 ret = QStringList(QStringLiteral("%"));
174 break;
175 default:
176 return -2; // subst with same and skip
177 }
178 return 2;
179}
180
182{
184
185 KRunMX1 mx1(service);
186 QString exec = service.exec();
187 if (mx1.expandMacrosShellQuote(exec) && !mx1.hasUrls) {
189 qCWarning(KIO_CORE) << service.entryPath() << "contains supported protocols but doesn't use %u or %U in its Exec line! This is inconsistent.";
190 }
191 return QStringList();
192 } else {
194 // compat mode: assume KIO if not set and it's a KDE app (or a KDE service)
195 const QStringList categories = service.property<QStringList>(QStringLiteral("Categories"));
196 if (categories.contains(QLatin1String("KDE")) || !service.isApplication() || service.entryPath().isEmpty() /*temp service*/) {
197 supportedProtocols.append(QStringLiteral("KIO"));
198 } else { // if no KDE app, be a bit over-generic
199 supportedProtocols.append(QStringLiteral("http"));
200 supportedProtocols.append(QStringLiteral("https")); // #253294
201 supportedProtocols.append(QStringLiteral("ftp"));
202 }
203 }
204 }
205
206 // qCDebug(KIO_CORE) << "supportedProtocols:" << supportedProtocols;
207 return supportedProtocols;
208}
209
210bool KIO::DesktopExecParser::isProtocolInSupportedList(const QUrl &url, const QStringList &supportedProtocols)
211{
212 return url.isLocalFile() //
213 || supportedProtocols.contains(QLatin1String("KIO")) //
214 || supportedProtocols.contains(url.scheme(), Qt::CaseInsensitive);
215}
216
217// We have up to two sources of data, for protocols not handled by KIO workers (so called "helper") :
218// 1) the exec line of the .protocol file, if there's one
219// 2) the application associated with x-scheme-handler/<protocol> if there's one
220bool KIO::DesktopExecParser::hasSchemeHandler(const QUrl &url) // KF6 TODO move to OpenUrlJob
221{
223 return true;
224 }
225 const KService::Ptr service = KApplicationTrader::preferredService(QLatin1String("x-scheme-handler/") + url.scheme());
226 if (service) {
227 qCDebug(KIO_CORE) << QLatin1String("preferred service for x-scheme-handler/") + url.scheme() << service->desktopEntryName();
228 }
229 return service;
230}
231
232class KIO::DesktopExecParserPrivate
233{
234public:
235 DesktopExecParserPrivate(const KService &_service, const QList<QUrl> &_urls)
236 : service(_service)
237 , urls(_urls)
238 , tempFiles(false)
239 {
240 }
241
242 bool isUrlSupported(const QUrl &url, const QStringList &supportedProtocols);
243
244 const KService &service;
245 QList<QUrl> urls;
246 bool tempFiles;
247 QString suggestedFileName;
248 QString m_errorString;
249};
250
252 : d(new DesktopExecParserPrivate(service, urls))
253{
254}
255
259
261{
262 d->tempFiles = tempFiles;
263}
264
266{
267 d->suggestedFileName = suggestedFileName;
268}
269
270static const QString kioexecPath()
271{
273 if (!QFileInfo::exists(kioexec)) {
274 kioexec = QStringLiteral(KDE_INSTALL_FULL_LIBEXECDIR_KF "/kioexec");
275 }
276 Q_ASSERT(QFileInfo::exists(kioexec));
277 return kioexec;
278}
279
280static QString findNonExecutableProgram(const QString &executable)
281{
282 // Relative to current dir, or absolute path
283 const QFileInfo fi(executable);
284 if (fi.exists() && !fi.isExecutable()) {
285 return executable;
286 }
287
288#ifdef Q_OS_UNIX
289 // This is a *very* simplified version of QStandardPaths::findExecutable
290 const QStringList searchPaths = QString::fromLocal8Bit(qgetenv("PATH")).split(QDir::listSeparator(), Qt::SkipEmptyParts);
291 for (const QString &searchPath : searchPaths) {
292 const QString candidate = searchPath + QLatin1Char('/') + executable;
293 const QFileInfo fileInfo(candidate);
294 if (fileInfo.exists()) {
295 if (fileInfo.isExecutable()) {
296 qWarning() << "Internal program error. QStandardPaths::findExecutable couldn't find" << executable << "but our own logic found it at"
297 << candidate << ". Please report a bug at https://bugs.kde.org";
298 } else {
299 return candidate;
300 }
301 }
302 }
303#endif
304 return QString();
305}
306
307bool KIO::DesktopExecParserPrivate::isUrlSupported(const QUrl &url, const QStringList &protocols)
308{
310 return true;
311 }
312
313 // supportedProtocols() only checks whether the .desktop file has MimeType=x-scheme-handler/xxx
314 // We also want to check whether the app has been set as default/associated in mimeapps.list
315 const auto handlers = KApplicationTrader::queryByMimeType(QLatin1String("x-scheme-handler/") + url.scheme());
316 for (const KService::Ptr &handler : handlers) {
317 if (handler->desktopEntryName() == service.desktopEntryName()) {
318 return true;
319 }
320 }
321
322 return false;
323}
324
326{
327 QString exec = d->service.exec();
328 if (exec.isEmpty()) {
329 d->m_errorString = i18n("No Exec field in %1", d->service.entryPath());
330 qCWarning(KIO_CORE) << "No Exec field in" << d->service.entryPath();
331 return QStringList();
332 }
333
334 // Extract the name of the binary to execute from the full Exec line, to see if it exists
335 const QString binary = executablePath(exec);
336 QString executableFullPath;
337 if (!binary.isEmpty()) { // skip all this if the Exec line is a complex shell command
338 if (QDir::isRelativePath(binary)) {
339 // Resolve the executable to ensure that helpers in libexec are found.
340 // Too bad for commands that need a shell - they must reside in $PATH.
341 executableFullPath = QStandardPaths::findExecutable(binary);
342 if (executableFullPath.isEmpty()) {
343 executableFullPath = QFile::decodeName(KDE_INSTALL_FULL_LIBEXECDIR_KF "/") + binary;
344 }
345 } else {
346 executableFullPath = binary;
347 }
348
349 // Now check that the binary exists and has the executable flag
350 if (!QFileInfo(executableFullPath).isExecutable()) {
351 // Does it really not exist, or is it non-executable (on Unix)? (bug #415567)
352 const QString nonExecutable = findNonExecutableProgram(binary);
353 if (nonExecutable.isEmpty()) {
354 d->m_errorString = i18n("Could not find the program '%1'", binary);
355 } else {
356 if (QDir::isRelativePath(binary)) {
357 d->m_errorString = i18n("The program '%1' was found at '%2' but it is missing executable permissions.", binary, nonExecutable);
358 } else {
359 d->m_errorString = i18n("The program '%1' is missing executable permissions.", nonExecutable);
360 }
361 }
362 return QStringList();
363 }
364 }
365
366 QStringList result;
367 bool appHasTempFileOption;
368
369 KRunMX1 mx1(d->service);
370 KRunMX2 mx2(d->urls);
371
372 if (!mx1.expandMacrosShellQuote(exec)) { // Error in shell syntax
373 d->m_errorString = i18n("Syntax error in command %1 coming from %2", exec, d->service.entryPath());
374 qCWarning(KIO_CORE) << "Syntax error in command" << d->service.exec() << ", service" << d->service.name();
375 return QStringList();
376 }
377
378 // FIXME: the current way of invoking kioexec disables term and su use
379
380 // Check if we need "tempexec" (kioexec in fact)
381 appHasTempFileOption = d->tempFiles && d->service.property<bool>(QStringLiteral("X-KDE-HasTempFileOption"));
382 if (d->tempFiles && !appHasTempFileOption && d->urls.size()) {
383 result << kioexecPath() << QStringLiteral("--tempfiles") << exec;
384 if (!d->suggestedFileName.isEmpty()) {
385 result << QStringLiteral("--suggestedfilename");
386 result << d->suggestedFileName;
387 }
388 result += QUrl::toStringList(d->urls);
389 return result;
390 }
391
392#ifdef WITH_QTDBUS
393 // Return true for non-KIO desktop files with explicit X-KDE-Protocols list, like vlc, for the special case below
394 auto isNonKIO = [this]() {
395 const QStringList protocols = d->service.property<QStringList>(QStringLiteral("X-KDE-Protocols"));
396 return !protocols.isEmpty() && !protocols.contains(QLatin1String("KIO"));
397 };
398
399 // Check if we need kioexec, or KIOFuse
400 bool useKioexec = false;
401
402 org::kde::KIOFuse::VFS kiofuse_iface(QStringLiteral("org.kde.KIOFuse"), QStringLiteral("/org/kde/KIOFuse"), QDBusConnection::sessionBus());
403 struct MountRequest {
405 int urlIndex;
406 };
407 QList<MountRequest> requests;
408 requests.reserve(d->urls.count());
409
410 const QStringList appSupportedProtocols = supportedProtocols(d->service);
411 for (int i = 0; i < d->urls.count(); ++i) {
412 const QUrl url = d->urls.at(i);
413 const bool supported = mx1.hasUrls ? d->isUrlSupported(url, appSupportedProtocols) : url.isLocalFile();
414 if (!supported) {
415 // If FUSE fails, and there is no scheme handler, we'll have to fallback to kioexec
416 useKioexec = true;
417 }
418
419 // NOTE: Some non-KIO apps may support the URLs (e.g. VLC supports smb://)
420 // but will not have the password if they are not in the URL itself.
421 // Hence convert URL to KIOFuse equivalent in case there is a password.
422 // @see https://pointieststick.com/2018/01/17/videos-on-samba-shares/
423 // @see https://bugs.kde.org/show_bug.cgi?id=330192
424 if (!supported || (!url.userName().isEmpty() && url.password().isEmpty() && isNonKIO())) {
425 requests.push_back({kiofuse_iface.mountUrl(url.toString()), i});
426 }
427 }
428
429 for (auto &request : requests) {
430 request.reply.waitForFinished();
431 }
432 const bool fuseError = std::any_of(requests.cbegin(), requests.cend(), [](const MountRequest &request) {
433 return request.reply.isError();
434 });
435
436 if (fuseError && useKioexec) {
437 // We need to run the app through kioexec
438 result << kioexecPath();
439 if (d->tempFiles) {
440 result << QStringLiteral("--tempfiles");
441 }
442 if (!d->suggestedFileName.isEmpty()) {
443 result << QStringLiteral("--suggestedfilename");
444 result << d->suggestedFileName;
445 }
446 result << exec;
447 result += QUrl::toStringList(d->urls);
448 return result;
449 }
450
451 // At this point we know we're not using kioexec, so feel free to replace
452 // KIO URLs with their KIOFuse local path.
453 for (const auto &request : std::as_const(requests)) {
454 if (!request.reply.isError()) {
455 d->urls[request.urlIndex] = QUrl::fromLocalFile(request.reply.value());
456 }
457 }
458#endif
459
460 if (appHasTempFileOption) {
461 exec += QLatin1String(" --tempfile");
462 }
463
464 // Did the user forget to append something like '%f'?
465 // If so, then assume that '%f' is the right choice => the application
466 // accepts only local files.
467 if (!mx1.hasSpec) {
468 exec += QLatin1String(" %f");
469 mx2.ignFile = true;
470 }
471
472 mx2.expandMacrosShellQuote(exec); // syntax was already checked, so don't check return value
473
474 /*
475 1 = need_shell, 2 = terminal, 4 = su
476
477 0 << split(cmd)
478 1 << "sh" << "-c" << cmd
479 2 << split(term) << "-e" << split(cmd)
480 3 << split(term) << "-e" << "sh" << "-c" << cmd
481
482 4 << "kdesu" << "-u" << user << "-c" << cmd
483 5 << "kdesu" << "-u" << user << "-c" << ("sh -c " + quote(cmd))
484 6 << split(term) << "-e" << "su" << user << "-c" << cmd
485 7 << split(term) << "-e" << "su" << user << "-c" << ("sh -c " + quote(cmd))
486
487 "sh -c" is needed in the "su" case, too, as su uses the user's login shell, not sh.
488 this could be optimized with the -s switch of some su versions (e.g., debian linux).
489 */
490
491 if (d->service.terminal()) {
492 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("General"));
493 QString terminal = cg.readPathEntry("TerminalApplication", QStringLiteral("konsole"));
494
495 const bool isKonsole = (terminal == QLatin1String("konsole"));
496 QStringList terminalParts = KShell::splitArgs(terminal);
497 QString terminalPath;
498 if (!terminalParts.isEmpty()) {
499 terminalPath = QStandardPaths::findExecutable(terminalParts.at(0));
500 }
501
502 if (terminalPath.isEmpty()) {
503 d->m_errorString = i18n("Terminal %1 not found while trying to run %2", terminal, d->service.entryPath());
504 qCWarning(KIO_CORE) << "Terminal" << terminal << "not found, service" << d->service.name();
505 return QStringList();
506 }
507 terminalParts[0] = terminalPath;
508 terminal = KShell::joinArgs(terminalParts);
509 if (isKonsole) {
510 if (!d->service.workingDirectory().isEmpty()) {
511 terminal += QLatin1String(" --workdir ") + KShell::quoteArg(d->service.workingDirectory());
512 }
513 terminal += QLatin1String(" -qwindowtitle '%c'");
514 if (!d->service.icon().isEmpty()) {
515 terminal += QLatin1String(" -qwindowicon ") + KShell::quoteArg(d->service.icon().replace(QLatin1Char('%'), QLatin1String("%%")));
516 }
517 }
518 terminal += QLatin1Char(' ') + d->service.terminalOptions();
519 if (!mx1.expandMacrosShellQuote(terminal)) {
520 d->m_errorString = i18n("Syntax error in command %1 while trying to run %2", terminal, d->service.entryPath());
521 qCWarning(KIO_CORE) << "Syntax error in command" << terminal << ", service" << d->service.name();
522 return QStringList();
523 }
524 mx2.expandMacrosShellQuote(terminal);
525 result = KShell::splitArgs(terminal); // assuming that the term spec never needs a shell!
526 result << QStringLiteral("-e");
527 }
528
529 KShell::Errors err;
531 if (!executableFullPath.isEmpty()) {
532 execlist[0] = executableFullPath;
533 }
534
535 if (d->service.substituteUid()) {
536 if (d->service.terminal()) {
537 result << QStringLiteral("su");
538 } else {
539 QString kdesu = QFile::decodeName(KDE_INSTALL_FULL_LIBEXECDIR_KF "/kdesu");
540 if (!QFile::exists(kdesu)) {
541 kdesu = QStandardPaths::findExecutable(QStringLiteral("kdesu"));
542 }
543 if (!QFile::exists(kdesu)) {
544 // Insert kdesu as string so we show a nice warning: 'Could not launch kdesu'
545 result << QStringLiteral("kdesu");
546 return result;
547 } else {
548 result << kdesu << QStringLiteral("-u");
549 }
550 }
551
552 result << d->service.username() << QStringLiteral("-c");
553 if (err == KShell::FoundMeta) {
554 exec = QLatin1String("/bin/sh -c ") + KShell::quoteArg(exec);
555 } else {
556 exec = KShell::joinArgs(execlist);
557 }
558 result << exec;
559 } else {
560 if (err == KShell::FoundMeta) {
561 result << QStringLiteral("/bin/sh") << QStringLiteral("-c") << exec;
562 } else {
563 result += execlist;
564 }
565 }
566
567 return result;
568}
569
571{
572 return d->m_errorString;
573}
574
575// static
577{
578 const QString bin = executablePath(execLine);
579 return bin.mid(bin.lastIndexOf(QLatin1Char('/')) + 1);
580}
581
582// static
584{
585 // Remove parameters and/or trailing spaces.
587 auto it = std::find_if(args.cbegin(), args.cend(), [](const QString &arg) {
588 return !arg.contains(QLatin1Char('='));
589 });
590 return it != args.cend() ? *it : QString{};
591}
QString readPathEntry(const char *key, const QString &aDefault) const
QString readEntry(const char *key, const char *aDefault=nullptr) const
KConfigGroup desktopGroup() const
static bool hasSchemeHandler(const QUrl &url)
Returns true if protocol should be opened by a "handler" application, i.e. an application associated ...
void setUrlsAreTempFiles(bool tempFiles)
If tempFiles is set to true and the urls given to the constructor are local files,...
QStringList resultingArguments() const
DesktopExecParser(const KService &service, const QList< QUrl > &urls)
Creates a parser for a desktop file Exec line.
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 QStringList supportedProtocols(const KService &service)
Returns the list of protocols which the application supports.
static bool isProtocolInSupportedList(const QUrl &url, const QStringList &supportedProtocols)
Returns true if protocol is in the list of protocols returned by supportedProtocols().
void setSuggestedFileName(const QString &suggestedFileName)
Sets the file name to use in the case of downloading the file to a tempfile in order to give to a non...
bool expandMacrosShellQuote(QString &str)
static bool isHelperProtocol(const QUrl &url)
Returns whether the protocol can act as a helper protocol.
QString desktopEntryName() const
QStringList supportedProtocols() const
T property(const QString &name) const
QString exec() const
QString icon() const
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 i18n(const char *text, const TYPE &arg...)
KSERVICE_EXPORT KService::List queryByMimeType(const QString &mimeType, FilterFunc filterFunc={})
KSERVICE_EXPORT KService::Ptr preferredService(const QString &mimeType)
KCOREADDONS_EXPORT QStringList splitArgs(const QString &cmd, Options flags=NoOptions, Errors *err=nullptr)
KCOREADDONS_EXPORT QString quoteArg(const QString &arg)
KCOREADDONS_EXPORT QString joinArgs(const QStringList &args)
QString applicationDirPath()
QDBusConnection sessionBus()
bool isRelativePath(const QString &path)
QChar listSeparator()
QString toNativeSeparators(const QString &pathName)
QString decodeName(const QByteArray &localFileName)
bool exists() const const
bool exists() const const
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
const_iterator cbegin() const const
const_iterator cend() const const
qsizetype count() const const
T & first()
bool isEmpty() const const
void push_back(parameter_type value)
void reserve(qsizetype size)
QString findExecutable(const QString &executableName, const QStringList &paths)
QString fromLocal8Bit(QByteArrayView str)
bool isEmpty() const const
bool isNull() const const
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
const QChar * unicode() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
CaseInsensitive
SkipEmptyParts
RemoveFilename
QUrl adjusted(FormattingOptions options) const const
QString fileName(ComponentFormattingOptions options) const const
QString fragment(ComponentFormattingOptions options) const const
QUrl fromLocalFile(const QString &localFile)
bool isLocalFile() const const
QString password(ComponentFormattingOptions options) const const
QString path(ComponentFormattingOptions options) const const
QString query(ComponentFormattingOptions options) const const
QString scheme() const const
QString toLocalFile() const const
QString toString(FormattingOptions options) const const
QStringList toStringList(const QList< QUrl > &urls, FormattingOptions options)
QString userName(ComponentFormattingOptions options) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Nov 8 2024 11:56:19 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.