diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index a033089a3..633ff7a08 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -486,6 +486,33 @@ bool BrowserService::isPasswordGeneratorRequested() const return m_passwordGeneratorRequested; } +// 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 BrowserService::isUrlIdentical(const QString& first, const QString& second) const +{ + auto trimUrl = [](QString url) { + url = url.trimmed(); + if (url.endsWith("/")) { + url.remove(url.length() - 1, 1); + } + + return url; + }; + + if (first.isEmpty() || second.isEmpty()) { + return false; + } + + const auto firstUrl = trimUrl(first); + const auto secondUrl = trimUrl(second); + if (firstUrl == secondUrl) { + return true; + } + + return QUrl(firstUrl).matches(QUrl(secondUrl), QUrl::StripTrailingSlash); +} + QString BrowserService::storeKey(const QString& key) { auto db = getDatabase(); diff --git a/src/browser/BrowserService.h b/src/browser/BrowserService.h index b9e153248..e0f871b4e 100644 --- a/src/browser/BrowserService.h +++ b/src/browser/BrowserService.h @@ -63,6 +63,7 @@ public: const QString& publicKey, const QString& secretKey); bool isPasswordGeneratorRequested() const; + bool isUrlIdentical(const QString& first, const QString& second) const; void addEntry(const QString& dbid, const QString& login, diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 6c462ac93..1a53ef36c 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -161,6 +161,9 @@ void EditEntryWidget::setupMain() connect(m_mainUi->fetchFaviconButton, SIGNAL(clicked()), m_iconsWidget, SLOT(downloadFavicon())); connect(m_mainUi->urlEdit, SIGNAL(textChanged(QString)), m_iconsWidget, SLOT(setUrl(QString))); m_mainUi->urlEdit->enableVerifyMode(); +#endif +#ifdef WITH_XC_BROWSER + connect(m_mainUi->urlEdit, SIGNAL(textChanged(QString)), this, SLOT(entryURLEdited(const QString&))); #endif connect(m_mainUi->expireCheck, &QCheckBox::toggled, [&](bool enabled) { m_mainUi->expireDatePicker->setEnabled(enabled); @@ -399,6 +402,11 @@ void EditEntryWidget::updateCurrentURL() m_browserUi->removeURLButton->setEnabled(false); } } + +void EditEntryWidget::entryURLEdited(const QString& url) +{ + m_additionalURLsDataModel->setEntryUrl(url); +} #endif void EditEntryWidget::setupProperties() diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h index 89422c0d4..3bd67032f 100644 --- a/src/gui/entry/EditEntryWidget.h +++ b/src/gui/entry/EditEntryWidget.h @@ -131,6 +131,7 @@ private slots: void removeCurrentURL(); void editCurrentURL(); void updateCurrentURL(); + void entryURLEdited(const QString& url); #endif private: diff --git a/src/gui/entry/EntryURLModel.cpp b/src/gui/entry/EntryURLModel.cpp index d43c8cb16..55d8dd51c 100644 --- a/src/gui/entry/EntryURLModel.cpp +++ b/src/gui/entry/EntryURLModel.cpp @@ -54,7 +54,7 @@ void EntryURLModel::setEntryAttributes(EntryAttributes* entryAttributes) endResetModel(); } -#include + QVariant EntryURLModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) { @@ -70,10 +70,11 @@ QVariant EntryURLModel::data(const QModelIndex& index, int role) const const auto urlValid = Tools::checkUrlValid(value); // Check for duplicate URLs in the attribute list. Excludes the current key/value from the comparison. - auto customAttributeKeys = m_entryAttributes->customKeys(); + auto customAttributeKeys = m_entryAttributes->customKeys().filter(BrowserService::ADDITIONAL_URL); customAttributeKeys.removeOne(key); - const auto duplicateUrl = m_entryAttributes->values(customAttributeKeys).contains(value) || value == m_entryUrl; + const auto duplicateUrl = m_entryAttributes->values(customAttributeKeys).contains(value) + || browserService()->isUrlIdentical(value, m_entryUrl); if (role == Qt::BackgroundRole && (!urlValid || duplicateUrl)) { StateColorPalette statePalette; return statePalette.color(StateColorPalette::ColorRole::Error); diff --git a/tests/TestBrowser.cpp b/tests/TestBrowser.cpp index 4ad39a4f7..d19c77067 100644 --- a/tests/TestBrowser.cpp +++ b/tests/TestBrowser.cpp @@ -668,3 +668,19 @@ void TestBrowser::testBestMatchingWithAdditionalURLs() QCOMPARE(sorted.length(), 1); QCOMPARE(sorted[0]->url(), urls[0]); } + +void TestBrowser::testIsUrlIdentical() +{ + QVERIFY(browserService()->isUrlIdentical("https://example.com", "https://example.com")); + QVERIFY(browserService()->isUrlIdentical("https://example.com", " https://example.com ")); + QVERIFY(!browserService()->isUrlIdentical("https://example.com", "https://example2.com")); + QVERIFY(!browserService()->isUrlIdentical("https://example.com/", "https://example.com/#login")); + QVERIFY(browserService()->isUrlIdentical("https://example.com", "https://example.com/")); + QVERIFY(browserService()->isUrlIdentical("https://example.com/", "https://example.com")); + QVERIFY(browserService()->isUrlIdentical("https://example.com/ ", " https://example.com")); + QVERIFY(!browserService()->isUrlIdentical("https://example.com/", " example.com")); + QVERIFY(browserService()->isUrlIdentical("https://example.com/path/to/nowhere", + "https://example.com/path/to/nowhere/")); + QVERIFY(!browserService()->isUrlIdentical("https://example.com/", "://example.com/")); + QVERIFY(browserService()->isUrlIdentical("ftp://127.0.0.1/", "ftp://127.0.0.1")); +} diff --git a/tests/TestBrowser.h b/tests/TestBrowser.h index c4bfb0471..58cce16f2 100644 --- a/tests/TestBrowser.h +++ b/tests/TestBrowser.h @@ -51,6 +51,7 @@ private slots: void testValidURLs(); void testBestMatchingCredentials(); void testBestMatchingWithAdditionalURLs(); + void testIsUrlIdentical(); private: QList createEntries(QStringList& urls, Group* root) const;