1 | /* |
2 | kshorturifilter.h |
3 | |
4 | This file is part of the KDE project |
5 | SPDX-FileCopyrightText: 2000 Dawit Alemayehu <adawit@kde.org> |
6 | SPDX-FileCopyrightText: 2000 Malte Starostik <starosti@zedat.fu-berlin.de> |
7 | |
8 | SPDX-License-Identifier: GPL-2.0-or-later |
9 | */ |
10 | |
11 | #include "kshorturifilter.h" |
12 | #include "../utils_p.h" |
13 | |
14 | #ifdef WITH_QTDBUS |
15 | #include <QDBusConnection> |
16 | #endif |
17 | |
18 | #include <QDir> |
19 | #include <QLoggingCategory> |
20 | #include <qplatformdefs.h> |
21 | |
22 | #include <KApplicationTrader> |
23 | #include <KConfig> |
24 | #include <KConfigGroup> |
25 | #include <KLocalizedString> |
26 | #include <KPluginFactory> |
27 | #include <KService> |
28 | #include <KUser> |
29 | #include <kprotocolinfo.h> |
30 | #include <kurlauthorized.h> |
31 | |
32 | namespace |
33 | { |
34 | Q_LOGGING_CATEGORY(category, "kf.kio.urifilters.shorturi" , QtWarningMsg) |
35 | } |
36 | |
37 | static bool isPotentialShortURL(const QString &cmd) |
38 | { |
39 | // Host names and IPv4 address... |
40 | // Exclude ".." and paths starting with "../", these are used to go up in a filesystem |
41 | // dir hierarchy |
42 | if (cmd != QLatin1String(".." ) && !cmd.startsWith(s: QLatin1String("../" )) && cmd.contains(c: QLatin1Char('.'))) { |
43 | return true; |
44 | } |
45 | |
46 | // IPv6 Address... |
47 | if (cmd.startsWith(c: QLatin1Char('[')) && cmd.contains(c: QLatin1Char(':'))) { |
48 | return true; |
49 | } |
50 | |
51 | return false; |
52 | } |
53 | |
54 | static QString removeArgs(const QString &_cmd) |
55 | { |
56 | QString cmd(_cmd); |
57 | |
58 | if (cmd.isEmpty()) { |
59 | return cmd; |
60 | } |
61 | |
62 | if (cmd[0] != QLatin1Char('\'') && cmd[0] != QLatin1Char('"')) { |
63 | // Remove command-line options (look for first non-escaped space) |
64 | int spacePos = 0; |
65 | |
66 | do { |
67 | spacePos = cmd.indexOf(ch: QLatin1Char(' '), from: spacePos + 1); |
68 | } while (spacePos > 1 && cmd[spacePos - 1] == QLatin1Char('\\')); |
69 | |
70 | if (spacePos > 0) { |
71 | cmd.truncate(pos: spacePos); |
72 | qCDebug(category) << "spacePos=" << spacePos << " returning " << cmd; |
73 | } |
74 | } |
75 | |
76 | return cmd; |
77 | } |
78 | |
79 | static bool isKnownProtocol(const QString &protocol) |
80 | { |
81 | if (KProtocolInfo::isKnownProtocol(protocol, updateCacheIfNotfound: false) || protocol == QLatin1String("mailto" )) { |
82 | return true; |
83 | } |
84 | const KService::Ptr service = KApplicationTrader::preferredService(mimeType: QLatin1String("x-scheme-handler/" ) + protocol); |
85 | return service; |
86 | } |
87 | |
88 | KShortUriFilter::KShortUriFilter(QObject *parent, const KPluginMetaData &data) |
89 | : KUriFilterPlugin(parent, data) |
90 | { |
91 | #ifdef WITH_QTDBUS |
92 | QDBusConnection::sessionBus() |
93 | .connect(service: QString(), QStringLiteral("/" ), QStringLiteral("org.kde.KUriFilterPlugin" ), QStringLiteral("configure" ), receiver: this, SLOT(configure())); |
94 | #endif |
95 | |
96 | configure(); |
97 | } |
98 | |
99 | bool KShortUriFilter::filterUri(KUriFilterData &data) const |
100 | { |
101 | /* |
102 | * Here is a description of how the shortURI deals with the supplied |
103 | * data. First it expands any environment variable settings and then |
104 | * deals with special shortURI cases. These special cases are the "smb:" |
105 | * URL scheme which is very specific to KDE, "#" and "##" which are |
106 | * shortcuts for man:/ and info:/ protocols respectively. It then handles |
107 | * local files. Then it checks to see if the URL is valid and one that is |
108 | * supported by KDE's IO system. If all the above checks fails, it simply |
109 | * lookups the URL in the user-defined list and returns without filtering |
110 | * if it is not found. TODO: the user-defined table is currently only manually |
111 | * hackable and is missing a config dialog. |
112 | */ |
113 | |
114 | // QUrl url = data.uri(); |
115 | QString cmd = data.typedString(); |
116 | |
117 | int firstNonSlash = 0; |
118 | while (firstNonSlash < cmd.length() && (cmd.at(i: firstNonSlash) == QLatin1Char('/'))) { |
119 | firstNonSlash++; |
120 | } |
121 | if (firstNonSlash > 1) { |
122 | cmd.remove(i: 0, len: firstNonSlash - 1); |
123 | } |
124 | |
125 | // Replicate what KUrl(cmd) did in KDE4. This could later be folded into the checks further down... |
126 | QUrl url; |
127 | if (Utils::isAbsoluteLocalPath(path: cmd)) { |
128 | url = QUrl::fromLocalFile(localfile: cmd); |
129 | } else { |
130 | url.setUrl(url: cmd); |
131 | } |
132 | |
133 | // WORKAROUND: Allow the use of '@' in the username component of a URL since |
134 | // other browsers such as firefox in their infinite wisdom allow such blatant |
135 | // violations of RFC 3986. BR# 69326/118413. |
136 | if (cmd.count(c: QLatin1Char('@')) > 1) { |
137 | const int lastIndex = cmd.lastIndexOf(c: QLatin1Char('@')); |
138 | // Percent encode all but the last '@'. |
139 | const auto suffix = QStringView(cmd).mid(pos: lastIndex); |
140 | cmd = QString::fromUtf8(ba: QUrl::toPercentEncoding(cmd.left(n: lastIndex), QByteArrayLiteral(":/" ))) + suffix; |
141 | url.setUrl(url: cmd); |
142 | } |
143 | |
144 | const bool isMalformed = !url.isValid(); |
145 | QString protocol = url.scheme(); |
146 | |
147 | qCDebug(category) << cmd; |
148 | |
149 | // Fix misparsing of "foo:80", QUrl thinks "foo" is the protocol and "80" is the path. |
150 | // However, be careful not to do that for valid hostless URLs, e.g. file:///foo! |
151 | if (!protocol.isEmpty() && url.host().isEmpty() && !url.path().isEmpty() && cmd.contains(c: QLatin1Char(':')) && !isKnownProtocol(protocol)) { |
152 | protocol.clear(); |
153 | } |
154 | |
155 | qCDebug(category) << "url=" << url << "cmd=" << cmd << "isMalformed=" << isMalformed; |
156 | |
157 | // TODO: Make this a bit more intelligent for Minicli! There |
158 | // is no need to make comparisons if the supplied data is a local |
159 | // executable and only the argument part, if any, changed! (Dawit) |
160 | // You mean caching the last filtering, to try and reuse it, to save stat()s? (David) |
161 | |
162 | const QLatin1String starthere_proto("start-here:" ); |
163 | if (cmd.startsWith(s: starthere_proto)) { |
164 | setFilteredUri(data, uri: QUrl(QStringLiteral("system:/" ))); |
165 | setUriType(data, type: KUriFilterData::LocalDir); |
166 | return true; |
167 | } |
168 | |
169 | // Handle MAN & INFO pages shortcuts... |
170 | const QLatin1String man_proto("man:" ); |
171 | const QLatin1String info_proto("info:" ); |
172 | if (cmd.startsWith(c: QLatin1Char('#')) || cmd.startsWith(s: man_proto) || cmd.startsWith(s: info_proto)) { |
173 | QStringView sview(cmd); |
174 | if (cmd.startsWith(s: QLatin1String("##" ))) { |
175 | cmd = QLatin1String("info:/" ) + sview.mid(pos: 2); |
176 | } else if (cmd.startsWith(c: QLatin1Char('#'))) { |
177 | cmd = QLatin1String("man:/" ) + sview.mid(pos: 1); |
178 | } else if (cmd == info_proto || cmd == man_proto) { |
179 | cmd += QLatin1Char('/'); |
180 | } |
181 | |
182 | setFilteredUri(data, uri: QUrl(cmd)); |
183 | setUriType(data, type: KUriFilterData::Help); |
184 | return true; |
185 | } |
186 | |
187 | // Detect UNC style (aka windows SMB) URLs |
188 | if (cmd.startsWith(s: QLatin1String("\\\\" ))) { |
189 | // make sure path is unix style |
190 | cmd.replace(before: QLatin1Char('\\'), after: QLatin1Char('/')); |
191 | cmd.prepend(s: QLatin1String("smb:" )); |
192 | setFilteredUri(data, uri: QUrl(cmd)); |
193 | setUriType(data, type: KUriFilterData::NetProtocol); |
194 | return true; |
195 | } |
196 | |
197 | bool expanded = false; |
198 | |
199 | // Expanding shortcut to HOME URL... |
200 | QString path; |
201 | QString ref; |
202 | QString query; |
203 | QString nameFilter; |
204 | |
205 | if (!Utils::isAbsoluteLocalPath(path: cmd) && QUrl(cmd).isRelative()) { |
206 | path = cmd; |
207 | qCDebug(category) << "path=cmd=" << path; |
208 | } else { |
209 | if (url.isLocalFile()) { |
210 | qCDebug(category) << "hasRef=" << url.hasFragment(); |
211 | // Split path from ref/query |
212 | // but not for "/tmp/a#b", if "a#b" is an existing file, |
213 | // or for "/tmp/a?b" (#58990) |
214 | if ((url.hasFragment() || !url.query().isEmpty()) && !url.path().endsWith(c: QLatin1Char('/'))) { // /tmp/?foo is a namefilter, not a query |
215 | path = url.path(); |
216 | ref = url.fragment(); |
217 | qCDebug(category) << "isLocalFile set path to" << path << "and ref to" << ref; |
218 | query = url.query(); |
219 | if (path.isEmpty() && !url.host().isEmpty()) { |
220 | path = QStringLiteral("/" ); |
221 | } |
222 | } else { |
223 | if (cmd.startsWith(s: QLatin1String("file://" ))) { |
224 | path = cmd.mid(position: strlen(s: "file://" )); |
225 | } else { |
226 | path = cmd; |
227 | } |
228 | qCDebug(category) << "(2) path=cmd=" << path; |
229 | } |
230 | } |
231 | } |
232 | |
233 | if (path.startsWith(c: QLatin1Char('~'))) { |
234 | int slashPos = path.indexOf(ch: QLatin1Char('/')); |
235 | if (slashPos == -1) { |
236 | slashPos = path.length(); |
237 | } |
238 | if (slashPos == 1) { // ~/ |
239 | path.replace(i: 0, len: 1, after: QDir::homePath()); |
240 | } else { // ~username/ |
241 | const QString userName(path.mid(position: 1, n: slashPos - 1)); |
242 | KUser user(userName); |
243 | if (user.isValid() && !user.homeDir().isEmpty()) { |
244 | path.replace(i: 0, len: slashPos, after: user.homeDir()); |
245 | } else { |
246 | if (user.isValid()) { |
247 | setErrorMsg(data, i18n("<qt><b>%1</b> does not have a home folder.</qt>" , userName)); |
248 | } else { |
249 | setErrorMsg(data, i18n("<qt>There is no user called <b>%1</b>.</qt>" , userName)); |
250 | } |
251 | setUriType(data, type: KUriFilterData::Error); |
252 | // Always return true for error conditions so |
253 | // that other filters will not be invoked !! |
254 | return true; |
255 | } |
256 | } |
257 | expanded = true; |
258 | } else if (path.startsWith(c: QLatin1Char('$'))) { |
259 | // Environment variable expansion. |
260 | static const QRegularExpression envVarExp(QStringLiteral("\\$[a-zA-Z_][a-zA-Z0-9_]*" )); |
261 | const auto match = envVarExp.match(subject: path); |
262 | if (match.hasMatch()) { |
263 | const QByteArray exp = qgetenv(varName: QStringView(path).mid(pos: 1, n: match.capturedLength(nth: 0) - 1).toLocal8Bit().constData()); |
264 | if (!exp.isEmpty()) { |
265 | path.replace(i: 0, len: match.capturedLength(nth: 0), after: QFile::decodeName(localFileName: exp)); |
266 | expanded = true; |
267 | } |
268 | } |
269 | } |
270 | |
271 | if (expanded || cmd.startsWith(c: QLatin1Char('/'))) { |
272 | // Look for #ref again, after $ and ~ expansion (testcase: $QTDIR/doc/html/functions.html#s) |
273 | // Can't use QUrl here, setPath would escape it... |
274 | const int pos = path.indexOf(ch: QLatin1Char('#')); |
275 | if (pos > -1) { |
276 | const QString newPath = path.left(n: pos); |
277 | if (QFile::exists(fileName: newPath)) { |
278 | ref = path.mid(position: pos + 1); |
279 | path = newPath; |
280 | qCDebug(category) << "Extracted ref: path=" << path << " ref=" << ref; |
281 | } |
282 | } |
283 | } |
284 | |
285 | bool isLocalFullPath = Utils::isAbsoluteLocalPath(path); |
286 | |
287 | // Checking for local resource match... |
288 | // Determine if "uri" is an absolute path to a local resource OR |
289 | // A local resource with a supplied absolute path in KUriFilterData |
290 | const QString abs_path = data.absolutePath(); |
291 | |
292 | const bool canBeAbsolute = (protocol.isEmpty() && !abs_path.isEmpty()); |
293 | const bool canBeLocalAbsolute = (canBeAbsolute && abs_path.startsWith(c: QLatin1Char('/')) && !isMalformed); |
294 | bool exists = false; |
295 | |
296 | /*qCDebug(category) << "abs_path=" << abs_path |
297 | << "protocol=" << protocol |
298 | << "canBeAbsolute=" << canBeAbsolute |
299 | << "canBeLocalAbsolute=" << canBeLocalAbsolute |
300 | << "isLocalFullPath=" << isLocalFullPath;*/ |
301 | |
302 | QT_STATBUF buff; |
303 | if (canBeLocalAbsolute) { |
304 | QString abs = QDir::cleanPath(path: abs_path); |
305 | // combine absolute path (abs_path) and relative path (cmd) into abs_path |
306 | int len = path.length(); |
307 | if ((len == 1 && path[0] == QLatin1Char('.')) || (len == 2 && path[0] == QLatin1Char('.') && path[1] == QLatin1Char('.'))) { |
308 | path += QLatin1Char('/'); |
309 | } |
310 | qCDebug(category) << "adding " << abs << " and " << path; |
311 | abs = QDir::cleanPath(path: abs + QLatin1Char('/') + path); |
312 | qCDebug(category) << "checking whether " << abs << " exists." ; |
313 | // Check if it exists |
314 | if (QT_STAT(file: QFile::encodeName(fileName: abs).constData(), buf: &buff) == 0) { |
315 | path = abs; // yes -> store as the new cmd |
316 | exists = true; |
317 | isLocalFullPath = true; |
318 | } |
319 | } |
320 | |
321 | if (isLocalFullPath && !exists && !isMalformed) { |
322 | exists = QT_STAT(file: QFile::encodeName(fileName: path).constData(), buf: &buff) == 0; |
323 | |
324 | if (!exists) { |
325 | // Support for name filter (/foo/*.txt), see also KonqMainWindow::detectNameFilter |
326 | // If the app using this filter doesn't support it, well, it'll simply error out itself |
327 | int lastSlash = path.lastIndexOf(c: QLatin1Char('/')); |
328 | if (lastSlash > -1 |
329 | && path.indexOf(ch: QLatin1Char(' '), from: lastSlash) == -1) { // no space after last slash, otherwise it's more likely command-line arguments |
330 | QString fileName = path.mid(position: lastSlash + 1); |
331 | QString testPath = path.left(n: lastSlash); |
332 | if ((fileName.indexOf(ch: QLatin1Char('*')) != -1 || fileName.indexOf(ch: QLatin1Char('[')) != -1 || fileName.indexOf(ch: QLatin1Char('?')) != -1) |
333 | && QT_STAT(file: QFile::encodeName(fileName: testPath).constData(), buf: &buff) == 0) { |
334 | nameFilter = fileName; |
335 | qCDebug(category) << "Setting nameFilter to" << nameFilter << "and path to" << testPath; |
336 | path = testPath; |
337 | exists = true; |
338 | } |
339 | } |
340 | } |
341 | } |
342 | |
343 | qCDebug(category) << "path =" << path << " isLocalFullPath=" << isLocalFullPath << " exists=" << exists << " url=" << url; |
344 | if (exists) { |
345 | QUrl u = QUrl::fromLocalFile(localfile: path); |
346 | qCDebug(category) << "ref=" << ref << "query=" << query; |
347 | u.setFragment(fragment: ref); |
348 | u.setQuery(query); |
349 | |
350 | if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("open" ), baseUrl: QUrl(), destUrl: u)) { |
351 | // No authorization, we pretend it's a file will get |
352 | // an access denied error later on. |
353 | setFilteredUri(data, uri: u); |
354 | setUriType(data, type: KUriFilterData::LocalFile); |
355 | return true; |
356 | } |
357 | |
358 | // Can be abs path to file or directory, or to executable with args |
359 | bool isDir = Utils::isDirMask(mode: buff.st_mode); |
360 | if (!isDir && access(name: QFile::encodeName(fileName: path).data(), X_OK) == 0) { |
361 | qCDebug(category) << "Abs path to EXECUTABLE" ; |
362 | setFilteredUri(data, uri: u); |
363 | setUriType(data, type: KUriFilterData::Executable); |
364 | return true; |
365 | } |
366 | |
367 | // Open "uri" as file:/xxx if it is a non-executable local resource. |
368 | if (isDir || Utils::isRegFileMask(mode: buff.st_mode)) { |
369 | qCDebug(category) << "Abs path as local file or directory" ; |
370 | if (!nameFilter.isEmpty()) { |
371 | u.setPath(path: Utils::concatPaths(path1: u.path(), path2: nameFilter)); |
372 | } |
373 | setFilteredUri(data, uri: u); |
374 | setUriType(data, type: (isDir) ? KUriFilterData::LocalDir : KUriFilterData::LocalFile); |
375 | return true; |
376 | } |
377 | |
378 | // Should we return LOCAL_FILE for non-regular files too? |
379 | qCDebug(category) << "File found, but not a regular file nor dir... socket?" ; |
380 | } |
381 | |
382 | if (data.checkForExecutables()) { |
383 | // Let us deal with possible relative URLs to see |
384 | // if it is executable under the user's $PATH variable. |
385 | // We try hard to avoid parsing any possible command |
386 | // line arguments or options that might have been supplied. |
387 | QString exe = removeArgs(cmd: cmd); |
388 | qCDebug(category) << "findExe with" << exe; |
389 | |
390 | if (!QStandardPaths::findExecutable(executableName: exe).isNull()) { |
391 | qCDebug(category) << "EXECUTABLE exe=" << exe; |
392 | setFilteredUri(data, uri: QUrl::fromLocalFile(localfile: exe)); |
393 | // check if we have command line arguments |
394 | if (exe != cmd) { |
395 | setArguments(data, args: cmd.right(n: cmd.length() - exe.length())); |
396 | } |
397 | setUriType(data, type: KUriFilterData::Executable); |
398 | return true; |
399 | } |
400 | } |
401 | |
402 | // Process URLs of known and supported protocols so we don't have |
403 | // to resort to the pattern matching scheme below which can possibly |
404 | // slow things down... |
405 | if (!isMalformed && !isLocalFullPath && !protocol.isEmpty()) { |
406 | qCDebug(category) << "looking for protocol" << protocol; |
407 | if (isKnownProtocol(protocol)) { |
408 | setFilteredUri(data, uri: url); |
409 | if (protocol == QLatin1String("man" ) || protocol == QLatin1String("help" )) { |
410 | setUriType(data, type: KUriFilterData::Help); |
411 | } else { |
412 | setUriType(data, type: KUriFilterData::NetProtocol); |
413 | } |
414 | return true; |
415 | } |
416 | } |
417 | |
418 | // Short url matches |
419 | if (!cmd.contains(c: QLatin1Char(' '))) { |
420 | // Okay this is the code that allows users to supply custom matches for specific |
421 | // URLs using Qt's QRegularExpression class. |
422 | for (const URLHint &hint : std::as_const(t: m_urlHints)) { |
423 | qCDebug(category) << "testing regexp for" << hint.prepend; |
424 | if (hint.hintRe.match(subject: cmd).capturedStart() == 0) { |
425 | const QString cmdStr = hint.prepend + cmd; |
426 | QUrl cmdUrl(cmdStr); |
427 | qCDebug(category) << "match - prepending" << hint.prepend << "->" << cmdStr << "->" << cmdUrl; |
428 | setFilteredUri(data, uri: cmdUrl); |
429 | setUriType(data, type: hint.type); |
430 | return true; |
431 | } |
432 | } |
433 | |
434 | // No protocol and not malformed means a valid short URL such as kde.org or |
435 | // user@192.168.0.1. However, it might also be valid only because it lacks |
436 | // the scheme component, e.g. www.kde,org (illegal ',' before 'org'). The |
437 | // check below properly deciphers the difference between the two and sends |
438 | // back the proper result. |
439 | if (protocol.isEmpty() && isPotentialShortURL(cmd)) { |
440 | QString urlStr = data.defaultUrlScheme(); |
441 | if (urlStr.isEmpty()) { |
442 | urlStr = m_strDefaultUrlScheme; |
443 | } |
444 | |
445 | const int index = urlStr.indexOf(ch: QLatin1Char(':')); |
446 | if (index == -1 || !isKnownProtocol(protocol: urlStr.left(n: index))) { |
447 | urlStr += QStringLiteral("://" ); |
448 | } |
449 | urlStr += cmd; |
450 | |
451 | QUrl fixedUrl(urlStr); |
452 | if (fixedUrl.isValid()) { |
453 | setFilteredUri(data, uri: fixedUrl); |
454 | setUriType(data, type: KUriFilterData::NetProtocol); |
455 | } else if (isKnownProtocol(protocol: fixedUrl.scheme())) { |
456 | setFilteredUri(data, uri: data.uri()); |
457 | setUriType(data, type: KUriFilterData::Error); |
458 | } |
459 | return true; |
460 | } |
461 | } |
462 | |
463 | // If we previously determined that the URL might be a file, |
464 | // and if it doesn't exist... we'll pretend it exists. |
465 | // This allows to use it for completion purposes. |
466 | // (If you change this logic again, look at the commit that was testing |
467 | // for KUrlAuthorized::authorizeUrlAction("open")) |
468 | if (isLocalFullPath && !exists) { |
469 | QUrl u = QUrl::fromLocalFile(localfile: path); |
470 | u.setFragment(fragment: ref); |
471 | setFilteredUri(data, uri: u); |
472 | setUriType(data, type: KUriFilterData::LocalFile); |
473 | return true; |
474 | } |
475 | |
476 | // If we reach this point, we cannot filter this thing so simply return false |
477 | // so that other filters, if present, can take a crack at it. |
478 | return false; |
479 | } |
480 | |
481 | void KShortUriFilter::configure() |
482 | { |
483 | KConfig config(objectName() + QStringLiteral("rc" ), KConfig::NoGlobals); |
484 | KConfigGroup cg(config.group(group: QString())); |
485 | |
486 | m_strDefaultUrlScheme = cg.readEntry(key: "DefaultProtocol" , QStringLiteral("https://" )); |
487 | const QMap<QString, QString> patterns = config.entryMap(QStringLiteral("Pattern" )); |
488 | const QMap<QString, QString> protocols = config.entryMap(QStringLiteral("Protocol" )); |
489 | KConfigGroup typeGroup(&config, QStringLiteral("Type" )); |
490 | |
491 | for (auto it = patterns.begin(); it != patterns.end(); ++it) { |
492 | QString protocol = protocols[it.key()]; |
493 | if (!protocol.isEmpty()) { |
494 | int type = typeGroup.readEntry(key: it.key(), aDefault: -1); |
495 | if (type > -1 && type <= KUriFilterData::Unknown) { |
496 | m_urlHints.append(t: URLHint(it.value(), protocol, static_cast<KUriFilterData::UriTypes>(type))); |
497 | } else { |
498 | m_urlHints.append(t: URLHint(it.value(), protocol)); |
499 | } |
500 | } |
501 | } |
502 | } |
503 | |
504 | K_PLUGIN_CLASS_WITH_JSON(KShortUriFilter, "kshorturifilter.json" ) |
505 | |
506 | #include "kshorturifilter.moc" |
507 | |
508 | #include "moc_kshorturifilter.cpp" |
509 | |