diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index b8f34456c..90b8de4d8 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -3088,6 +3088,14 @@ Would you like to correct it? Do not use HTTP Auth toggle for this and sub groups + + Omit WWW subdomain from matching: + + + + Omit WWW subdomain from matching toggle for this and sub groups + + EditGroupWidgetKeeShare diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index 82bb552ad..7e0a8d6b1 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -56,6 +56,7 @@ const QString BrowserService::OPTION_SKIP_AUTO_SUBMIT = QStringLiteral("BrowserS const QString BrowserService::OPTION_HIDE_ENTRY = QStringLiteral("BrowserHideEntry"); const QString BrowserService::OPTION_ONLY_HTTP_AUTH = QStringLiteral("BrowserOnlyHttpAuth"); const QString BrowserService::OPTION_NOT_HTTP_AUTH = QStringLiteral("BrowserNotHttpAuth"); +const QString BrowserService::OPTION_OMIT_WWW = QStringLiteral("BrowserOmitWww"); // Multiple URL's const QString BrowserService::ADDITIONAL_URL = QStringLiteral("KP2A_URL"); @@ -426,8 +427,8 @@ QString BrowserService::getKey(const QString& id) } QJsonArray BrowserService::findMatchingEntries(const QString& dbid, - const QString& siteUrlStr, - const QString& formUrlStr, + const QString& siteUrl, + const QString& formUrl, const QString& realm, const StringPairList& keyList, const bool httpAuth) @@ -435,13 +436,13 @@ QJsonArray BrowserService::findMatchingEntries(const QString& dbid, Q_UNUSED(dbid); const bool alwaysAllowAccess = browserSettings()->alwaysAllowAccess(); const bool ignoreHttpAuth = browserSettings()->httpAuthPermission(); - const QString siteHost = QUrl(siteUrlStr).host(); - const QString formHost = QUrl(formUrlStr).host(); + const QString siteHost = QUrl(siteUrl).host(); + const QString formHost = QUrl(formUrl).host(); // Check entries for authorization QList pwEntriesToConfirm; QList pwEntries; - for (auto* entry : searchEntries(siteUrlStr, formUrlStr, keyList)) { + for (auto* entry : searchEntries(siteUrl, formUrl, keyList)) { auto entryCustomData = entry->customData(); if (!httpAuth @@ -484,7 +485,7 @@ QJsonArray BrowserService::findMatchingEntries(const QString& dbid, // Confirm entries QList selectedEntriesToConfirm = - confirmEntries(pwEntriesToConfirm, siteUrlStr, siteHost, formHost, realm, httpAuth); + confirmEntries(pwEntriesToConfirm, siteUrl, siteHost, formHost, realm, httpAuth); if (!selectedEntriesToConfirm.isEmpty()) { pwEntries.append(selectedEntriesToConfirm); } @@ -499,7 +500,7 @@ QJsonArray BrowserService::findMatchingEntries(const QString& dbid, } // Sort results - pwEntries = sortEntries(pwEntries, siteUrlStr, formUrlStr); + pwEntries = sortEntries(pwEntries, siteUrl, formUrl); // Fill the list QJsonArray result; @@ -513,8 +514,8 @@ QJsonArray BrowserService::findMatchingEntries(const QString& dbid, void BrowserService::addEntry(const QString& dbid, const QString& login, const QString& password, - const QString& siteUrlStr, - const QString& formUrlStr, + const QString& siteUrl, + const QString& formUrl, const QString& realm, const QString& group, const QString& groupUuid, @@ -530,8 +531,8 @@ void BrowserService::addEntry(const QString& dbid, auto* entry = new Entry(); entry->setUuid(QUuid::createUuid()); - entry->setTitle(QUrl(siteUrlStr).host()); - entry->setUrl(siteUrlStr); + entry->setTitle(QUrl(siteUrl).host()); + entry->setUrl(siteUrl); entry->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON); entry->setUsername(login); entry->setPassword(password); @@ -550,8 +551,8 @@ void BrowserService::addEntry(const QString& dbid, entry->setGroup(getDefaultEntryGroup(db)); } - const QString host = QUrl(siteUrlStr).host(); - const QString submitHost = QUrl(formUrlStr).host(); + const QString host = QUrl(siteUrl).host(); + const QString submitHost = QUrl(formUrl).host(); BrowserEntryConfig config; config.allow(host); @@ -572,8 +573,8 @@ bool BrowserService::updateEntry(const QString& dbid, const QString& uuid, const QString& login, const QString& password, - const QString& siteUrlStr, - const QString& formUrlStr) + const QString& siteUrl, + const QString& formUrl) { // TODO: select database based on this key id Q_UNUSED(dbid); @@ -585,7 +586,7 @@ bool BrowserService::updateEntry(const QString& dbid, Entry* entry = db->rootGroup()->findEntryByUuid(Tools::hexToUuid(uuid)); if (!entry) { // If entry is not found for update, add a new one to the selected database - addEntry(dbid, login, password, siteUrlStr, formUrlStr, "", "", "", db); + addEntry(dbid, login, password, siteUrl, formUrl, "", "", "", db); return true; } @@ -614,7 +615,7 @@ bool BrowserService::updateEntry(const QString& dbid, dialogResult = MessageBox::question( nullptr, tr("KeePassXC: Update Entry"), - tr("Do you want to update the information in %1 - %2?").arg(QUrl(siteUrlStr).host(), username), + tr("Do you want to update the information in %1 - %2?").arg(QUrl(siteUrl).host(), username), MessageBox::Save | MessageBox::Cancel, MessageBox::Cancel, MessageBox::Raise); @@ -663,7 +664,7 @@ bool BrowserService::deleteEntry(const QString& uuid) } QList -BrowserService::searchEntries(const QSharedPointer& db, const QString& siteUrlStr, const QString& formUrlStr) +BrowserService::searchEntries(const QSharedPointer& db, const QString& siteUrl, const QString& formUrl) { QList entries; auto* rootGroup = db->rootGroup(); @@ -677,6 +678,9 @@ BrowserService::searchEntries(const QSharedPointer& db, const QString& continue; } + const auto omitWwwSubdomain = + group->resolveCustomDataTriState(BrowserService::OPTION_OMIT_WWW) == Group::Enable; + for (auto* entry : group->entries()) { if (entry->isRecycled() || (entry->customData()->contains(BrowserService::OPTION_HIDE_ENTRY) @@ -684,16 +688,7 @@ BrowserService::searchEntries(const QSharedPointer& db, const QString& continue; } - // Search for additional URL's starting with KP2A_URL - for (const auto& key : entry->attributes()->keys()) { - if (key.startsWith(ADDITIONAL_URL) && handleURL(entry->attributes()->value(key), siteUrlStr, formUrlStr) - && !entries.contains(entry)) { - entries.append(entry); - continue; - } - } - - if (!handleEntry(entry, siteUrlStr, formUrlStr)) { + if (!shouldIncludeEntry(entry, siteUrl, formUrl, omitWwwSubdomain)) { continue; } @@ -708,7 +703,7 @@ BrowserService::searchEntries(const QSharedPointer& db, const QString& } QList -BrowserService::searchEntries(const QString& siteUrlStr, const QString& formUrlStr, const StringPairList& keyList) +BrowserService::searchEntries(const QString& siteUrl, const QString& formUrl, const StringPairList& keyList) { // Check if database is connected with KeePassXC-Browser auto databaseConnected = [&](const QSharedPointer& db) { @@ -738,11 +733,11 @@ BrowserService::searchEntries(const QString& siteUrlStr, const QString& formUrlS } // Search entries matching the hostname - QString hostname = QUrl(siteUrlStr).host(); + QString hostname = QUrl(siteUrl).host(); QList entries; do { for (const auto& db : databases) { - entries << searchEntries(db, siteUrlStr, formUrlStr); + entries << searchEntries(db, siteUrl, formUrl); } } while (entries.isEmpty() && removeFirstDomain(hostname)); @@ -853,9 +848,9 @@ BrowserService::sortEntries(QList& pwEntries, const QString& siteUrlStr, } QList BrowserService::confirmEntries(QList& pwEntriesToConfirm, - const QString& siteUrlStr, + const QString& siteUrl, const QString& siteHost, - const QString& formUrlStr, + const QString& formUrl, const QString& realm, const bool httpAuth) { @@ -874,8 +869,8 @@ QList BrowserService::confirmEntries(QList& pwEntriesToConfirm, BrowserEntryConfig config; config.load(entry); config.deny(siteHost); - if (!formUrlStr.isEmpty() && siteHost != formUrlStr) { - config.deny(formUrlStr); + if (!formUrl.isEmpty() && siteHost != formUrl) { + config.deny(formUrl); } if (!realm.isEmpty()) { config.setRealm(realm); @@ -883,7 +878,7 @@ QList BrowserService::confirmEntries(QList& pwEntriesToConfirm, config.save(entry); }); - accessControlDialog.setItems(pwEntriesToConfirm, siteUrlStr, httpAuth); + accessControlDialog.setItems(pwEntriesToConfirm, siteUrl, httpAuth); QList allowedEntries; if (accessControlDialog.exec() == QDialog::Accepted) { @@ -894,8 +889,8 @@ QList BrowserService::confirmEntries(QList& pwEntriesToConfirm, BrowserEntryConfig config; config.load(entry); config.allow(siteHost); - if (!formUrlStr.isEmpty() && siteHost != formUrlStr) { - config.allow(formUrlStr); + if (!formUrl.isEmpty() && siteHost != formUrl) { + config.allow(formUrl); } if (!realm.isEmpty()) { config.setRealm(realm); @@ -1006,14 +1001,14 @@ Group* BrowserService::getDefaultEntryGroup(const QSharedPointer& sele // Returns the maximum sort priority given a set of match urls and the // extension provided site and form url. -int BrowserService::sortPriority(const QStringList& urls, const QString& siteUrlStr, const QString& formUrlStr) +int BrowserService::sortPriority(const QStringList& urls, const QString& siteUrl, const QString& formUrl) { QList priorityList; // NOTE: QUrl::matches is utterly broken in Qt < 5.11, so we work around that // by removing parts of the url that we don't match and direct matching others const auto stdOpts = QUrl::RemoveFragment | QUrl::RemoveUserInfo; - const auto siteUrl = QUrl(siteUrlStr).adjusted(stdOpts); - const auto formUrl = QUrl(formUrlStr).adjusted(stdOpts); + const auto adjustedSiteUrl = QUrl(siteUrl).adjusted(stdOpts); + const auto adjustedFormUrl = QUrl(formUrl).adjusted(stdOpts); auto getPriority = [&](const QString& givenUrl) { auto url = QUrl::fromUserInput(givenUrl).adjusted(stdOpts); @@ -1031,38 +1026,38 @@ int BrowserService::sortPriority(const QStringList& urls, const QString& siteUrl // Reject invalid urls and hosts, except 'localhost', and scheme mismatch if (!url.isValid() || (!url.host().contains(".") && url.host() != "localhost") - || url.scheme() != siteUrl.scheme()) { + || url.scheme() != adjustedSiteUrl.scheme()) { return 0; } // Exact match with site url or form url - if (url.matches(siteUrl, QUrl::None) || url.matches(formUrl, QUrl::None)) { + if (url.matches(adjustedSiteUrl, QUrl::None) || url.matches(adjustedFormUrl, QUrl::None)) { return 100; } // Exact match without the query string - if (url.matches(siteUrl, QUrl::RemoveQuery) || url.matches(formUrl, QUrl::RemoveQuery)) { + if (url.matches(adjustedSiteUrl, QUrl::RemoveQuery) || url.matches(adjustedFormUrl, QUrl::RemoveQuery)) { return 90; } // Parent directory match - if (url.isParentOf(siteUrl) || url.isParentOf(formUrl)) { + if (url.isParentOf(adjustedSiteUrl) || url.isParentOf(adjustedFormUrl)) { return 85; } // Match without path (ie, FQDN match), form url prioritizes lower than site url - if (url.host() == siteUrl.host()) { + if (url.host() == adjustedSiteUrl.host()) { return 80; } - if (url.host() == formUrl.host()) { + if (url.host() == adjustedFormUrl.host()) { return 70; } // Site/form url ends with given url (subdomain mismatch) - if (siteUrl.host().endsWith(url.host())) { + if (adjustedSiteUrl.host().endsWith(url.host())) { return 60; } - if (formUrl.host().endsWith(url.host())) { + if (adjustedFormUrl.host().endsWith(url.host())) { return 50; } @@ -1108,7 +1103,10 @@ bool BrowserService::removeFirstDomain(QString& hostname) /* Test if a search URL matches a custom entry. If the URL has the schema "keepassxc", some special checks will be made. * Otherwise, this simply delegates to handleURL(). */ -bool BrowserService::handleEntry(Entry* entry, const QString& url, const QString& submitUrl) +bool BrowserService::shouldIncludeEntry(Entry* entry, + const QString& url, + const QString& submitUrl, + const bool omitWwwSubdomain) { // Use this special scheme to find entries by UUID if (url.startsWith("keepassxc://by-uuid/")) { @@ -1116,10 +1114,21 @@ bool BrowserService::handleEntry(Entry* entry, const QString& url, const QString } else if (url.startsWith("keepassxc://by-path/")) { return url.endsWith("by-path/" + entry->path()); } - return handleURL(entry->url(), url, submitUrl); + + const auto allEntryUrls = entry->getAllUrls(); + for (const auto& entryUrl : allEntryUrls) { + if (handleURL(entryUrl, url, submitUrl, omitWwwSubdomain)) { + return true; + } + } + + return false; } -bool BrowserService::handleURL(const QString& entryUrl, const QString& siteUrlStr, const QString& formUrlStr) +bool BrowserService::handleURL(const QString& entryUrl, + const QString& siteUrl, + const QString& formUrl, + const bool omitWwwSubdomain) { if (entryUrl.isEmpty()) { return false; @@ -1136,9 +1145,14 @@ bool BrowserService::handleURL(const QString& entryUrl, const QString& siteUrlSt } } + // Remove WWW subdomain from matching if group setting is enabled + if (omitWwwSubdomain && entryQUrl.host().startsWith("www.")) { + entryQUrl.setHost(entryQUrl.host().remove("www.")); + } + // Make a direct compare if a local file is used - if (siteUrlStr.startsWith("file://")) { - return entryUrl == formUrlStr; + if (siteUrl.startsWith("file://")) { + return entryUrl == formUrl; } // URL host validation fails @@ -1147,7 +1161,7 @@ bool BrowserService::handleURL(const QString& entryUrl, const QString& siteUrlSt } // Match port, if used - QUrl siteQUrl(siteUrlStr); + QUrl siteQUrl(siteUrl); if (entryQUrl.port() > 0 && entryQUrl.port() != siteQUrl.port()) { return false; } diff --git a/src/browser/BrowserService.h b/src/browser/BrowserService.h index f91887cd0..1ab3342fb 100644 --- a/src/browser/BrowserService.h +++ b/src/browser/BrowserService.h @@ -99,6 +99,7 @@ public: static const QString OPTION_HIDE_ENTRY; static const QString OPTION_ONLY_HTTP_AUTH; static const QString OPTION_NOT_HTTP_AUTH; + static const QString OPTION_OMIT_WWW; static const QString ADDITIONAL_URL; signals: @@ -128,26 +129,29 @@ private: Hidden }; - QList - searchEntries(const QSharedPointer& db, const QString& siteUrlStr, const QString& formUrlStr); - QList searchEntries(const QString& siteUrlStr, const QString& formUrlStr, const StringPairList& keyList); - QList sortEntries(QList& pwEntries, const QString& siteUrlStr, const QString& formUrlStr); + QList searchEntries(const QSharedPointer& db, const QString& siteUrl, const QString& formUrl); + QList searchEntries(const QString& siteUrl, const QString& formUrl, const StringPairList& keyList); + QList sortEntries(QList& pwEntries, const QString& siteUrl, const QString& formUrl); QList confirmEntries(QList& pwEntriesToConfirm, - const QString& siteUrlStr, + const QString& siteUrl, const QString& siteHost, - const QString& formUrlStr, + const QString& formUrl, const QString& realm, const bool httpAuth); QJsonObject prepareEntry(const Entry* entry); QJsonArray getChildrenFromGroup(Group* group); Access checkAccess(const Entry* entry, const QString& siteHost, const QString& formHost, const QString& realm); Group* getDefaultEntryGroup(const QSharedPointer& selectedDb = {}); - int sortPriority(const QStringList& urls, const QString& siteUrlStr, const QString& formUrlStr); + int sortPriority(const QStringList& urls, const QString& siteUrl, const QString& formUrl); bool schemeFound(const QString& url); bool isIpAddress(const QString& host) const; bool removeFirstDomain(QString& hostname); - bool handleEntry(Entry* entry, const QString& url, const QString& submitUrl); - bool handleURL(const QString& entryUrl, const QString& siteUrlStr, const QString& formUrlStr); + bool + shouldIncludeEntry(Entry* entry, const QString& url, const QString& submitUrl, const bool omitWwwSubdomain = false); + bool handleURL(const QString& entryUrl, + const QString& siteUrl, + const QString& formUrl, + const bool omitWwwSubdomain = false); QString getTopLevelDomainFromUrl(const QString& url) const; QString baseDomain(const QString& hostname) const; QSharedPointer getDatabase(); diff --git a/src/gui/group/EditGroupWidget.cpp b/src/gui/group/EditGroupWidget.cpp index 687dd056d..fc6f80624 100644 --- a/src/gui/group/EditGroupWidget.cpp +++ b/src/gui/group/EditGroupWidget.cpp @@ -219,6 +219,7 @@ void EditGroupWidget::loadGroup(Group* group, bool create, const QSharedPointer< auto inheritSkipSubmit = false; auto inheritOnlyHttp = false; auto inheritNoHttp = false; + auto inheritOmitWww = false; auto parent = group->parentGroup(); if (parent) { @@ -226,12 +227,14 @@ void EditGroupWidget::loadGroup(Group* group, bool create, const QSharedPointer< inheritSkipSubmit = parent->resolveCustomDataTriState(BrowserService::OPTION_SKIP_AUTO_SUBMIT); inheritOnlyHttp = parent->resolveCustomDataTriState(BrowserService::OPTION_ONLY_HTTP_AUTH); inheritNoHttp = parent->resolveCustomDataTriState(BrowserService::OPTION_NOT_HTTP_AUTH); + inheritOmitWww = parent->resolveCustomDataTriState(BrowserService::OPTION_OMIT_WWW); } addTriStateItems(m_browserUi->browserIntegrationHideEntriesComboBox, inheritHideEntries); addTriStateItems(m_browserUi->browserIntegrationSkipAutoSubmitComboBox, inheritSkipSubmit); addTriStateItems(m_browserUi->browserIntegrationOnlyHttpAuthComboBox, inheritOnlyHttp); addTriStateItems(m_browserUi->browserIntegrationNotHttpAuthComboBox, inheritNoHttp); + addTriStateItems(m_browserUi->browserIntegrationOmitWwwCombobox, inheritOmitWww); m_browserUi->browserIntegrationHideEntriesComboBox->setCurrentIndex( indexFromTriState(group->resolveCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY, false))); @@ -241,6 +244,8 @@ void EditGroupWidget::loadGroup(Group* group, bool create, const QSharedPointer< indexFromTriState(group->resolveCustomDataTriState(BrowserService::OPTION_ONLY_HTTP_AUTH, false))); m_browserUi->browserIntegrationNotHttpAuthComboBox->setCurrentIndex( indexFromTriState(group->resolveCustomDataTriState(BrowserService::OPTION_NOT_HTTP_AUTH, false))); + m_browserUi->browserIntegrationOmitWwwCombobox->setCurrentIndex( + indexFromTriState(group->resolveCustomDataTriState(BrowserService::OPTION_OMIT_WWW, false))); } #endif @@ -309,6 +314,9 @@ void EditGroupWidget::apply() m_temporaryGroup->setCustomDataTriState( BrowserService::OPTION_NOT_HTTP_AUTH, triStateFromIndex(m_browserUi->browserIntegrationNotHttpAuthComboBox->currentIndex())); + m_temporaryGroup->setCustomDataTriState( + BrowserService::OPTION_OMIT_WWW, + triStateFromIndex(m_browserUi->browserIntegrationOmitWwwCombobox->currentIndex())); } #endif diff --git a/src/gui/group/EditGroupWidgetBrowser.ui b/src/gui/group/EditGroupWidgetBrowser.ui index f043438d0..94069f7e6 100644 --- a/src/gui/group/EditGroupWidgetBrowser.ui +++ b/src/gui/group/EditGroupWidgetBrowser.ui @@ -145,7 +145,24 @@          -    + +   +    +    Omit WWW subdomain from matching: +    +    +    Qt::AlignLeading|Qt::AlignLeft|Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter +    +    +    +    +    +    +    Omit WWW subdomain from matching toggle for this and sub groups +    +    +    +             Qt::Vertical @@ -168,6 +185,7 @@ browserIntegrationSkipAutoSubmitComboBox browserIntegrationOnlyHttpAuthComboBox browserIntegrationNotHttpAuthComboBox + browserIntegrationOmitWwwCombobox diff --git a/tests/TestBrowser.cpp b/tests/TestBrowser.cpp index 6dcff3b74..4ad39a4f7 100644 --- a/tests/TestBrowser.cpp +++ b/tests/TestBrowser.cpp @@ -281,7 +281,7 @@ void TestBrowser::compareEntriesByPath(QSharedPointer db, QListtitle(); /* Look for an entry with that path. First using handleEntry, then through the search */ - QCOMPARE(m_browserService->handleEntry(entry, testUrl, ""), true); + QCOMPARE(m_browserService->shouldIncludeEntry(entry, testUrl, ""), true); auto result = m_browserService->searchEntries(db, testUrl, ""); QCOMPARE(result.length(), 1); QCOMPARE(result[0], entry); @@ -311,7 +311,7 @@ void TestBrowser::testSearchEntriesByUUID() for (Entry* entry : entries) { QString testUrl = "keepassxc://by-uuid/" + entry->uuidToHex(); /* Look for an entry with that UUID. First using handleEntry, then through the search */ - QCOMPARE(m_browserService->handleEntry(entry, testUrl, ""), true); + QCOMPARE(m_browserService->shouldIncludeEntry(entry, testUrl, ""), true); auto result = m_browserService->searchEntries(db, testUrl, ""); QCOMPARE(result.length(), 1); QCOMPARE(result[0], entry); @@ -329,7 +329,7 @@ void TestBrowser::testSearchEntriesByUUID() QString testUrl = "keepassxc://by-uuid/" + uuid; for (Entry* entry : entries) { - QCOMPARE(m_browserService->handleEntry(entry, testUrl, ""), false); + QCOMPARE(m_browserService->shouldIncludeEntry(entry, testUrl, ""), false); } auto result = m_browserService->searchEntries(db, testUrl, ""); @@ -439,6 +439,16 @@ void TestBrowser::testSubdomainsAndPaths() QCOMPARE(result[2]->url(), QString("http://www.github.com")); QCOMPARE(result[3]->url(), QString("www.github.com/")); + // With www subdomain omitted + root->setCustomDataTriState(BrowserService::OPTION_OMIT_WWW, Group::Enable); + result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session"); + root->setCustomDataTriState(BrowserService::OPTION_OMIT_WWW, Group::Inherit); + QCOMPARE(result.length(), 4); + QCOMPARE(result[0]->url(), QString("https://www.github.com/login/page.xml")); + QCOMPARE(result[1]->url(), QString("https://github.com")); + QCOMPARE(result[2]->url(), QString("http://www.github.com")); + QCOMPARE(result[3]->url(), QString("www.github.com/")); + // With scheme matching there should be only 1 result browserSettings()->setMatchUrlScheme(true); result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");