Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/browser/BrowserService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1542,7 +1542,7 @@ bool BrowserService::handleURL(const QString& entryUrl,
}

// Match the base domain
if (urlTools()->getBaseDomainFromUrl(siteQUrl.host()) != urlTools()->getBaseDomainFromUrl(entryQUrl.host())) {
if (UrlTools::getBaseDomainFromUrl(siteQUrl.host()) != UrlTools::getBaseDomainFromUrl(entryQUrl.host())) {
return false;
}

Expand Down
6 changes: 3 additions & 3 deletions src/browser/PasskeyUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,11 @@ bool PasskeyUtils::isRegistrableDomainSuffix(const QString& hostSuffixString, co
return false;
}

if (hostSuffix == urlTools()->getTopLevelDomainFromUrl(hostSuffix)) {
if (hostSuffix == UrlTools::getTopLevelDomainFromUrl(hostSuffix)) {
return false;
}

const auto originalPublicSuffix = urlTools()->getTopLevelDomainFromUrl(originalHost);
const auto originalPublicSuffix = UrlTools::getTopLevelDomainFromUrl(originalHost);
if (originalPublicSuffix.isEmpty()) {
return false;
}
Expand All @@ -256,7 +256,7 @@ bool PasskeyUtils::isDomain(const QString& hostName) const
{
const auto domain = QUrl::fromUserInput(hostName).host();
return !domain.isEmpty() && !domain.endsWith('.') && Tools::isAsciiString(domain)
&& !urlTools()->domainHasIllegalCharacters(domain) && !urlTools()->isIpAddress(hostName);
&& !UrlTools::domainHasIllegalCharacters(domain) && !UrlTools::isIpAddress(hostName);
}

bool PasskeyUtils::isUserVerificationValid(const QString& userVerification) const
Expand Down
4 changes: 2 additions & 2 deletions src/gui/IconDownloader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ void IconDownloader::setUrl(const QString& entryUrl)
// Determine the second-level domain, if available
QString secondLevelDomain;
if (!hostIsIp) {
secondLevelDomain = urlTools()->getBaseDomainFromUrl(url.toString());
secondLevelDomain = UrlTools::getBaseDomainFromUrl(url.toString());
}

// Start with the "fallback" url (if enabled) to try to get the best favicon
Expand Down Expand Up @@ -172,7 +172,7 @@ void IconDownloader::fetchFinished()
QString url = m_url;

bool error = (m_reply->error() != QNetworkReply::NoError);
QUrl redirectTarget = urlTools()->getRedirectTarget(m_reply);
QUrl redirectTarget = UrlTools::getRedirectTarget(m_reply);

m_reply->deleteLater();
m_reply = nullptr;
Expand Down
2 changes: 1 addition & 1 deletion src/gui/URLEdit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ void URLEdit::updateStylesheet(const QString& url)
const QString stylesheetTemplate("QLineEdit { background: %1; }");
const auto resolvedUrl = m_entry ? m_entry->resolveMultiplePlaceholders(url) : url;

if (!urlTools()->isUrlValid(resolvedUrl)) {
if (!UrlTools::isUrlValid(resolvedUrl)) {
const StateColorPalette statePalette;
const auto color = statePalette.color(StateColorPalette::ColorRole::Error);
setStyleSheet(stylesheetTemplate.arg(color.name()));
Expand Down
72 changes: 33 additions & 39 deletions src/gui/UrlTools.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,43 +20,34 @@
#include <QHostAddress>
#include <QNetworkCookie>
#include <QNetworkCookieJar>
#include <QNetworkReply>
#include <QVariant>
#endif
#include <QRegularExpression>
#include <QUrl>

const QString UrlTools::URL_WILDCARD = "1kpxcwc1";

Q_GLOBAL_STATIC(UrlTools, s_urlTools)
#include <utility>

UrlTools* UrlTools::instance()
{
return s_urlTools;
}
const QString UrlTools::URL_WILDCARD = "1kpxcwc1";

QUrl UrlTools::convertVariantToUrl(const QVariant& var) const
#if defined(KPXC_FEATURE_NETWORK) || defined(KPXC_FEATURE_BROWSER)
QUrl UrlTools::getRedirectTarget(QNetworkReply* reply)
{
QUrl url;
QVariant var = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
if (var.canConvert<QUrl>()) {
url = var.toUrl();
}
return url;
}

#if defined(KPXC_FEATURE_NETWORK) || defined(KPXC_FEATURE_BROWSER)
QUrl UrlTools::getRedirectTarget(QNetworkReply* reply) const
{
QVariant var = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
QUrl url = convertVariantToUrl(var);
return url;
}

/**
* Gets the base domain of URL or hostname.
*
* Returns the base domain, e.g. https://another.example.co.uk -> example.co.uk
* Up-to-date list can be found: https://publicsuffix.org/list/public_suffix_list.dat
*/
QString UrlTools::getBaseDomainFromUrl(const QString& url) const
QString UrlTools::getBaseDomainFromUrl(const QString& url)
{
auto qUrl = QUrl::fromUserInput(url);

Expand Down Expand Up @@ -85,7 +76,7 @@ QString UrlTools::getBaseDomainFromUrl(const QString& url) const
*
* Returns the TLD e.g. https://another.example.co.uk -> co.uk
*/
QString UrlTools::getTopLevelDomainFromUrl(const QString& url) const
QString UrlTools::getTopLevelDomainFromUrl(const QString& url)
{
auto host = QUrl::fromUserInput(url).host();
if (isIpAddress(host)) {
Expand All @@ -112,7 +103,7 @@ QString UrlTools::getTopLevelDomainFromUrl(const QString& url) const
return host;
}

bool UrlTools::isIpAddress(const QString& host) const
bool UrlTools::isIpAddress(const QString& host)
{
// Handle IPv6 host with brackets, e.g [::1]
const auto hostAddress = host.startsWith('[') && host.endsWith(']') ? host.mid(1, host.length() - 2) : host;
Expand All @@ -121,35 +112,38 @@ bool UrlTools::isIpAddress(const QString& host) const
}
#endif

// Returns true if URLs are identical. Paths with "/" are removed during comparison.
// URLs without scheme reverts to https.
// Special handling is needed because QUrl::matches() with QUrl::StripTrailingSlash does not strip "/" paths.
bool UrlTools::isUrlIdentical(const QString& first, const QString& second) const
namespace
{
auto trimUrl = [](QString url) {
url = url.trimmed();
if (url.endsWith("/")) {
url.remove(url.length() - 1, 1);
QString trimUrl(QString& url)
Comment thread
varjolintu marked this conversation as resolved.
{
url = url.trimmed().replace("*", UrlTools::URL_WILDCARD);
if (url.endsWith('/')) {
url.chop(1); // Removes the last character
}

return url;
};
}
} // namespace

// Returns true if URLs are identical. Paths with "/" are removed during comparison.
// URLs without scheme reverts to https.
// Special handling is needed because QUrl::matches() with QUrl::StripTrailingSlash does not strip "/" paths.
bool UrlTools::isUrlIdentical(QString first, QString second)
{
if (first.isEmpty() || second.isEmpty()) {
return false;
}

// Replace URL wildcards for comparison if found
const auto firstUrl = trimUrl(QString(first).replace("*", UrlTools::URL_WILDCARD));
const auto secondUrl = trimUrl(QString(second).replace("*", UrlTools::URL_WILDCARD));
const auto firstUrl = trimUrl(first);
const auto secondUrl = trimUrl(second);
if (firstUrl == secondUrl) {
return true;
}

return QUrl(firstUrl).matches(QUrl(secondUrl), QUrl::StripTrailingSlash);
}

bool UrlTools::isUrlValid(const QString& urlField, bool looseComparison) const
bool UrlTools::isUrlValid(const QString& urlField, bool looseComparison)
{
if (urlField.isEmpty() || urlField.startsWith("cmd://", Qt::CaseInsensitive)
|| urlField.startsWith("kdbx://", Qt::CaseInsensitive) || urlField.startsWith("{REF:A", Qt::CaseInsensitive)) {
Expand All @@ -169,14 +163,14 @@ bool UrlTools::isUrlValid(const QString& urlField, bool looseComparison) const

// Get the URL inside ""
url.remove(0, 1);
url.remove(url.length() - 1, 1);
url.chop(1);
} else {
// Do not allow URL with just wildcards, or double wildcards
if (url.length() == url.count("*") || url.contains("**") || url.contains("*.*")) {
return false;
}

url.replace("*", UrlTools::URL_WILDCARD);
url.replace("*", URL_WILDCARD);
}
}

Expand All @@ -193,16 +187,16 @@ bool UrlTools::isUrlValid(const QString& urlField, bool looseComparison) const

#if defined(KPXC_FEATURE_NETWORK) || defined(KPXC_FEATURE_BROWSER)
// Prevent TLD wildcards
if (looseComparison && url.contains(UrlTools::URL_WILDCARD)) {
if (looseComparison && url.contains(URL_WILDCARD)) {
const auto tld = getTopLevelDomainFromUrl(url);
if (tld.contains(UrlTools::URL_WILDCARD) || qUrl.host() == QString("%1.%2").arg(UrlTools::URL_WILDCARD, tld)) {
if (tld.contains(URL_WILDCARD) || qUrl.host() == QString("%1.%2").arg(URL_WILDCARD, tld)) {
return false;
}
}
#endif

// Check for illegal characters. Adds also the wildcard * to the list
QRegularExpression re("[<>\\^`{|}\\*]");
static const QRegularExpression re("[<>\\^`{|}\\*]");
auto match = re.match(url);
if (match.hasMatch()) {
return false;
Expand All @@ -211,8 +205,8 @@ bool UrlTools::isUrlValid(const QString& urlField, bool looseComparison) const
return true;
}

bool UrlTools::domainHasIllegalCharacters(const QString& domain) const
bool UrlTools::domainHasIllegalCharacters(const QString& domain)
{
QRegularExpression re(R"([\s\^#|/:<>\?@\[\]\\])");
static const QRegularExpression re(R"([\s\^#|/:<>\?@\[\]\\])");
return re.match(domain).hasMatch();
}
44 changes: 13 additions & 31 deletions src/gui/UrlTools.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,43 +19,25 @@
#define KEEPASSXC_URLTOOLS_H

#include "config-keepassx.h"
#include <QObject>
#include <QUrl>
#include <QVariant>
#include <QString>
#if defined(KPXC_FEATURE_NETWORK) || defined(KPXC_FEATURE_BROWSER)
#include <QNetworkReply>
#include <QUrl>
class QNetworkReply;
#endif

class UrlTools : public QObject
namespace UrlTools
{
Q_OBJECT

public:
explicit UrlTools() = default;
static UrlTools* instance();

#if defined(KPXC_FEATURE_NETWORK) || defined(KPXC_FEATURE_BROWSER)
QUrl getRedirectTarget(QNetworkReply* reply) const;
QString getBaseDomainFromUrl(const QString& url) const;
QString getTopLevelDomainFromUrl(const QString& url) const;
bool isIpAddress(const QString& host) const;
QUrl getRedirectTarget(QNetworkReply* reply);
QString getBaseDomainFromUrl(const QString& url);
QString getTopLevelDomainFromUrl(const QString& url);
bool isIpAddress(const QString& host);
#endif
bool isUrlIdentical(const QString& first, const QString& second) const;
bool isUrlValid(const QString& urlField, bool looseComparison = false) const;
bool domainHasIllegalCharacters(const QString& domain) const;
bool isUrlIdentical(QString first, QString second);
bool isUrlValid(const QString& urlField, bool looseComparison = false);
bool domainHasIllegalCharacters(const QString& domain);

static const QString URL_WILDCARD;

private:
QUrl convertVariantToUrl(const QVariant& var) const;

private:
Q_DISABLE_COPY(UrlTools);
};

static inline UrlTools* urlTools()
{
return UrlTools::instance();
}
extern const QString URL_WILDCARD;
} // namespace UrlTools

#endif // KEEPASSXC_URLTOOLS_H
4 changes: 2 additions & 2 deletions src/gui/entry/EntryURLModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,14 @@ QVariant EntryURLModel::data(const QModelIndex& index, int role) const
}

const auto value = m_entryAttributes->value(key);
const auto urlValid = urlTools()->isUrlValid(value, true);
const auto urlValid = UrlTools::isUrlValid(value, true);

// Check for duplicate URLs in the attribute list. Excludes the current key/value from the comparison.
auto customAttributeKeys = m_entryAttributes->customKeys().filter(EntryAttributes::AdditionalUrlAttribute);
customAttributeKeys.removeOne(key);

const auto duplicateUrl =
m_entryAttributes->values(customAttributeKeys).contains(value) || urlTools()->isUrlIdentical(value, m_entryUrl);
m_entryAttributes->values(customAttributeKeys).contains(value) || UrlTools::isUrlIdentical(value, m_entryUrl);
if (role == Qt::BackgroundRole && (!urlValid || duplicateUrl)) {
StateColorPalette statePalette;
return statePalette.color(StateColorPalette::ColorRole::Error);
Expand Down
Loading
Loading