diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index acc94785d1..77acf290ea 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -218,6 +218,7 @@ add_subdirectory(proxy)
if(WITH_XC_BROWSER)
set(keepassxcbrowser_LIB keepassxcbrowser)
set(keepassx_SOURCES ${keepassx_SOURCES} gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp)
+ set(keepassx_SOURCES ${keepassx_SOURCES} gui/entry/EntryURLModel.cpp)
endif()
add_subdirectory(autotype)
diff --git a/src/browser/BrowserOptionDialog.cpp b/src/browser/BrowserOptionDialog.cpp
index eea5cb4b10..a5bb921da5 100644
--- a/src/browser/BrowserOptionDialog.cpp
+++ b/src/browser/BrowserOptionDialog.cpp
@@ -53,12 +53,6 @@ BrowserOptionDialog::BrowserOptionDialog(QWidget* parent)
m_ui->scriptWarningWidget->setVisible(false);
m_ui->scriptWarningWidget->setAutoHideTimeout(-1);
- m_ui->scriptWarningWidget->showMessage(
- tr("Warning, the keepassxc-proxy application was not found!"
- "
Please check the KeePassXC installation directory or confirm the custom path in advanced options."
- "
Browser integration WILL NOT WORK without the proxy application."
- "
Expected Path: "),
- MessageWidget::Warning);
m_ui->warningWidget->showMessage(tr("Warning: The following options can be dangerous!"),
MessageWidget::Warning);
@@ -154,9 +148,13 @@ void BrowserOptionDialog::loadSettings()
// Check for native messaging host location errors
QString path;
if (!settings->checkIfProxyExists(path)) {
- QString text = m_ui->scriptWarningWidget->text();
- text.append(path);
- m_ui->scriptWarningWidget->setText(text);
+ auto text =
+ tr("Warning, the keepassxc-proxy application was not found!"
+ "
Please check the KeePassXC installation directory or confirm the custom path in advanced options."
+ "
Browser integration WILL NOT WORK without the proxy application."
+ "
Expected Path: %1")
+ .arg(path);
+ m_ui->scriptWarningWidget->showMessage(text, MessageWidget::Warning);
m_ui->scriptWarningWidget->setVisible(true);
} else {
m_ui->scriptWarningWidget->setVisible(false);
diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp
index 7b7d7a3afa..8a6ad0ec50 100644
--- a/src/browser/BrowserService.cpp
+++ b/src/browser/BrowserService.cpp
@@ -41,18 +41,20 @@
#include "gui/macutils/MacUtils.h"
#endif
-const char BrowserService::KEEPASSXCBROWSER_NAME[] = "KeePassXC-Browser Settings";
-const char BrowserService::KEEPASSXCBROWSER_OLD_NAME[] = "keepassxc-browser Settings";
-const char BrowserService::ASSOCIATE_KEY_PREFIX[] = "KPXC_BROWSER_";
-static const char KEEPASSXCBROWSER_GROUP_NAME[] = "KeePassXC-Browser Passwords";
+const QString BrowserService::KEEPASSXCBROWSER_NAME = QStringLiteral("KeePassXC-Browser Settings");
+const QString BrowserService::KEEPASSXCBROWSER_OLD_NAME = QStringLiteral("keepassxc-browser Settings");
+const QString BrowserService::ASSOCIATE_KEY_PREFIX = QStringLiteral("KPXC_BROWSER_");
+static const QString KEEPASSXCBROWSER_GROUP_NAME = QStringLiteral("KeePassXC-Browser Passwords");
static int KEEPASSXCBROWSER_DEFAULT_ICON = 1;
// These are for the settings and password conversion
-const char BrowserService::LEGACY_ASSOCIATE_KEY_PREFIX[] = "Public Key: ";
-static const char KEEPASSHTTP_NAME[] = "KeePassHttp Settings";
-static const char KEEPASSHTTP_GROUP_NAME[] = "KeePassHttp Passwords";
+const QString BrowserService::LEGACY_ASSOCIATE_KEY_PREFIX = QStringLiteral("Public Key: ");
+static const QString KEEPASSHTTP_NAME = QStringLiteral("KeePassHttp Settings");
+static const QString KEEPASSHTTP_GROUP_NAME = QStringLiteral("KeePassHttp Passwords");
// Extra entry related options saved in custom data
-const char BrowserService::OPTION_SKIP_AUTO_SUBMIT[] = "BrowserSkipAutoSubmit";
-const char BrowserService::OPTION_HIDE_ENTRY[] = "BrowserHideEntry";
+const QString BrowserService::OPTION_SKIP_AUTO_SUBMIT = QStringLiteral("BrowserSkipAutoSubmit");
+const QString BrowserService::OPTION_HIDE_ENTRY = QStringLiteral("BrowserHideEntry");
+// Multiple URL's
+const QString BrowserService::ADDITIONAL_URL = QStringLiteral("KP2A_URL");
BrowserService::BrowserService(DatabaseTabWidget* parent)
: m_dbTabWidget(parent)
@@ -320,7 +322,7 @@ QString BrowserService::storeKey(const QString& key)
return {};
}
- contains = db->metadata()->customData()->contains(QLatin1String(ASSOCIATE_KEY_PREFIX) + id);
+ contains = db->metadata()->customData()->contains(ASSOCIATE_KEY_PREFIX + id);
if (contains) {
dialogResult = MessageBox::warning(nullptr,
tr("KeePassXC: Overwrite existing key?"),
@@ -333,7 +335,7 @@ QString BrowserService::storeKey(const QString& key)
} while (contains && dialogResult == MessageBox::Cancel);
hideWindow();
- db->metadata()->customData()->set(QLatin1String(ASSOCIATE_KEY_PREFIX) + id, key);
+ db->metadata()->customData()->set(ASSOCIATE_KEY_PREFIX + id, key);
return id;
}
@@ -344,7 +346,7 @@ QString BrowserService::getKey(const QString& id)
return {};
}
- return db->metadata()->customData()->value(QLatin1String(ASSOCIATE_KEY_PREFIX) + id);
+ return db->metadata()->customData()->value(ASSOCIATE_KEY_PREFIX + id);
}
QJsonArray BrowserService::findMatchingEntries(const QString& id,
@@ -377,9 +379,9 @@ QJsonArray BrowserService::findMatchingEntries(const QString& id,
// Check entries for authorization
QList pwEntriesToConfirm;
QList pwEntries;
- for (Entry* entry : searchEntries(url, keyList)) {
- if (entry->customData()->contains(BrowserService::OPTION_HIDE_ENTRY) &&
- entry->customData()->value(BrowserService::OPTION_HIDE_ENTRY) == "true") {
+ for (auto* entry : searchEntries(url, keyList)) {
+ if (entry->customData()->contains(BrowserService::OPTION_HIDE_ENTRY)
+ && entry->customData()->value(BrowserService::OPTION_HIDE_ENTRY) == "true") {
continue;
}
@@ -425,7 +427,7 @@ QJsonArray BrowserService::findMatchingEntries(const QString& id,
pwEntries = sortEntries(pwEntries, host, submitUrl);
// Fill the list
- for (Entry* entry : pwEntries) {
+ for (auto* entry : pwEntries) {
result.append(prepareEntry(entry));
}
@@ -588,22 +590,30 @@ BrowserService::searchEntries(const QSharedPointer& db, const QString&
return entries;
}
- for (Entry* entry : EntrySearcher().search(baseDomain(hostname), rootGroup)) {
- QString entryUrl = entry->url();
- QUrl entryQUrl(entryUrl);
- QString entryScheme = entryQUrl.scheme();
- QUrl qUrl(url);
-
- // Ignore entry if port or scheme defined in the URL doesn't match
- if ((entryQUrl.port() > 0 && entryQUrl.port() != qUrl.port())
- || (browserSettings()->matchUrlScheme() && !entryScheme.isEmpty()
- && entryScheme.compare(qUrl.scheme()) != 0)) {
+ for (const auto& group : rootGroup->groupsRecursive(true)) {
+ if (group->isRecycled() || !group->resolveSearchingEnabled()) {
continue;
}
- // Filter to match hostname in URL field
- if ((!entryUrl.isEmpty() && hostname.contains(entryUrl))
- || (matchUrlScheme(entryUrl) && hostname.endsWith(entryQUrl.host()))) {
+ for (auto* entry : group->entries()) {
+ if (entry->isRecycled()) {
+ continue;
+ }
+
+ // Search for additional URL's starting with KP2A_URL
+ if (entry->attributes()->keys().contains(ADDITIONAL_URL)) {
+ for (const auto& key : entry->attributes()->keys()) {
+ if (key.startsWith(ADDITIONAL_URL) && handleURL(entry->attributes()->value(key), hostname, url)) {
+ entries.append(entry);
+ continue;
+ }
+ }
+ }
+
+ if (!handleURL(entry->url(), hostname, url)) {
+ continue;
+ }
+
entries.append(entry);
}
}
@@ -616,7 +626,7 @@ QList BrowserService::searchEntries(const QString& url, const StringPair
// Check if database is connected with KeePassXC-Browser
auto databaseConnected = [&](const QSharedPointer& db) {
for (const StringPair& keyPair : keyList) {
- QString key = db->metadata()->customData()->value(QLatin1String(ASSOCIATE_KEY_PREFIX) + keyPair.first);
+ QString key = db->metadata()->customData()->value(ASSOCIATE_KEY_PREFIX + keyPair.first);
if (!key.isEmpty() && keyPair.second == key) {
return true;
}
@@ -668,7 +678,7 @@ void BrowserService::convertAttributesToCustomData(const QSharedPointergroupsRecursive(true)) {
- if (g->name() == keePassHttpGroupName) {
- g->setName(keePassBrowserGroupName);
+ for (auto* g : rootGroup->groupsRecursive(true)) {
+ if (g->name() == KEEPASSHTTP_GROUP_NAME) {
+ g->setName(KEEPASSXCBROWSER_GROUP_NAME);
break;
}
}
@@ -745,7 +752,7 @@ QList BrowserService::sortEntries(QList& pwEntries, const QStrin
// Build map of prioritized entries
QMultiMap priorities;
- for (Entry* entry : pwEntries) {
+ for (auto* entry : pwEntries) {
priorities.insert(sortPriority(entry, host, submitUrl, baseSubmitUrl), entry);
}
@@ -801,7 +808,7 @@ bool BrowserService::confirmEntries(QList& pwEntriesToConfirm,
int res = accessControlDialog.exec();
if (accessControlDialog.remember()) {
- for (Entry* entry : pwEntriesToConfirm) {
+ for (auto* entry : pwEntriesToConfirm) {
BrowserEntryConfig config;
config.load(entry);
if (res == QDialog::Accepted) {
@@ -853,8 +860,8 @@ QJsonObject BrowserService::prepareEntry(const Entry* entry)
if (browserSettings()->supportKphFields()) {
const EntryAttributes* attr = entry->attributes();
QJsonArray stringFields;
- for (const QString& key : attr->keys()) {
- if (key.startsWith(QLatin1String("KPH: "))) {
+ for (const auto& key : attr->keys()) {
+ if (key.startsWith("KPH: ")) {
QJsonObject sField;
sField[key] = entry->resolveMultiplePlaceholders(attr->value(key));
stringFields.append(sField);
@@ -899,17 +906,15 @@ Group* BrowserService::getDefaultEntryGroup(const QSharedPointer& sele
return nullptr;
}
- const QString groupName = QLatin1String(KEEPASSXCBROWSER_GROUP_NAME);
-
for (auto* g : rootGroup->groupsRecursive(true)) {
- if (g->name() == groupName && !g->isRecycled()) {
+ if (g->name() == KEEPASSXCBROWSER_GROUP_NAME && !g->isRecycled()) {
return db->rootGroup()->findGroupByUuid(g->uuid());
}
}
auto* group = new Group();
group->setUuid(QUuid::createUuid());
- group->setName(groupName);
+ group->setName(KEEPASSXCBROWSER_GROUP_NAME);
group->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON);
group->setParent(rootGroup);
return group;
@@ -987,6 +992,26 @@ bool BrowserService::removeFirstDomain(QString& hostname)
return false;
}
+bool BrowserService::handleURL(const QString& entryUrl, const QString& hostname, const QString& url)
+{
+ QUrl entryQUrl(entryUrl);
+ QString entryScheme = entryQUrl.scheme();
+ QUrl qUrl(url);
+
+ // Ignore entry if port or scheme defined in the URL doesn't match
+ if ((entryQUrl.port() > 0 && entryQUrl.port() != qUrl.port())
+ || (browserSettings()->matchUrlScheme() && !entryScheme.isEmpty() && entryScheme.compare(qUrl.scheme()) != 0)) {
+ return false;
+ }
+
+ // Filter to match hostname in URL field
+ if ((!entryUrl.isEmpty() && hostname.contains(entryUrl))
+ || (matchUrlScheme(entryUrl) && hostname.endsWith(entryQUrl.host()))) {
+ return true;
+ }
+ return false;
+};
+
/**
* Gets the base domain of URL.
*
@@ -1080,9 +1105,8 @@ int BrowserService::moveKeysToCustomData(Entry* entry, const QSharedPointermetadata()->customData()->contains(QLatin1String(ASSOCIATE_KEY_PREFIX) + publicKey)) {
- db->metadata()->customData()->set(QLatin1String(ASSOCIATE_KEY_PREFIX) + publicKey,
- entry->attributes()->value(key));
+ if (db && !db->metadata()->customData()->contains(ASSOCIATE_KEY_PREFIX + publicKey)) {
+ db->metadata()->customData()->set(ASSOCIATE_KEY_PREFIX + publicKey, entry->attributes()->value(key));
++keyCounter;
}
}
diff --git a/src/browser/BrowserService.h b/src/browser/BrowserService.h
index 2958155b4b..a18a974483 100644
--- a/src/browser/BrowserService.h
+++ b/src/browser/BrowserService.h
@@ -68,12 +68,13 @@ class BrowserService : public QObject
void convertAttributesToCustomData(const QSharedPointer& currentDb = {});
public:
- static const char KEEPASSXCBROWSER_NAME[];
- static const char KEEPASSXCBROWSER_OLD_NAME[];
- static const char ASSOCIATE_KEY_PREFIX[];
- static const char LEGACY_ASSOCIATE_KEY_PREFIX[];
- static const char OPTION_SKIP_AUTO_SUBMIT[];
- static const char OPTION_HIDE_ENTRY[];
+ static const QString KEEPASSXCBROWSER_NAME;
+ static const QString KEEPASSXCBROWSER_OLD_NAME;
+ static const QString ASSOCIATE_KEY_PREFIX;
+ static const QString LEGACY_ASSOCIATE_KEY_PREFIX;
+ static const QString OPTION_SKIP_AUTO_SUBMIT;
+ static const QString OPTION_HIDE_ENTRY;
+ static const QString ADDITIONAL_URL;
public slots:
QJsonArray findMatchingEntries(const QString& id,
@@ -129,6 +130,7 @@ public slots:
sortPriority(const Entry* entry, const QString& host, const QString& submitUrl, const QString& baseSubmitUrl) const;
bool matchUrlScheme(const QString& url);
bool removeFirstDomain(QString& hostname);
+ bool handleURL(const QString& entryUrl, const QString& hostname, const QString& url);
QString baseDomain(const QString& url) const;
QSharedPointer getDatabase();
QSharedPointer selectedDatabase();
diff --git a/src/core/Bootstrap.cpp b/src/core/Bootstrap.cpp
index 2d1a3e0878..204942f4de 100644
--- a/src/core/Bootstrap.cpp
+++ b/src/core/Bootstrap.cpp
@@ -257,7 +257,7 @@ namespace Bootstrap
nullptr, // do not change owner or group
pACL, // DACL specified
nullptr // do not change SACL
- );
+ );
Cleanup:
diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp
index 60d64167ae..677c7b3872 100644
--- a/src/core/Entry.cpp
+++ b/src/core/Entry.cpp
@@ -762,7 +762,8 @@ Entry* Entry::clone(CloneFlags flags) const
entry->m_autoTypeAssociations->copyDataFrom(m_autoTypeAssociations);
if (flags & CloneIncludeHistory) {
for (Entry* historyItem : m_history) {
- Entry* historyItemClone = historyItem->clone(flags & ~CloneIncludeHistory & ~CloneNewUuid & ~CloneResetTimeInfo);
+ Entry* historyItemClone =
+ historyItem->clone(flags & ~CloneIncludeHistory & ~CloneNewUuid & ~CloneResetTimeInfo);
historyItemClone->setUpdateTimeinfo(false);
historyItemClone->setUuid(entry->uuid());
historyItemClone->setUpdateTimeinfo(true);
diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp
index fac85c21e6..4ea30c1f6f 100644
--- a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp
+++ b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp
@@ -242,12 +242,12 @@ void DatabaseSettingsWidgetBrowser::convertAttributesToCustomData()
{
if (MessageBox::Yes
!= MessageBox::question(
- this,
- tr("Move KeePassHTTP attributes to custom data"),
- tr("Do you really want to move all legacy browser integration data to the latest standard?\n"
- "This is necessary to maintain compatibility with the browser plugin."),
- MessageBox::Yes | MessageBox::Cancel,
- MessageBox::Cancel)) {
+ this,
+ tr("Move KeePassHTTP attributes to custom data"),
+ tr("Do you really want to move all legacy browser integration data to the latest standard?\n"
+ "This is necessary to maintain compatibility with the browser plugin."),
+ MessageBox::Yes | MessageBox::Cancel,
+ MessageBox::Cancel)) {
return;
}
diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp
index 0238131f5a..3345848afe 100644
--- a/src/gui/entry/EditEntryWidget.cpp
+++ b/src/gui/entry/EditEntryWidget.cpp
@@ -51,6 +51,7 @@
#include "sshagent/SSHAgent.h"
#endif
#ifdef WITH_XC_BROWSER
+#include "EntryURLModel.h"
#include "browser/BrowserService.h"
#endif
#include "gui/Clipboard.h"
@@ -82,7 +83,9 @@ EditEntryWidget::EditEntryWidget(QWidget* parent)
, m_sshAgentWidget(new QWidget())
#endif
#ifdef WITH_XC_BROWSER
+ , m_browserSettingsChanged(false)
, m_browserWidget(new QWidget())
+ , m_additionalURLsDataModel(new EntryURLModel(this))
#endif
, m_editWidgetProperties(new EditWidgetProperties())
, m_historyWidget(new QWidget())
@@ -265,18 +268,112 @@ void EditEntryWidget::setupBrowser()
if (config()->get("Browser/Enabled", false).toBool()) {
addPage(tr("Browser Integration"), FilePath::instance()->icon("apps", "internet-web-browser"), m_browserWidget);
- connect(m_browserUi->skipAutoSubmitCheckbox, SIGNAL(toggled(bool)), SLOT(updateBrowser()));
- connect(m_browserUi->hideEntryCheckbox, SIGNAL(toggled(bool)), SLOT(updateBrowser()));
+ m_additionalURLsDataModel->setEntryAttributes(m_entryAttributes);
+ m_browserUi->additionalURLsView->setModel(m_additionalURLsDataModel);
+
+ // clang-format off
+ connect(m_browserUi->skipAutoSubmitCheckbox, SIGNAL(toggled(bool)), SLOT(updateBrowserModified()));
+ connect(m_browserUi->hideEntryCheckbox, SIGNAL(toggled(bool)), SLOT(updateBrowserModified()));
+ connect(m_browserUi->addURLButton, SIGNAL(clicked()), SLOT(insertURL()));
+ connect(m_browserUi->removeURLButton, SIGNAL(clicked()), SLOT(removeCurrentURL()));
+ connect(m_browserUi->editURLButton, SIGNAL(clicked()), SLOT(editCurrentURL()));
+ connect(m_browserUi->additionalURLsView->selectionModel(),
+ SIGNAL(currentChanged(QModelIndex,QModelIndex)),
+ SLOT(updateCurrentURL()));
+ connect(m_additionalURLsDataModel,
+ SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector&)),
+ SLOT(updateCurrentAttribute()));
+ // clang-format on
}
}
+void EditEntryWidget::updateBrowserModified()
+{
+ m_browserSettingsChanged = true;
+}
+
void EditEntryWidget::updateBrowser()
{
+ if (!m_browserSettingsChanged) {
+ return;
+ }
+
auto skip = m_browserUi->skipAutoSubmitCheckbox->isChecked();
auto hide = m_browserUi->hideEntryCheckbox->isChecked();
m_customData->set(BrowserService::OPTION_SKIP_AUTO_SUBMIT, (skip ? QString("true") : QString("false")));
m_customData->set(BrowserService::OPTION_HIDE_ENTRY, (hide ? QString("true") : QString("false")));
}
+
+void EditEntryWidget::insertURL()
+{
+ Q_ASSERT(!m_history);
+
+ QString name("KP2A_URL");
+ int i = 1;
+
+ while (m_entryAttributes->keys().contains(name)) {
+ name = QString("KP2A_URL_%1").arg(i);
+ i++;
+ }
+
+ m_entryAttributes->set(name, tr(""));
+ QModelIndex index = m_additionalURLsDataModel->indexByKey(name);
+
+ m_browserUi->additionalURLsView->setCurrentIndex(index);
+ m_browserUi->additionalURLsView->edit(index);
+
+ setModified(true);
+}
+
+void EditEntryWidget::removeCurrentURL()
+{
+ Q_ASSERT(!m_history);
+
+ QModelIndex index = m_browserUi->additionalURLsView->currentIndex();
+
+ if (index.isValid()) {
+ auto result = MessageBox::question(this,
+ tr("Confirm Removal"),
+ tr("Are you sure you want to remove this URL?"),
+ MessageBox::Remove | MessageBox::Cancel,
+ MessageBox::Cancel);
+
+ if (result == MessageBox::Remove) {
+ m_entryAttributes->remove(m_additionalURLsDataModel->keyByIndex(index));
+ if (m_additionalURLsDataModel->rowCount() == 0) {
+ m_browserUi->editURLButton->setEnabled(false);
+ m_browserUi->removeURLButton->setEnabled(false);
+ }
+ setModified(true);
+ }
+ }
+}
+
+void EditEntryWidget::editCurrentURL()
+{
+ Q_ASSERT(!m_history);
+
+ QModelIndex index = m_browserUi->additionalURLsView->currentIndex();
+
+ if (index.isValid()) {
+ m_browserUi->additionalURLsView->edit(index);
+ setModified(true);
+ }
+}
+
+void EditEntryWidget::updateCurrentURL()
+{
+ QModelIndex index = m_browserUi->additionalURLsView->currentIndex();
+
+ if (index.isValid()) {
+ // Don't allow editing in history view
+ m_browserUi->editURLButton->setEnabled(!m_history);
+ m_browserUi->removeURLButton->setEnabled(!m_history);
+ } else {
+ m_browserUi->editURLButton->setEnabled(false);
+ m_browserUi->removeURLButton->setEnabled(false);
+ }
+}
#endif
void EditEntryWidget::setupProperties()
@@ -366,8 +463,11 @@ void EditEntryWidget::setupEntryUpdate()
#ifdef WITH_XC_BROWSER
if (config()->get("Browser/Enabled", false).toBool()) {
- connect(m_browserUi->skipAutoSubmitCheckbox, SIGNAL(toggled(bool)), this, SLOT(setModified()));
- connect(m_browserUi->hideEntryCheckbox, SIGNAL(toggled(bool)), this, SLOT(setModified()));
+ connect(m_browserUi->skipAutoSubmitCheckbox, SIGNAL(toggled(bool)), SLOT(setModified()));
+ connect(m_browserUi->hideEntryCheckbox, SIGNAL(toggled(bool)), SLOT(setModified()));
+ connect(m_browserUi->addURLButton, SIGNAL(toggled(bool)), SLOT(setModified()));
+ connect(m_browserUi->removeURLButton, SIGNAL(toggled(bool)), SLOT(setModified()));
+ connect(m_browserUi->editURLButton, SIGNAL(toggled(bool)), SLOT(setModified()));
}
#endif
}
@@ -862,7 +962,8 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
#ifdef WITH_XC_BROWSER
if (m_customData->contains(BrowserService::OPTION_SKIP_AUTO_SUBMIT)) {
- m_browserUi->skipAutoSubmitCheckbox->setChecked(m_customData->value(BrowserService::OPTION_SKIP_AUTO_SUBMIT) == "true");
+ m_browserUi->skipAutoSubmitCheckbox->setChecked(m_customData->value(BrowserService::OPTION_SKIP_AUTO_SUBMIT)
+ == "true");
} else {
m_browserUi->skipAutoSubmitCheckbox->setChecked(false);
}
@@ -872,6 +973,15 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
} else {
m_browserUi->hideEntryCheckbox->setChecked(false);
}
+
+ m_browserUi->addURLButton->setEnabled(!m_history);
+ m_browserUi->removeURLButton->setEnabled(false);
+ m_browserUi->editURLButton->setEnabled(false);
+ m_browserUi->additionalURLsView->setEditTriggers(editTriggers);
+
+ if (m_additionalURLsDataModel->rowCount() != 0) {
+ m_browserUi->additionalURLsView->setCurrentIndex(m_additionalURLsDataModel->index(0, 0));
+ }
#endif
m_editWidgetProperties->setFields(entry->timeInfo(), entry->uuid());
@@ -946,6 +1056,12 @@ bool EditEntryWidget::commitEntry()
}
#endif
+#ifdef WITH_XC_BROWSER
+ if (config()->get("Browser/Enabled", false).toBool()) {
+ updateBrowser();
+ }
+#endif
+
if (!m_create) {
m_entry->beginUpdate();
}
diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h
index e70c548c39..8c6fee0bc0 100644
--- a/src/gui/entry/EditEntryWidget.h
+++ b/src/gui/entry/EditEntryWidget.h
@@ -46,6 +46,9 @@ class QStringListModel;
#include "sshagent/KeeAgentSettings.h"
class OpenSSHKey;
#endif
+#ifdef WITH_XC_BROWSER
+class EntryURLModel;
+#endif
namespace Ui
{
@@ -120,7 +123,12 @@ private slots:
void copyPublicKey();
#endif
#ifdef WITH_XC_BROWSER
+ void updateBrowserModified();
void updateBrowser();
+ void insertURL();
+ void removeCurrentURL();
+ void editCurrentURL();
+ void updateCurrentURL();
#endif
private:
@@ -175,7 +183,9 @@ private slots:
QWidget* const m_sshAgentWidget;
#endif
#ifdef WITH_XC_BROWSER
+ bool m_browserSettingsChanged;
QWidget* const m_browserWidget;
+ EntryURLModel* const m_additionalURLsDataModel;
#endif
EditWidgetProperties* const m_editWidgetProperties;
QWidget* const m_historyWidget;
diff --git a/src/gui/entry/EditEntryWidgetBrowser.ui b/src/gui/entry/EditEntryWidgetBrowser.ui
index 73e64dfb3c..4d0d29cf72 100644
--- a/src/gui/entry/EditEntryWidgetBrowser.ui
+++ b/src/gui/entry/EditEntryWidgetBrowser.ui
@@ -54,26 +54,86 @@
-
-
-
- Qt::Vertical
-
-
- QSizePolicy::Expanding
-
-
-
- 20
- 40
-
-
-
+
+
+ Additional URL's
+
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ QAbstractScrollArea::AdjustToContents
+
+
+ QListView::Adjust
+
+
+
+ -
+
+
-
+
+
+ Add
+
+
+
+ -
+
+
+ false
+
+
+ Remove
+
+
+
+ -
+
+
+ false
+
+
+ Edit
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+ QSizePolicy::Expanding
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
skipAutoSubmitCheckbox
hideEntryCheckbox
+ additionalURLsView
+ addURLButton
+ removeURLButton
+ editURLButton
diff --git a/src/gui/entry/EntryURLModel.cpp b/src/gui/entry/EntryURLModel.cpp
new file mode 100644
index 0000000000..3667c78f0c
--- /dev/null
+++ b/src/gui/entry/EntryURLModel.cpp
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2012 Felix Geyer
+ * Copyright (C) 2019 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "EntryURLModel.h"
+
+#include "core/Entry.h"
+#include "core/Tools.h"
+
+#include
+
+EntryURLModel::EntryURLModel(QObject* parent)
+ : QStandardItemModel(parent)
+ , m_entryAttributes(nullptr)
+{
+}
+
+void EntryURLModel::setEntryAttributes(EntryAttributes* entryAttributes)
+{
+ beginResetModel();
+
+ if (m_entryAttributes) {
+ m_entryAttributes->disconnect(this);
+ }
+
+ m_entryAttributes = entryAttributes;
+
+ if (m_entryAttributes) {
+ updateAttributes();
+ // clang-format off
+ connect(m_entryAttributes, SIGNAL(added(QString)), SLOT(updateAttributes()));
+ connect(m_entryAttributes, SIGNAL(customKeyModified(QString)), SLOT(updateAttributes()));
+ connect(m_entryAttributes, SIGNAL(removed(QString)), SLOT(updateAttributes()));
+ connect(m_entryAttributes, SIGNAL(renamed(QString,QString)), SLOT(updateAttributes()));
+ connect(m_entryAttributes, SIGNAL(reset()), SLOT(updateAttributes()));
+ // clang-format on
+ }
+
+ endResetModel();
+}
+
+bool EntryURLModel::setData(const QModelIndex& index, const QVariant& value, int role)
+{
+ if (!index.isValid() || role != Qt::EditRole || value.type() != QVariant::String || value.toString().isEmpty()) {
+ return false;
+ }
+
+ const int row = index.row();
+ const QString key = m_urls.at(row).first;
+ const QString oldValue = m_urls.at(row).second;
+
+ if (EntryAttributes::isDefaultAttribute(key) || m_entryAttributes->containsValue(value.toString())) {
+ return false;
+ }
+
+ m_entryAttributes->set(key, value.toString());
+
+ emit dataChanged(this->index(row, 0), this->index(row, columnCount() - 1));
+ return true;
+}
+
+QModelIndex EntryURLModel::indexByKey(const QString& key) const
+{
+ int row = -1;
+ for (int i = 0; i < m_urls.size(); ++i) {
+ if (m_urls.at(i).first == key) {
+ row = i;
+ break;
+ }
+ }
+
+ if (row == -1) {
+ return QModelIndex();
+ } else {
+ return index(row, 0);
+ }
+}
+
+QString EntryURLModel::keyByIndex(const QModelIndex& index) const
+{
+ if (!index.isValid()) {
+ return QString();
+ } else {
+ return m_urls.at(index.row()).first;
+ }
+}
+
+void EntryURLModel::updateAttributes()
+{
+ clear();
+ m_urls.clear();
+
+ const QList attributesKeyList = m_entryAttributes->keys();
+ for (const QString& key : attributesKeyList) {
+ if (!EntryAttributes::isDefaultAttribute(key) && key.contains("KP2A_URL")) {
+ const auto value = m_entryAttributes->value(key);
+ m_urls.append(qMakePair(key, value));
+
+ auto* item = new QStandardItem(value);
+ if (m_entryAttributes->isProtected(key)) {
+ item->setFlags(item->flags() & ~Qt::ItemIsEnabled);
+ }
+ appendRow(item);
+ }
+ }
+}
diff --git a/src/gui/entry/EntryURLModel.h b/src/gui/entry/EntryURLModel.h
new file mode 100644
index 0000000000..09344d92af
--- /dev/null
+++ b/src/gui/entry/EntryURLModel.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2012 Felix Geyer
+ * Copyright (C) 2019 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSXC_ENTRYURLMODEL_H
+#define KEEPASSXC_ENTRYURLMODEL_H
+
+#include
+
+class EntryAttributes;
+
+class EntryURLModel : public QStandardItemModel
+{
+ Q_OBJECT
+
+public:
+ explicit EntryURLModel(QObject* parent = nullptr);
+ void setEntryAttributes(EntryAttributes* entryAttributes);
+ void insertRow(const QString& key, const QString& value);
+ bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override;
+ QModelIndex indexByKey(const QString& key) const;
+ QString keyByIndex(const QModelIndex& index) const;
+
+private slots:
+ void updateAttributes();
+
+private:
+ QList> m_urls;
+ EntryAttributes* m_entryAttributes;
+};
+
+#endif // KEEPASSXC_ENTRYURLMODEL_H
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index d76fd3fc59..c4df1d8e66 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -89,7 +89,7 @@ endmacro(add_unit_test)
set(TEST_LIBRARIES
keepassx_core
- ${keepasshttp_LIB}
+ ${keepassxcbrowser_LIB}
${autotype_LIB}
Qt5::Core
Qt5::Concurrent
diff --git a/tests/TestBrowser.cpp b/tests/TestBrowser.cpp
index b66c3f7224..576b72c181 100644
--- a/tests/TestBrowser.cpp
+++ b/tests/TestBrowser.cpp
@@ -245,6 +245,40 @@ void TestBrowser::testSearchEntriesWithPort()
QCOMPARE(result[0]->url(), QString("http://127.0.0.1:443"));
}
+void TestBrowser::testSearchEntriesWithAdditionalURLs()
+{
+ auto db = QSharedPointer::create();
+ auto* root = db->rootGroup();
+
+ QList entries;
+ QList urls;
+ urls.push_back("https://github.com/");
+ urls.push_back("https://www.example.com");
+ urls.push_back("http://domain.com");
+
+ for (int i = 0; i < urls.length(); ++i) {
+ auto entry = new Entry();
+ entry->setGroup(root);
+ entry->beginUpdate();
+ entry->setUrl(urls[i]);
+ entry->setUsername(QString("User %1").arg(i));
+ entry->endUpdate();
+ entries.push_back(entry);
+ }
+
+ // Add an additional URL to the first entry
+ entries.first()->attributes()->set(BrowserService::ADDITIONAL_URL, "https://keepassxc.org");
+
+ auto result = m_browserService->searchEntries(db, "github.com", "https://github.com"); // db, hostname, url
+ QCOMPARE(result.length(), 1);
+ QCOMPARE(result[0]->url(), QString("https://github.com/"));
+
+ // Search the additional URL. It should return the same entry
+ auto additionalResult = m_browserService->searchEntries(db, "keepassxc.org", "https://keepassxc.org");
+ QCOMPARE(additionalResult.length(), 1);
+ QCOMPARE(additionalResult[0]->url(), QString("https://github.com/"));
+}
+
void TestBrowser::testSortEntries()
{
auto db = QSharedPointer::create();
diff --git a/tests/TestBrowser.h b/tests/TestBrowser.h
index 0b939b0772..0eed0d23f9 100644
--- a/tests/TestBrowser.h
+++ b/tests/TestBrowser.h
@@ -42,6 +42,7 @@ private slots:
void testSortPriority();
void testSearchEntries();
void testSearchEntriesWithPort();
+ void testSearchEntriesWithAdditionalURLs();
void testSortEntries();
void testGetDatabaseGroups();
diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp
index d9fbc25b4c..8bc865799a 100644
--- a/tests/TestCli.cpp
+++ b/tests/TestCli.cpp
@@ -549,8 +549,7 @@ void TestCli::testCreate()
m_stderrFile->reset();
m_stdoutFile->reset();
- QCOMPARE(m_stdoutFile->readLine(),
- QByteArray("Enter password to encrypt database (optional): \n"));
+ QCOMPARE(m_stdoutFile->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully created new database.\n"));
Utils::Test::setNextPassword("a");
@@ -578,8 +577,7 @@ void TestCli::testCreate()
m_stdoutFile->seek(pos);
m_stderrFile->seek(errPos);
- QCOMPARE(m_stdoutFile->readLine(),
- QByteArray("Enter password to encrypt database (optional): \n"));
+ QCOMPARE(m_stdoutFile->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully created new database.\n"));
Utils::Test::setNextPassword("a");
@@ -596,8 +594,7 @@ void TestCli::testCreate()
m_stdoutFile->seek(pos);
m_stderrFile->seek(errPos);
- QCOMPARE(m_stdoutFile->readLine(),
- QByteArray("Enter password to encrypt database (optional): \n"));
+ QCOMPARE(m_stdoutFile->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully created new database.\n"));
Utils::Test::setNextPassword("a");
@@ -1041,8 +1038,7 @@ void TestCli::testImport()
importCmd.execute({"import", "-q", m_xmlFile->fileName(), databaseFilenameQuiet});
m_stdoutFile->seek(pos);
- QCOMPARE(m_stdoutFile->readAll(),
- QByteArray("Enter password to encrypt database (optional): \n"));
+ QCOMPARE(m_stdoutFile->readAll(), QByteArray("Enter password to encrypt database (optional): \n"));
Utils::Test::setNextPassword("a");
auto dbQuiet = QSharedPointer(Utils::unlockDatabase(databaseFilenameQuiet, true, "", "", Utils::DEVNULL));
diff --git a/tests/TestEntry.cpp b/tests/TestEntry.cpp
index 708654a6a6..39e4bd12c8 100644
--- a/tests/TestEntry.cpp
+++ b/tests/TestEntry.cpp
@@ -124,7 +124,7 @@ void TestEntry::testClone()
QVERIFY(entryCloneResetTime->timeInfo().creationTime() != entryOrg->timeInfo().creationTime());
// Date back history of original entry
- Entry * firstHistoryItem = entryOrg->historyItems()[0];
+ Entry* firstHistoryItem = entryOrg->historyItems()[0];
TimeInfo entryOrgHistoryTimeInfo = firstHistoryItem->timeInfo();
QDateTime datedBackEntryOrgModificationTime = entryOrgHistoryTimeInfo.lastModificationTime().addMSecs(-10);
entryOrgHistoryTimeInfo.setLastModificationTime(datedBackEntryOrgModificationTime);
@@ -140,9 +140,8 @@ void TestEntry::testClone()
// Timeinfo of history items should not be modified
QList entryOrgHistory = entryOrg->historyItems(), clonedHistory = entryCloneHistory->historyItems();
auto entryOrgHistoryItem = entryOrgHistory.constBegin();
- for(auto entryCloneHistoryItem = clonedHistory.constBegin()
- ;entryCloneHistoryItem != clonedHistory.constEnd()
- ;++entryCloneHistoryItem, ++entryOrgHistoryItem) {
+ for (auto entryCloneHistoryItem = clonedHistory.constBegin(); entryCloneHistoryItem != clonedHistory.constEnd();
+ ++entryCloneHistoryItem, ++entryOrgHistoryItem) {
QCOMPARE((*entryOrgHistoryItem)->timeInfo(), (*entryCloneHistoryItem)->timeInfo());
}
diff --git a/tests/gui/TestGuiBrowser.cpp b/tests/gui/TestGuiBrowser.cpp
index 1b578df02b..834aea581c 100644
--- a/tests/gui/TestGuiBrowser.cpp
+++ b/tests/gui/TestGuiBrowser.cpp
@@ -25,6 +25,8 @@
#include
#include
#include
+#include
+#include
#include
#include
#include
@@ -128,6 +130,9 @@ void TestGuiBrowser::cleanupTestCase()
void TestGuiBrowser::testEntrySettings()
{
+ // Enable the Browser Integration
+ config()->set("Browser/Enabled", true);
+
auto* toolBar = m_mainWindow->findChild("toolBar");
auto* entryView = m_dbWidget->findChild("entryView");
@@ -146,7 +151,7 @@ void TestGuiBrowser::testEntrySettings()
auto* editEntryWidget = m_dbWidget->findChild("editEntryWidget");
// Switch to Properties page and select all rows from the custom data table
- editEntryWidget->setCurrentPage(4);
+ editEntryWidget->setCurrentPage(5);
auto customDataTableView = editEntryWidget->findChild("customDataTable");
QVERIFY(customDataTableView);
QTest::mouseClick(customDataTableView, Qt::LeftButton);
@@ -171,6 +176,56 @@ void TestGuiBrowser::testEntrySettings()
QCOMPARE(entry->customData()->size(), 0);
}
+void TestGuiBrowser::testAdditionalURLs()
+{
+ auto* toolBar = m_mainWindow->findChild("toolBar");
+ auto* entryView = m_dbWidget->findChild("entryView");
+
+ entryView->setFocus();
+ QVERIFY(entryView->hasFocus());
+
+ // Select the first entry in the database
+ QModelIndex entryItem = entryView->model()->index(0, 1);
+ clickIndex(entryItem, entryView, Qt::LeftButton);
+
+ auto* entryEditAction = m_mainWindow->findChild("actionEntryEdit");
+ QWidget* entryEditWidget = toolBar->widgetForAction(entryEditAction);
+ QTest::mouseClick(entryEditWidget, Qt::LeftButton);
+ QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
+ auto* editEntryWidget = m_dbWidget->findChild("editEntryWidget");
+
+ // Switch to Browser Integration page and add three URL's
+ editEntryWidget->setCurrentPage(4);
+ auto* addURLButton = editEntryWidget->findChild("addURLButton");
+ QVERIFY(addURLButton);
+
+ auto* urlList = editEntryWidget->findChild("additionalURLsView");
+ QVERIFY(urlList);
+
+ QStringList testURLs = {"https://example1.com", "https://example2.com", "https://example3.com"};
+
+ for (const auto& url : testURLs) {
+ QTest::mouseClick(addURLButton, Qt::LeftButton);
+ QApplication::processEvents();
+ QTest::keyClicks(urlList->focusWidget(), url);
+ QTest::keyClick(urlList->focusWidget(), Qt::Key_Enter);
+ }
+
+ // Check the values from attributesEdit
+ editEntryWidget->setCurrentPage(1);
+ auto* attributesView = editEntryWidget->findChild("attributesView");
+ auto* attrTextEdit = editEntryWidget->findChild("attributesEdit");
+
+ // Go top of the list
+ attributesView->setFocus();
+ QTest::keyClick(attributesView->focusWidget(), Qt::Key_PageUp);
+
+ for (const auto& url : testURLs) {
+ QCOMPARE(attrTextEdit->toPlainText(), url);
+ QTest::keyClick(attributesView->focusWidget(), Qt::Key_Down);
+ }
+}
+
void TestGuiBrowser::triggerAction(const QString& name)
{
auto* action = m_mainWindow->findChild(name);
diff --git a/tests/gui/TestGuiBrowser.h b/tests/gui/TestGuiBrowser.h
index e9bab810f2..53a9c73c42 100644
--- a/tests/gui/TestGuiBrowser.h
+++ b/tests/gui/TestGuiBrowser.h
@@ -44,6 +44,7 @@ private slots:
void cleanupTestCase();
void testEntrySettings();
+ void testAdditionalURLs();
private:
void triggerAction(const QString& name);