KIO

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

KDE's Doxygen guidelines are available online.