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

KDE's Doxygen guidelines are available online.