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