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"