File indexing completed on 2024-05-12 11:54:37

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"