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
32namespace
33{
34Q_LOGGING_CATEGORY(category, "kf.kio.urifilters.shorturi", QtWarningMsg)
35}
36
37static 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
54static 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
79static 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
88KShortUriFilter::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
99bool 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
481void 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
504K_PLUGIN_CLASS_WITH_JSON(KShortUriFilter, "kshorturifilter.json")
505
506#include "kshorturifilter.moc"
507
508#include "moc_kshorturifilter.cpp"
509

source code of kio/src/urifilters/shorturi/kshorturifilter.cpp