File indexing completed on 2024-04-28 03:55:44
0001 /* 0002 kshorturifilter.h 0003 0004 This file is part of the KDE project 0005 SPDX-FileCopyrightText: 2000 Dawit Alemayehu <adawit@kde.org> 0006 SPDX-FileCopyrightText: 2000 Malte Starostik <starosti@zedat.fu-berlin.de> 0007 0008 SPDX-License-Identifier: GPL-2.0-or-later 0009 */ 0010 0011 #include "kshorturifilter.h" 0012 #include "../utils_p.h" 0013 0014 #include <QDBusConnection> 0015 #include <QDir> 0016 #include <QLoggingCategory> 0017 #include <qplatformdefs.h> 0018 0019 #include <KApplicationTrader> 0020 #include <KConfig> 0021 #include <KConfigGroup> 0022 #include <KLocalizedString> 0023 #include <KPluginFactory> 0024 #include <KService> 0025 #include <KUser> 0026 #include <kprotocolinfo.h> 0027 #include <kurlauthorized.h> 0028 0029 namespace 0030 { 0031 Q_LOGGING_CATEGORY(category, "kf.kio.urifilters.shorturi", QtWarningMsg) 0032 } 0033 0034 static bool isPotentialShortURL(const QString &cmd) 0035 { 0036 // Host names and IPv4 address... 0037 // Exclude ".." and paths starting with "../", these are used to go up in a filesystem 0038 // dir hierarchy 0039 if (cmd != QLatin1String("..") && !cmd.startsWith(QLatin1String("../")) && cmd.contains(QLatin1Char('.'))) { 0040 return true; 0041 } 0042 0043 // IPv6 Address... 0044 if (cmd.startsWith(QLatin1Char('[')) && cmd.contains(QLatin1Char(':'))) { 0045 return true; 0046 } 0047 0048 return false; 0049 } 0050 0051 static QString removeArgs(const QString &_cmd) 0052 { 0053 QString cmd(_cmd); 0054 0055 if (cmd.isEmpty()) { 0056 return cmd; 0057 } 0058 0059 if (cmd[0] != QLatin1Char('\'') && cmd[0] != QLatin1Char('"')) { 0060 // Remove command-line options (look for first non-escaped space) 0061 int spacePos = 0; 0062 0063 do { 0064 spacePos = cmd.indexOf(QLatin1Char(' '), spacePos + 1); 0065 } while (spacePos > 1 && cmd[spacePos - 1] == QLatin1Char('\\')); 0066 0067 if (spacePos > 0) { 0068 cmd.truncate(spacePos); 0069 qCDebug(category) << "spacePos=" << spacePos << " returning " << cmd; 0070 } 0071 } 0072 0073 return cmd; 0074 } 0075 0076 static bool isKnownProtocol(const QString &protocol) 0077 { 0078 if (KProtocolInfo::isKnownProtocol(protocol, false) || protocol == QLatin1String("mailto")) { 0079 return true; 0080 } 0081 const KService::Ptr service = KApplicationTrader::preferredService(QLatin1String("x-scheme-handler/") + protocol); 0082 return service; 0083 } 0084 0085 KShortUriFilter::KShortUriFilter(QObject *parent, const KPluginMetaData &data) 0086 : KUriFilterPlugin(parent, data) 0087 { 0088 QDBusConnection::sessionBus() 0089 .connect(QString(), QStringLiteral("/"), QStringLiteral("org.kde.KUriFilterPlugin"), QStringLiteral("configure"), this, SLOT(configure())); 0090 configure(); 0091 } 0092 0093 bool KShortUriFilter::filterUri(KUriFilterData &data) const 0094 { 0095 /* 0096 * Here is a description of how the shortURI deals with the supplied 0097 * data. First it expands any environment variable settings and then 0098 * deals with special shortURI cases. These special cases are the "smb:" 0099 * URL scheme which is very specific to KDE, "#" and "##" which are 0100 * shortcuts for man:/ and info:/ protocols respectively. It then handles 0101 * local files. Then it checks to see if the URL is valid and one that is 0102 * supported by KDE's IO system. If all the above checks fails, it simply 0103 * lookups the URL in the user-defined list and returns without filtering 0104 * if it is not found. TODO: the user-defined table is currently only manually 0105 * hackable and is missing a config dialog. 0106 */ 0107 0108 // QUrl url = data.uri(); 0109 QString cmd = data.typedString(); 0110 0111 int firstNonSlash = 0; 0112 while (firstNonSlash < cmd.length() && (cmd.at(firstNonSlash) == QLatin1Char('/'))) { 0113 firstNonSlash++; 0114 } 0115 if (firstNonSlash > 1) { 0116 cmd.remove(0, firstNonSlash - 1); 0117 } 0118 0119 // Replicate what KUrl(cmd) did in KDE4. This could later be folded into the checks further down... 0120 QUrl url; 0121 if (Utils::isAbsoluteLocalPath(cmd)) { 0122 url = QUrl::fromLocalFile(cmd); 0123 } else { 0124 url.setUrl(cmd); 0125 } 0126 0127 // WORKAROUND: Allow the use of '@' in the username component of a URL since 0128 // other browsers such as firefox in their infinite wisdom allow such blatant 0129 // violations of RFC 3986. BR# 69326/118413. 0130 if (cmd.count(QLatin1Char('@')) > 1) { 0131 const int lastIndex = cmd.lastIndexOf(QLatin1Char('@')); 0132 // Percent encode all but the last '@'. 0133 const auto suffix = QStringView(cmd).mid(lastIndex); 0134 cmd = QString::fromUtf8(QUrl::toPercentEncoding(cmd.left(lastIndex), QByteArrayLiteral(":/"))) + suffix; 0135 url.setUrl(cmd); 0136 } 0137 0138 const bool isMalformed = !url.isValid(); 0139 QString protocol = url.scheme(); 0140 0141 qCDebug(category) << cmd; 0142 0143 // Fix misparsing of "foo:80", QUrl thinks "foo" is the protocol and "80" is the path. 0144 // However, be careful not to do that for valid hostless URLs, e.g. file:///foo! 0145 if (!protocol.isEmpty() && url.host().isEmpty() && !url.path().isEmpty() && cmd.contains(QLatin1Char(':')) && !isKnownProtocol(protocol)) { 0146 protocol.clear(); 0147 } 0148 0149 qCDebug(category) << "url=" << url << "cmd=" << cmd << "isMalformed=" << isMalformed; 0150 0151 // TODO: Make this a bit more intelligent for Minicli! There 0152 // is no need to make comparisons if the supplied data is a local 0153 // executable and only the argument part, if any, changed! (Dawit) 0154 // You mean caching the last filtering, to try and reuse it, to save stat()s? (David) 0155 0156 const QLatin1String starthere_proto("start-here:"); 0157 if (cmd.startsWith(starthere_proto)) { 0158 setFilteredUri(data, QUrl(QStringLiteral("system:/"))); 0159 setUriType(data, KUriFilterData::LocalDir); 0160 return true; 0161 } 0162 0163 // Handle MAN & INFO pages shortcuts... 0164 const QLatin1String man_proto("man:"); 0165 const QLatin1String info_proto("info:"); 0166 if (cmd.startsWith(QLatin1Char('#')) || cmd.startsWith(man_proto) || cmd.startsWith(info_proto)) { 0167 QStringView sview(cmd); 0168 if (cmd.startsWith(QLatin1String("##"))) { 0169 cmd = QLatin1String("info:/") + sview.mid(2); 0170 } else if (cmd.startsWith(QLatin1Char('#'))) { 0171 cmd = QLatin1String("man:/") + sview.mid(1); 0172 } else if (cmd == info_proto || cmd == man_proto) { 0173 cmd += QLatin1Char('/'); 0174 } 0175 0176 setFilteredUri(data, QUrl(cmd)); 0177 setUriType(data, KUriFilterData::Help); 0178 return true; 0179 } 0180 0181 // Detect UNC style (aka windows SMB) URLs 0182 if (cmd.startsWith(QLatin1String("\\\\"))) { 0183 // make sure path is unix style 0184 cmd.replace(QLatin1Char('\\'), QLatin1Char('/')); 0185 cmd.prepend(QLatin1String("smb:")); 0186 setFilteredUri(data, QUrl(cmd)); 0187 setUriType(data, KUriFilterData::NetProtocol); 0188 return true; 0189 } 0190 0191 bool expanded = false; 0192 0193 // Expanding shortcut to HOME URL... 0194 QString path; 0195 QString ref; 0196 QString query; 0197 QString nameFilter; 0198 0199 if (!Utils::isAbsoluteLocalPath(cmd) && QUrl(cmd).isRelative()) { 0200 path = cmd; 0201 qCDebug(category) << "path=cmd=" << path; 0202 } else { 0203 if (url.isLocalFile()) { 0204 qCDebug(category) << "hasRef=" << url.hasFragment(); 0205 // Split path from ref/query 0206 // but not for "/tmp/a#b", if "a#b" is an existing file, 0207 // or for "/tmp/a?b" (#58990) 0208 if ((url.hasFragment() || !url.query().isEmpty()) && !url.path().endsWith(QLatin1Char('/'))) { // /tmp/?foo is a namefilter, not a query 0209 path = url.path(); 0210 ref = url.fragment(); 0211 qCDebug(category) << "isLocalFile set path to" << path << "and ref to" << ref; 0212 query = url.query(); 0213 if (path.isEmpty() && !url.host().isEmpty()) { 0214 path = QStringLiteral("/"); 0215 } 0216 } else { 0217 if (cmd.startsWith(QLatin1String("file://"))) { 0218 path = cmd.mid(strlen("file://")); 0219 } else { 0220 path = cmd; 0221 } 0222 qCDebug(category) << "(2) path=cmd=" << path; 0223 } 0224 } 0225 } 0226 0227 if (path.startsWith(QLatin1Char('~'))) { 0228 int slashPos = path.indexOf(QLatin1Char('/')); 0229 if (slashPos == -1) { 0230 slashPos = path.length(); 0231 } 0232 if (slashPos == 1) { // ~/ 0233 path.replace(0, 1, QDir::homePath()); 0234 } else { // ~username/ 0235 const QString userName(path.mid(1, slashPos - 1)); 0236 KUser user(userName); 0237 if (user.isValid() && !user.homeDir().isEmpty()) { 0238 path.replace(0, slashPos, user.homeDir()); 0239 } else { 0240 if (user.isValid()) { 0241 setErrorMsg(data, i18n("<qt><b>%1</b> does not have a home folder.</qt>", userName)); 0242 } else { 0243 setErrorMsg(data, i18n("<qt>There is no user called <b>%1</b>.</qt>", userName)); 0244 } 0245 setUriType(data, KUriFilterData::Error); 0246 // Always return true for error conditions so 0247 // that other filters will not be invoked !! 0248 return true; 0249 } 0250 } 0251 expanded = true; 0252 } else if (path.startsWith(QLatin1Char('$'))) { 0253 // Environment variable expansion. 0254 static const QRegularExpression envVarExp(QStringLiteral("\\$[a-zA-Z_][a-zA-Z0-9_]*")); 0255 const auto match = envVarExp.match(path); 0256 if (match.hasMatch()) { 0257 const QByteArray exp = qgetenv(QStringView(path).mid(1, match.capturedLength(0) - 1).toLocal8Bit().constData()); 0258 if (!exp.isEmpty()) { 0259 path.replace(0, match.capturedLength(0), QFile::decodeName(exp)); 0260 expanded = true; 0261 } 0262 } 0263 } 0264 0265 if (expanded || cmd.startsWith(QLatin1Char('/'))) { 0266 // Look for #ref again, after $ and ~ expansion (testcase: $QTDIR/doc/html/functions.html#s) 0267 // Can't use QUrl here, setPath would escape it... 0268 const int pos = path.indexOf(QLatin1Char('#')); 0269 if (pos > -1) { 0270 const QString newPath = path.left(pos); 0271 if (QFile::exists(newPath)) { 0272 ref = path.mid(pos + 1); 0273 path = newPath; 0274 qCDebug(category) << "Extracted ref: path=" << path << " ref=" << ref; 0275 } 0276 } 0277 } 0278 0279 bool isLocalFullPath = Utils::isAbsoluteLocalPath(path); 0280 0281 // Checking for local resource match... 0282 // Determine if "uri" is an absolute path to a local resource OR 0283 // A local resource with a supplied absolute path in KUriFilterData 0284 const QString abs_path = data.absolutePath(); 0285 0286 const bool canBeAbsolute = (protocol.isEmpty() && !abs_path.isEmpty()); 0287 const bool canBeLocalAbsolute = (canBeAbsolute && abs_path.startsWith(QLatin1Char('/')) && !isMalformed); 0288 bool exists = false; 0289 0290 /*qCDebug(category) << "abs_path=" << abs_path 0291 << "protocol=" << protocol 0292 << "canBeAbsolute=" << canBeAbsolute 0293 << "canBeLocalAbsolute=" << canBeLocalAbsolute 0294 << "isLocalFullPath=" << isLocalFullPath;*/ 0295 0296 QT_STATBUF buff; 0297 if (canBeLocalAbsolute) { 0298 QString abs = QDir::cleanPath(abs_path); 0299 // combine absolute path (abs_path) and relative path (cmd) into abs_path 0300 int len = path.length(); 0301 if ((len == 1 && path[0] == QLatin1Char('.')) || (len == 2 && path[0] == QLatin1Char('.') && path[1] == QLatin1Char('.'))) { 0302 path += QLatin1Char('/'); 0303 } 0304 qCDebug(category) << "adding " << abs << " and " << path; 0305 abs = QDir::cleanPath(abs + QLatin1Char('/') + path); 0306 qCDebug(category) << "checking whether " << abs << " exists."; 0307 // Check if it exists 0308 if (QT_STAT(QFile::encodeName(abs).constData(), &buff) == 0) { 0309 path = abs; // yes -> store as the new cmd 0310 exists = true; 0311 isLocalFullPath = true; 0312 } 0313 } 0314 0315 if (isLocalFullPath && !exists && !isMalformed) { 0316 exists = QT_STAT(QFile::encodeName(path).constData(), &buff) == 0; 0317 0318 if (!exists) { 0319 // Support for name filter (/foo/*.txt), see also KonqMainWindow::detectNameFilter 0320 // If the app using this filter doesn't support it, well, it'll simply error out itself 0321 int lastSlash = path.lastIndexOf(QLatin1Char('/')); 0322 if (lastSlash > -1 0323 && path.indexOf(QLatin1Char(' '), lastSlash) == -1) { // no space after last slash, otherwise it's more likely command-line arguments 0324 QString fileName = path.mid(lastSlash + 1); 0325 QString testPath = path.left(lastSlash); 0326 if ((fileName.indexOf(QLatin1Char('*')) != -1 || fileName.indexOf(QLatin1Char('[')) != -1 || fileName.indexOf(QLatin1Char('?')) != -1) 0327 && QT_STAT(QFile::encodeName(testPath).constData(), &buff) == 0) { 0328 nameFilter = fileName; 0329 qCDebug(category) << "Setting nameFilter to" << nameFilter << "and path to" << testPath; 0330 path = testPath; 0331 exists = true; 0332 } 0333 } 0334 } 0335 } 0336 0337 qCDebug(category) << "path =" << path << " isLocalFullPath=" << isLocalFullPath << " exists=" << exists << " url=" << url; 0338 if (exists) { 0339 QUrl u = QUrl::fromLocalFile(path); 0340 qCDebug(category) << "ref=" << ref << "query=" << query; 0341 u.setFragment(ref); 0342 u.setQuery(query); 0343 0344 if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("open"), QUrl(), u)) { 0345 // No authorization, we pretend it's a file will get 0346 // an access denied error later on. 0347 setFilteredUri(data, u); 0348 setUriType(data, KUriFilterData::LocalFile); 0349 return true; 0350 } 0351 0352 // Can be abs path to file or directory, or to executable with args 0353 bool isDir = Utils::isDirMask(buff.st_mode); 0354 if (!isDir && access(QFile::encodeName(path).data(), X_OK) == 0) { 0355 qCDebug(category) << "Abs path to EXECUTABLE"; 0356 setFilteredUri(data, u); 0357 setUriType(data, KUriFilterData::Executable); 0358 return true; 0359 } 0360 0361 // Open "uri" as file:/xxx if it is a non-executable local resource. 0362 if (isDir || Utils::isRegFileMask(buff.st_mode)) { 0363 qCDebug(category) << "Abs path as local file or directory"; 0364 if (!nameFilter.isEmpty()) { 0365 u.setPath(Utils::concatPaths(u.path(), nameFilter)); 0366 } 0367 setFilteredUri(data, u); 0368 setUriType(data, (isDir) ? KUriFilterData::LocalDir : KUriFilterData::LocalFile); 0369 return true; 0370 } 0371 0372 // Should we return LOCAL_FILE for non-regular files too? 0373 qCDebug(category) << "File found, but not a regular file nor dir... socket?"; 0374 } 0375 0376 if (data.checkForExecutables()) { 0377 // Let us deal with possible relative URLs to see 0378 // if it is executable under the user's $PATH variable. 0379 // We try hard to avoid parsing any possible command 0380 // line arguments or options that might have been supplied. 0381 QString exe = removeArgs(cmd); 0382 qCDebug(category) << "findExe with" << exe; 0383 0384 if (!QStandardPaths::findExecutable(exe).isNull()) { 0385 qCDebug(category) << "EXECUTABLE exe=" << exe; 0386 setFilteredUri(data, QUrl::fromLocalFile(exe)); 0387 // check if we have command line arguments 0388 if (exe != cmd) { 0389 setArguments(data, cmd.right(cmd.length() - exe.length())); 0390 } 0391 setUriType(data, KUriFilterData::Executable); 0392 return true; 0393 } 0394 } 0395 0396 // Process URLs of known and supported protocols so we don't have 0397 // to resort to the pattern matching scheme below which can possibly 0398 // slow things down... 0399 if (!isMalformed && !isLocalFullPath && !protocol.isEmpty()) { 0400 qCDebug(category) << "looking for protocol" << protocol; 0401 if (isKnownProtocol(protocol)) { 0402 setFilteredUri(data, url); 0403 if (protocol == QLatin1String("man") || protocol == QLatin1String("help")) { 0404 setUriType(data, KUriFilterData::Help); 0405 } else { 0406 setUriType(data, KUriFilterData::NetProtocol); 0407 } 0408 return true; 0409 } 0410 } 0411 0412 // Short url matches 0413 if (!cmd.contains(QLatin1Char(' '))) { 0414 // Okay this is the code that allows users to supply custom matches for specific 0415 // URLs using Qt's QRegularExpression class. 0416 for (const URLHint &hint : std::as_const(m_urlHints)) { 0417 qCDebug(category) << "testing regexp for" << hint.prepend; 0418 if (hint.hintRe.match(cmd).capturedStart() == 0) { 0419 const QString cmdStr = hint.prepend + cmd; 0420 QUrl cmdUrl(cmdStr); 0421 qCDebug(category) << "match - prepending" << hint.prepend << "->" << cmdStr << "->" << cmdUrl; 0422 setFilteredUri(data, cmdUrl); 0423 setUriType(data, hint.type); 0424 return true; 0425 } 0426 } 0427 0428 // No protocol and not malformed means a valid short URL such as kde.org or 0429 // user@192.168.0.1. However, it might also be valid only because it lacks 0430 // the scheme component, e.g. www.kde,org (illegal ',' before 'org'). The 0431 // check below properly deciphers the difference between the two and sends 0432 // back the proper result. 0433 if (protocol.isEmpty() && isPotentialShortURL(cmd)) { 0434 QString urlStr = data.defaultUrlScheme(); 0435 if (urlStr.isEmpty()) { 0436 urlStr = m_strDefaultUrlScheme; 0437 } 0438 0439 const int index = urlStr.indexOf(QLatin1Char(':')); 0440 if (index == -1 || !isKnownProtocol(urlStr.left(index))) { 0441 urlStr += QStringLiteral("://"); 0442 } 0443 urlStr += cmd; 0444 0445 QUrl fixedUrl(urlStr); 0446 if (fixedUrl.isValid()) { 0447 setFilteredUri(data, fixedUrl); 0448 setUriType(data, KUriFilterData::NetProtocol); 0449 } else if (isKnownProtocol(fixedUrl.scheme())) { 0450 setFilteredUri(data, data.uri()); 0451 setUriType(data, KUriFilterData::Error); 0452 } 0453 return true; 0454 } 0455 } 0456 0457 // If we previously determined that the URL might be a file, 0458 // and if it doesn't exist... we'll pretend it exists. 0459 // This allows to use it for completion purposes. 0460 // (If you change this logic again, look at the commit that was testing 0461 // for KUrlAuthorized::authorizeUrlAction("open")) 0462 if (isLocalFullPath && !exists) { 0463 QUrl u = QUrl::fromLocalFile(path); 0464 u.setFragment(ref); 0465 setFilteredUri(data, u); 0466 setUriType(data, KUriFilterData::LocalFile); 0467 return true; 0468 } 0469 0470 // If we reach this point, we cannot filter this thing so simply return false 0471 // so that other filters, if present, can take a crack at it. 0472 return false; 0473 } 0474 0475 void KShortUriFilter::configure() 0476 { 0477 KConfig config(objectName() + QStringLiteral("rc"), KConfig::NoGlobals); 0478 KConfigGroup cg(config.group(QString())); 0479 0480 m_strDefaultUrlScheme = cg.readEntry("DefaultProtocol", QStringLiteral("https://")); 0481 const QMap<QString, QString> patterns = config.entryMap(QStringLiteral("Pattern")); 0482 const QMap<QString, QString> protocols = config.entryMap(QStringLiteral("Protocol")); 0483 KConfigGroup typeGroup(&config, QStringLiteral("Type")); 0484 0485 for (auto it = patterns.begin(); it != patterns.end(); ++it) { 0486 QString protocol = protocols[it.key()]; 0487 if (!protocol.isEmpty()) { 0488 int type = typeGroup.readEntry(it.key(), -1); 0489 if (type > -1 && type <= KUriFilterData::Unknown) { 0490 m_urlHints.append(URLHint(it.value(), protocol, static_cast<KUriFilterData::UriTypes>(type))); 0491 } else { 0492 m_urlHints.append(URLHint(it.value(), protocol)); 0493 } 0494 } 0495 } 0496 } 0497 0498 K_PLUGIN_CLASS_WITH_JSON(KShortUriFilter, "kshorturifilter.json") 0499 0500 #include "kshorturifilter.moc" 0501 0502 #include "moc_kshorturifilter.cpp"