From af335c231f5ff1cf2c5b6225e46f048522e1bd5a Mon Sep 17 00:00:00 2001 From: varjolintu Date: Thu, 15 Aug 2019 12:35:11 +0300 Subject: [PATCH] Add support for multiple URLs in an entry * Fixes #398 The new Browser Integration entry settings page has a list view with any additional URL's. These URL's are added to the entry attributes with KP2A_URL_, which means those are directly compatible with Keepass2Android. --- src/CMakeLists.txt | 1 + src/browser/BrowserOptionDialog.cpp | 16 +-- src/browser/BrowserService.cpp | 120 ++++++++++------- src/browser/BrowserService.h | 14 +- src/core/Bootstrap.cpp | 2 +- src/core/Entry.cpp | 3 +- .../DatabaseSettingsWidgetBrowser.cpp | 12 +- src/gui/entry/EditEntryWidget.cpp | 126 +++++++++++++++++- src/gui/entry/EditEntryWidget.h | 10 ++ src/gui/entry/EditEntryWidgetBrowser.ui | 88 ++++++++++-- src/gui/entry/EntryURLModel.cpp | 120 +++++++++++++++++ src/gui/entry/EntryURLModel.h | 46 +++++++ tests/CMakeLists.txt | 2 +- tests/TestBrowser.cpp | 34 +++++ tests/TestBrowser.h | 1 + tests/TestCli.cpp | 12 +- tests/TestEntry.cpp | 7 +- tests/gui/TestGuiBrowser.cpp | 57 +++++++- tests/gui/TestGuiBrowser.h | 1 + 19 files changed, 568 insertions(+), 104 deletions(-) create mode 100644 src/gui/entry/EntryURLModel.cpp create mode 100644 src/gui/entry/EntryURLModel.h 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);