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_<counter>, which means those are directly compatible with Keepass2Android.
This commit is contained in:
varjolintu 2019-08-15 12:35:11 +03:00 committed by Jonathan White
parent e50261a99c
commit f726d7501f
19 changed files with 568 additions and 104 deletions

View File

@ -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)

View File

@ -53,12 +53,6 @@ BrowserOptionDialog::BrowserOptionDialog(QWidget* parent)
m_ui->scriptWarningWidget->setVisible(false);
m_ui->scriptWarningWidget->setAutoHideTimeout(-1);
m_ui->scriptWarningWidget->showMessage(
tr("<b>Warning</b>, the keepassxc-proxy application was not found!"
"<br />Please check the KeePassXC installation directory or confirm the custom path in advanced options."
"<br />Browser integration WILL NOT WORK without the proxy application."
"<br />Expected Path: "),
MessageWidget::Warning);
m_ui->warningWidget->showMessage(tr("<b>Warning:</b> 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("<b>Warning</b>, the keepassxc-proxy application was not found!"
"<br />Please check the KeePassXC installation directory or confirm the custom path in advanced options."
"<br />Browser integration WILL NOT WORK without the proxy application."
"<br />Expected Path: %1")
.arg(path);
m_ui->scriptWarningWidget->showMessage(text, MessageWidget::Warning);
m_ui->scriptWarningWidget->setVisible(true);
} else {
m_ui->scriptWarningWidget->setVisible(false);

View File

@ -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<Entry*> pwEntriesToConfirm;
QList<Entry*> 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<Database>& 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<Entry*> BrowserService::searchEntries(const QString& url, const StringPair
// Check if database is connected with KeePassXC-Browser
auto databaseConnected = [&](const QSharedPointer<Database>& 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 QSharedPointer<Database
int counter = 0;
int keyCounter = 0;
for (Entry* entry : entries) {
for (auto* entry : entries) {
if (progress.wasCanceled()) {
return;
}
@ -721,12 +731,9 @@ void BrowserService::convertAttributesToCustomData(const QSharedPointer<Database
return;
}
const QString keePassBrowserGroupName = QLatin1String(KEEPASSXCBROWSER_GROUP_NAME);
const QString keePassHttpGroupName = QLatin1String(KEEPASSHTTP_GROUP_NAME);
for (Group* g : rootGroup->groupsRecursive(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<Entry*> BrowserService::sortEntries(QList<Entry*>& pwEntries, const QStrin
// Build map of prioritized entries
QMultiMap<int, Entry*> 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<Entry*>& 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<Database>& 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 QSharedPointer<Data
publicKey.remove(LEGACY_ASSOCIATE_KEY_PREFIX);
// Add key to database custom data
if (db && !db->metadata()->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;
}
}

View File

@ -68,12 +68,13 @@ public:
void convertAttributesToCustomData(const QSharedPointer<Database>& 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 @@ private:
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<Database> getDatabase();
QSharedPointer<Database> selectedDatabase();

View File

@ -257,7 +257,7 @@ namespace Bootstrap
nullptr, // do not change owner or group
pACL, // DACL specified
nullptr // do not change SACL
);
);
Cleanup:

View File

@ -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);

View File

@ -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;
}

View File

@ -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<int>&)),
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("<empty URL>"));
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();
}

View File

@ -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:
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;

View File

@ -54,26 +54,86 @@
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Additional URL's</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_1">
<item>
<widget class="QListView" name="additionalURLsView">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContents</enum>
</property>
<property name="resizeMode">
<enum>QListView::Adjust</enum>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="additionalURLsButtonLayout">
<item>
<widget class="QPushButton" name="addURLButton">
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeURLButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="editURLButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Edit</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>skipAutoSubmitCheckbox</tabstop>
<tabstop>hideEntryCheckbox</tabstop>
<tabstop>additionalURLsView</tabstop>
<tabstop>addURLButton</tabstop>
<tabstop>removeURLButton</tabstop>
<tabstop>editURLButton</tabstop>
</tabstops>
<resources/>
<connections/>

View File

@ -0,0 +1,120 @@
/*
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "EntryURLModel.h"
#include "core/Entry.h"
#include "core/Tools.h"
#include <algorithm>
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<QString> 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);
}
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_ENTRYURLMODEL_H
#define KEEPASSXC_ENTRYURLMODEL_H
#include <QStandardItemModel>
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<QPair<QString, QString>> m_urls;
EntryAttributes* m_entryAttributes;
};
#endif // KEEPASSXC_ENTRYURLMODEL_H

View File

@ -89,7 +89,7 @@ endmacro(add_unit_test)
set(TEST_LIBRARIES
keepassx_core
${keepasshttp_LIB}
${keepassxcbrowser_LIB}
${autotype_LIB}
Qt5::Core
Qt5::Concurrent

View File

@ -245,6 +245,40 @@ void TestBrowser::testSearchEntriesWithPort()
QCOMPARE(result[0]->url(), QString("http://127.0.0.1:443"));
}
void TestBrowser::testSearchEntriesWithAdditionalURLs()
{
auto db = QSharedPointer<Database>::create();
auto* root = db->rootGroup();
QList<Entry*> entries;
QList<QString> 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<Database>::create();

View File

@ -42,6 +42,7 @@ private slots:
void testSortPriority();
void testSearchEntries();
void testSearchEntriesWithPort();
void testSearchEntriesWithAdditionalURLs();
void testSortEntries();
void testGetDatabaseGroups();

View File

@ -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<Database>(Utils::unlockDatabase(databaseFilenameQuiet, true, "", "", Utils::DEVNULL));

View File

@ -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<Entry*> 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());
}

View File

@ -25,6 +25,8 @@
#include <QDebug>
#include <QDialogButtonBox>
#include <QLineEdit>
#include <QListView>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QTableView>
#include <QToolBar>
@ -128,6 +130,9 @@ void TestGuiBrowser::cleanupTestCase()
void TestGuiBrowser::testEntrySettings()
{
// Enable the Browser Integration
config()->set("Browser/Enabled", true);
auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
@ -146,7 +151,7 @@ void TestGuiBrowser::testEntrySettings()
auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
// Switch to Properties page and select all rows from the custom data table
editEntryWidget->setCurrentPage(4);
editEntryWidget->setCurrentPage(5);
auto customDataTableView = editEntryWidget->findChild<QTableView*>("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<QToolBar*>("toolBar");
auto* entryView = m_dbWidget->findChild<EntryView*>("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<QAction*>("actionEntryEdit");
QWidget* entryEditWidget = toolBar->widgetForAction(entryEditAction);
QTest::mouseClick(entryEditWidget, Qt::LeftButton);
QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
// Switch to Browser Integration page and add three URL's
editEntryWidget->setCurrentPage(4);
auto* addURLButton = editEntryWidget->findChild<QPushButton*>("addURLButton");
QVERIFY(addURLButton);
auto* urlList = editEntryWidget->findChild<QListView*>("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<QListView*>("attributesView");
auto* attrTextEdit = editEntryWidget->findChild<QPlainTextEdit*>("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<QAction*>(name);

View File

@ -44,6 +44,7 @@ private slots:
void cleanupTestCase();
void testEntrySettings();
void testAdditionalURLs();
private:
void triggerAction(const QString& name);