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