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) if(WITH_XC_BROWSER)
set(keepassxcbrowser_LIB keepassxcbrowser) set(keepassxcbrowser_LIB keepassxcbrowser)
set(keepassx_SOURCES ${keepassx_SOURCES} gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp) set(keepassx_SOURCES ${keepassx_SOURCES} gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp)
set(keepassx_SOURCES ${keepassx_SOURCES} gui/entry/EntryURLModel.cpp)
endif() endif()
add_subdirectory(autotype) add_subdirectory(autotype)

View File

@ -53,12 +53,6 @@ BrowserOptionDialog::BrowserOptionDialog(QWidget* parent)
m_ui->scriptWarningWidget->setVisible(false); m_ui->scriptWarningWidget->setVisible(false);
m_ui->scriptWarningWidget->setAutoHideTimeout(-1); 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!"), m_ui->warningWidget->showMessage(tr("<b>Warning:</b> The following options can be dangerous!"),
MessageWidget::Warning); MessageWidget::Warning);
@ -154,9 +148,13 @@ void BrowserOptionDialog::loadSettings()
// Check for native messaging host location errors // Check for native messaging host location errors
QString path; QString path;
if (!settings->checkIfProxyExists(path)) { if (!settings->checkIfProxyExists(path)) {
QString text = m_ui->scriptWarningWidget->text(); auto text =
text.append(path); tr("<b>Warning</b>, the keepassxc-proxy application was not found!"
m_ui->scriptWarningWidget->setText(text); "<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); m_ui->scriptWarningWidget->setVisible(true);
} else { } else {
m_ui->scriptWarningWidget->setVisible(false); m_ui->scriptWarningWidget->setVisible(false);

View File

@ -41,18 +41,20 @@
#include "gui/macutils/MacUtils.h" #include "gui/macutils/MacUtils.h"
#endif #endif
const char BrowserService::KEEPASSXCBROWSER_NAME[] = "KeePassXC-Browser Settings"; const QString BrowserService::KEEPASSXCBROWSER_NAME = QStringLiteral("KeePassXC-Browser Settings");
const char BrowserService::KEEPASSXCBROWSER_OLD_NAME[] = "keepassxc-browser Settings"; const QString BrowserService::KEEPASSXCBROWSER_OLD_NAME = QStringLiteral("keepassxc-browser Settings");
const char BrowserService::ASSOCIATE_KEY_PREFIX[] = "KPXC_BROWSER_"; const QString BrowserService::ASSOCIATE_KEY_PREFIX = QStringLiteral("KPXC_BROWSER_");
static const char KEEPASSXCBROWSER_GROUP_NAME[] = "KeePassXC-Browser Passwords"; static const QString KEEPASSXCBROWSER_GROUP_NAME = QStringLiteral("KeePassXC-Browser Passwords");
static int KEEPASSXCBROWSER_DEFAULT_ICON = 1; static int KEEPASSXCBROWSER_DEFAULT_ICON = 1;
// These are for the settings and password conversion // These are for the settings and password conversion
const char BrowserService::LEGACY_ASSOCIATE_KEY_PREFIX[] = "Public Key: "; const QString BrowserService::LEGACY_ASSOCIATE_KEY_PREFIX = QStringLiteral("Public Key: ");
static const char KEEPASSHTTP_NAME[] = "KeePassHttp Settings"; static const QString KEEPASSHTTP_NAME = QStringLiteral("KeePassHttp Settings");
static const char KEEPASSHTTP_GROUP_NAME[] = "KeePassHttp Passwords"; static const QString KEEPASSHTTP_GROUP_NAME = QStringLiteral("KeePassHttp Passwords");
// Extra entry related options saved in custom data // Extra entry related options saved in custom data
const char BrowserService::OPTION_SKIP_AUTO_SUBMIT[] = "BrowserSkipAutoSubmit"; const QString BrowserService::OPTION_SKIP_AUTO_SUBMIT = QStringLiteral("BrowserSkipAutoSubmit");
const char BrowserService::OPTION_HIDE_ENTRY[] = "BrowserHideEntry"; const QString BrowserService::OPTION_HIDE_ENTRY = QStringLiteral("BrowserHideEntry");
// Multiple URL's
const QString BrowserService::ADDITIONAL_URL = QStringLiteral("KP2A_URL");
BrowserService::BrowserService(DatabaseTabWidget* parent) BrowserService::BrowserService(DatabaseTabWidget* parent)
: m_dbTabWidget(parent) : m_dbTabWidget(parent)
@ -320,7 +322,7 @@ QString BrowserService::storeKey(const QString& key)
return {}; return {};
} }
contains = db->metadata()->customData()->contains(QLatin1String(ASSOCIATE_KEY_PREFIX) + id); contains = db->metadata()->customData()->contains(ASSOCIATE_KEY_PREFIX + id);
if (contains) { if (contains) {
dialogResult = MessageBox::warning(nullptr, dialogResult = MessageBox::warning(nullptr,
tr("KeePassXC: Overwrite existing key?"), tr("KeePassXC: Overwrite existing key?"),
@ -333,7 +335,7 @@ QString BrowserService::storeKey(const QString& key)
} while (contains && dialogResult == MessageBox::Cancel); } while (contains && dialogResult == MessageBox::Cancel);
hideWindow(); hideWindow();
db->metadata()->customData()->set(QLatin1String(ASSOCIATE_KEY_PREFIX) + id, key); db->metadata()->customData()->set(ASSOCIATE_KEY_PREFIX + id, key);
return id; return id;
} }
@ -344,7 +346,7 @@ QString BrowserService::getKey(const QString& id)
return {}; 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, QJsonArray BrowserService::findMatchingEntries(const QString& id,
@ -377,9 +379,9 @@ QJsonArray BrowserService::findMatchingEntries(const QString& id,
// Check entries for authorization // Check entries for authorization
QList<Entry*> pwEntriesToConfirm; QList<Entry*> pwEntriesToConfirm;
QList<Entry*> pwEntries; QList<Entry*> pwEntries;
for (Entry* entry : searchEntries(url, keyList)) { for (auto* entry : searchEntries(url, keyList)) {
if (entry->customData()->contains(BrowserService::OPTION_HIDE_ENTRY) && if (entry->customData()->contains(BrowserService::OPTION_HIDE_ENTRY)
entry->customData()->value(BrowserService::OPTION_HIDE_ENTRY) == "true") { && entry->customData()->value(BrowserService::OPTION_HIDE_ENTRY) == "true") {
continue; continue;
} }
@ -425,7 +427,7 @@ QJsonArray BrowserService::findMatchingEntries(const QString& id,
pwEntries = sortEntries(pwEntries, host, submitUrl); pwEntries = sortEntries(pwEntries, host, submitUrl);
// Fill the list // Fill the list
for (Entry* entry : pwEntries) { for (auto* entry : pwEntries) {
result.append(prepareEntry(entry)); result.append(prepareEntry(entry));
} }
@ -588,22 +590,30 @@ BrowserService::searchEntries(const QSharedPointer<Database>& db, const QString&
return entries; return entries;
} }
for (Entry* entry : EntrySearcher().search(baseDomain(hostname), rootGroup)) { for (const auto& group : rootGroup->groupsRecursive(true)) {
QString entryUrl = entry->url(); if (group->isRecycled() || !group->resolveSearchingEnabled()) {
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)) {
continue; continue;
} }
// Filter to match hostname in URL field for (auto* entry : group->entries()) {
if ((!entryUrl.isEmpty() && hostname.contains(entryUrl)) if (entry->isRecycled()) {
|| (matchUrlScheme(entryUrl) && hostname.endsWith(entryQUrl.host()))) { 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); entries.append(entry);
} }
} }
@ -616,7 +626,7 @@ QList<Entry*> BrowserService::searchEntries(const QString& url, const StringPair
// Check if database is connected with KeePassXC-Browser // Check if database is connected with KeePassXC-Browser
auto databaseConnected = [&](const QSharedPointer<Database>& db) { auto databaseConnected = [&](const QSharedPointer<Database>& db) {
for (const StringPair& keyPair : keyList) { 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) { if (!key.isEmpty() && keyPair.second == key) {
return true; return true;
} }
@ -668,7 +678,7 @@ void BrowserService::convertAttributesToCustomData(const QSharedPointer<Database
int counter = 0; int counter = 0;
int keyCounter = 0; int keyCounter = 0;
for (Entry* entry : entries) { for (auto* entry : entries) {
if (progress.wasCanceled()) { if (progress.wasCanceled()) {
return; return;
} }
@ -721,12 +731,9 @@ void BrowserService::convertAttributesToCustomData(const QSharedPointer<Database
return; return;
} }
const QString keePassBrowserGroupName = QLatin1String(KEEPASSXCBROWSER_GROUP_NAME); for (auto* g : rootGroup->groupsRecursive(true)) {
const QString keePassHttpGroupName = QLatin1String(KEEPASSHTTP_GROUP_NAME); if (g->name() == KEEPASSHTTP_GROUP_NAME) {
g->setName(KEEPASSXCBROWSER_GROUP_NAME);
for (Group* g : rootGroup->groupsRecursive(true)) {
if (g->name() == keePassHttpGroupName) {
g->setName(keePassBrowserGroupName);
break; break;
} }
} }
@ -745,7 +752,7 @@ QList<Entry*> BrowserService::sortEntries(QList<Entry*>& pwEntries, const QStrin
// Build map of prioritized entries // Build map of prioritized entries
QMultiMap<int, Entry*> priorities; QMultiMap<int, Entry*> priorities;
for (Entry* entry : pwEntries) { for (auto* entry : pwEntries) {
priorities.insert(sortPriority(entry, host, submitUrl, baseSubmitUrl), entry); priorities.insert(sortPriority(entry, host, submitUrl, baseSubmitUrl), entry);
} }
@ -801,7 +808,7 @@ bool BrowserService::confirmEntries(QList<Entry*>& pwEntriesToConfirm,
int res = accessControlDialog.exec(); int res = accessControlDialog.exec();
if (accessControlDialog.remember()) { if (accessControlDialog.remember()) {
for (Entry* entry : pwEntriesToConfirm) { for (auto* entry : pwEntriesToConfirm) {
BrowserEntryConfig config; BrowserEntryConfig config;
config.load(entry); config.load(entry);
if (res == QDialog::Accepted) { if (res == QDialog::Accepted) {
@ -853,8 +860,8 @@ QJsonObject BrowserService::prepareEntry(const Entry* entry)
if (browserSettings()->supportKphFields()) { if (browserSettings()->supportKphFields()) {
const EntryAttributes* attr = entry->attributes(); const EntryAttributes* attr = entry->attributes();
QJsonArray stringFields; QJsonArray stringFields;
for (const QString& key : attr->keys()) { for (const auto& key : attr->keys()) {
if (key.startsWith(QLatin1String("KPH: "))) { if (key.startsWith("KPH: ")) {
QJsonObject sField; QJsonObject sField;
sField[key] = entry->resolveMultiplePlaceholders(attr->value(key)); sField[key] = entry->resolveMultiplePlaceholders(attr->value(key));
stringFields.append(sField); stringFields.append(sField);
@ -899,17 +906,15 @@ Group* BrowserService::getDefaultEntryGroup(const QSharedPointer<Database>& sele
return nullptr; return nullptr;
} }
const QString groupName = QLatin1String(KEEPASSXCBROWSER_GROUP_NAME);
for (auto* g : rootGroup->groupsRecursive(true)) { 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()); return db->rootGroup()->findGroupByUuid(g->uuid());
} }
} }
auto* group = new Group(); auto* group = new Group();
group->setUuid(QUuid::createUuid()); group->setUuid(QUuid::createUuid());
group->setName(groupName); group->setName(KEEPASSXCBROWSER_GROUP_NAME);
group->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON); group->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON);
group->setParent(rootGroup); group->setParent(rootGroup);
return group; return group;
@ -987,6 +992,26 @@ bool BrowserService::removeFirstDomain(QString& hostname)
return false; 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. * Gets the base domain of URL.
* *
@ -1080,9 +1105,8 @@ int BrowserService::moveKeysToCustomData(Entry* entry, const QSharedPointer<Data
publicKey.remove(LEGACY_ASSOCIATE_KEY_PREFIX); publicKey.remove(LEGACY_ASSOCIATE_KEY_PREFIX);
// Add key to database custom data // Add key to database custom data
if (db && !db->metadata()->customData()->contains(QLatin1String(ASSOCIATE_KEY_PREFIX) + publicKey)) { if (db && !db->metadata()->customData()->contains(ASSOCIATE_KEY_PREFIX + publicKey)) {
db->metadata()->customData()->set(QLatin1String(ASSOCIATE_KEY_PREFIX) + publicKey, db->metadata()->customData()->set(ASSOCIATE_KEY_PREFIX + publicKey, entry->attributes()->value(key));
entry->attributes()->value(key));
++keyCounter; ++keyCounter;
} }
} }

View File

@ -68,12 +68,13 @@ public:
void convertAttributesToCustomData(const QSharedPointer<Database>& currentDb = {}); void convertAttributesToCustomData(const QSharedPointer<Database>& currentDb = {});
public: public:
static const char KEEPASSXCBROWSER_NAME[]; static const QString KEEPASSXCBROWSER_NAME;
static const char KEEPASSXCBROWSER_OLD_NAME[]; static const QString KEEPASSXCBROWSER_OLD_NAME;
static const char ASSOCIATE_KEY_PREFIX[]; static const QString ASSOCIATE_KEY_PREFIX;
static const char LEGACY_ASSOCIATE_KEY_PREFIX[]; static const QString LEGACY_ASSOCIATE_KEY_PREFIX;
static const char OPTION_SKIP_AUTO_SUBMIT[]; static const QString OPTION_SKIP_AUTO_SUBMIT;
static const char OPTION_HIDE_ENTRY[]; static const QString OPTION_HIDE_ENTRY;
static const QString ADDITIONAL_URL;
public slots: public slots:
QJsonArray findMatchingEntries(const QString& id, QJsonArray findMatchingEntries(const QString& id,
@ -129,6 +130,7 @@ private:
sortPriority(const Entry* entry, const QString& host, const QString& submitUrl, const QString& baseSubmitUrl) const; sortPriority(const Entry* entry, const QString& host, const QString& submitUrl, const QString& baseSubmitUrl) const;
bool matchUrlScheme(const QString& url); bool matchUrlScheme(const QString& url);
bool removeFirstDomain(QString& hostname); bool removeFirstDomain(QString& hostname);
bool handleURL(const QString& entryUrl, const QString& hostname, const QString& url);
QString baseDomain(const QString& url) const; QString baseDomain(const QString& url) const;
QSharedPointer<Database> getDatabase(); QSharedPointer<Database> getDatabase();
QSharedPointer<Database> selectedDatabase(); QSharedPointer<Database> selectedDatabase();

View File

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

View File

@ -762,7 +762,8 @@ Entry* Entry::clone(CloneFlags flags) const
entry->m_autoTypeAssociations->copyDataFrom(m_autoTypeAssociations); entry->m_autoTypeAssociations->copyDataFrom(m_autoTypeAssociations);
if (flags & CloneIncludeHistory) { if (flags & CloneIncludeHistory) {
for (Entry* historyItem : m_history) { 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->setUpdateTimeinfo(false);
historyItemClone->setUuid(entry->uuid()); historyItemClone->setUuid(entry->uuid());
historyItemClone->setUpdateTimeinfo(true); historyItemClone->setUpdateTimeinfo(true);

View File

@ -242,12 +242,12 @@ void DatabaseSettingsWidgetBrowser::convertAttributesToCustomData()
{ {
if (MessageBox::Yes if (MessageBox::Yes
!= MessageBox::question( != MessageBox::question(
this, this,
tr("Move KeePassHTTP attributes to custom data"), tr("Move KeePassHTTP attributes to custom data"),
tr("Do you really want to move all legacy browser integration data to the latest standard?\n" 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."), "This is necessary to maintain compatibility with the browser plugin."),
MessageBox::Yes | MessageBox::Cancel, MessageBox::Yes | MessageBox::Cancel,
MessageBox::Cancel)) { MessageBox::Cancel)) {
return; return;
} }

View File

@ -51,6 +51,7 @@
#include "sshagent/SSHAgent.h" #include "sshagent/SSHAgent.h"
#endif #endif
#ifdef WITH_XC_BROWSER #ifdef WITH_XC_BROWSER
#include "EntryURLModel.h"
#include "browser/BrowserService.h" #include "browser/BrowserService.h"
#endif #endif
#include "gui/Clipboard.h" #include "gui/Clipboard.h"
@ -82,7 +83,9 @@ EditEntryWidget::EditEntryWidget(QWidget* parent)
, m_sshAgentWidget(new QWidget()) , m_sshAgentWidget(new QWidget())
#endif #endif
#ifdef WITH_XC_BROWSER #ifdef WITH_XC_BROWSER
, m_browserSettingsChanged(false)
, m_browserWidget(new QWidget()) , m_browserWidget(new QWidget())
, m_additionalURLsDataModel(new EntryURLModel(this))
#endif #endif
, m_editWidgetProperties(new EditWidgetProperties()) , m_editWidgetProperties(new EditWidgetProperties())
, m_historyWidget(new QWidget()) , m_historyWidget(new QWidget())
@ -265,18 +268,112 @@ void EditEntryWidget::setupBrowser()
if (config()->get("Browser/Enabled", false).toBool()) { if (config()->get("Browser/Enabled", false).toBool()) {
addPage(tr("Browser Integration"), FilePath::instance()->icon("apps", "internet-web-browser"), m_browserWidget); addPage(tr("Browser Integration"), FilePath::instance()->icon("apps", "internet-web-browser"), m_browserWidget);
connect(m_browserUi->skipAutoSubmitCheckbox, SIGNAL(toggled(bool)), SLOT(updateBrowser())); m_additionalURLsDataModel->setEntryAttributes(m_entryAttributes);
connect(m_browserUi->hideEntryCheckbox, SIGNAL(toggled(bool)), SLOT(updateBrowser())); 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() void EditEntryWidget::updateBrowser()
{ {
if (!m_browserSettingsChanged) {
return;
}
auto skip = m_browserUi->skipAutoSubmitCheckbox->isChecked(); auto skip = m_browserUi->skipAutoSubmitCheckbox->isChecked();
auto hide = m_browserUi->hideEntryCheckbox->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_SKIP_AUTO_SUBMIT, (skip ? QString("true") : QString("false")));
m_customData->set(BrowserService::OPTION_HIDE_ENTRY, (hide ? 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 #endif
void EditEntryWidget::setupProperties() void EditEntryWidget::setupProperties()
@ -366,8 +463,11 @@ void EditEntryWidget::setupEntryUpdate()
#ifdef WITH_XC_BROWSER #ifdef WITH_XC_BROWSER
if (config()->get("Browser/Enabled", false).toBool()) { if (config()->get("Browser/Enabled", false).toBool()) {
connect(m_browserUi->skipAutoSubmitCheckbox, SIGNAL(toggled(bool)), this, SLOT(setModified())); connect(m_browserUi->skipAutoSubmitCheckbox, SIGNAL(toggled(bool)), SLOT(setModified()));
connect(m_browserUi->hideEntryCheckbox, SIGNAL(toggled(bool)), this, 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 #endif
} }
@ -862,7 +962,8 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
#ifdef WITH_XC_BROWSER #ifdef WITH_XC_BROWSER
if (m_customData->contains(BrowserService::OPTION_SKIP_AUTO_SUBMIT)) { 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 { } else {
m_browserUi->skipAutoSubmitCheckbox->setChecked(false); m_browserUi->skipAutoSubmitCheckbox->setChecked(false);
} }
@ -872,6 +973,15 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
} else { } else {
m_browserUi->hideEntryCheckbox->setChecked(false); 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 #endif
m_editWidgetProperties->setFields(entry->timeInfo(), entry->uuid()); m_editWidgetProperties->setFields(entry->timeInfo(), entry->uuid());
@ -946,6 +1056,12 @@ bool EditEntryWidget::commitEntry()
} }
#endif #endif
#ifdef WITH_XC_BROWSER
if (config()->get("Browser/Enabled", false).toBool()) {
updateBrowser();
}
#endif
if (!m_create) { if (!m_create) {
m_entry->beginUpdate(); m_entry->beginUpdate();
} }

View File

@ -46,6 +46,9 @@ class QStringListModel;
#include "sshagent/KeeAgentSettings.h" #include "sshagent/KeeAgentSettings.h"
class OpenSSHKey; class OpenSSHKey;
#endif #endif
#ifdef WITH_XC_BROWSER
class EntryURLModel;
#endif
namespace Ui namespace Ui
{ {
@ -120,7 +123,12 @@ private slots:
void copyPublicKey(); void copyPublicKey();
#endif #endif
#ifdef WITH_XC_BROWSER #ifdef WITH_XC_BROWSER
void updateBrowserModified();
void updateBrowser(); void updateBrowser();
void insertURL();
void removeCurrentURL();
void editCurrentURL();
void updateCurrentURL();
#endif #endif
private: private:
@ -175,7 +183,9 @@ private:
QWidget* const m_sshAgentWidget; QWidget* const m_sshAgentWidget;
#endif #endif
#ifdef WITH_XC_BROWSER #ifdef WITH_XC_BROWSER
bool m_browserSettingsChanged;
QWidget* const m_browserWidget; QWidget* const m_browserWidget;
EntryURLModel* const m_additionalURLsDataModel;
#endif #endif
EditWidgetProperties* const m_editWidgetProperties; EditWidgetProperties* const m_editWidgetProperties;
QWidget* const m_historyWidget; QWidget* const m_historyWidget;

View File

@ -54,26 +54,86 @@
</widget> </widget>
</item> </item>
<item> <item>
<spacer name="verticalSpacer_3"> <widget class="QGroupBox" name="groupBox_3">
<property name="orientation"> <property name="title">
<enum>Qt::Vertical</enum> <string>Additional URL's</string>
</property> </property>
<property name="sizeType"> <layout class="QHBoxLayout" name="horizontalLayout_1">
<enum>QSizePolicy::Expanding</enum> <item>
</property> <widget class="QListView" name="additionalURLsView">
<property name="sizeHint" stdset="0"> <property name="minimumSize">
<size> <size>
<width>20</width> <width>0</width>
<height>40</height> <height>0</height>
</size> </size>
</property> </property>
</spacer> <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> </item>
</layout> </layout>
</widget> </widget>
<tabstops> <tabstops>
<tabstop>skipAutoSubmitCheckbox</tabstop> <tabstop>skipAutoSubmitCheckbox</tabstop>
<tabstop>hideEntryCheckbox</tabstop> <tabstop>hideEntryCheckbox</tabstop>
<tabstop>additionalURLsView</tabstop>
<tabstop>addURLButton</tabstop>
<tabstop>removeURLButton</tabstop>
<tabstop>editURLButton</tabstop>
</tabstops> </tabstops>
<resources/> <resources/>
<connections/> <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 set(TEST_LIBRARIES
keepassx_core keepassx_core
${keepasshttp_LIB} ${keepassxcbrowser_LIB}
${autotype_LIB} ${autotype_LIB}
Qt5::Core Qt5::Core
Qt5::Concurrent Qt5::Concurrent

View File

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

View File

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

View File

@ -549,8 +549,7 @@ void TestCli::testCreate()
m_stderrFile->reset(); m_stderrFile->reset();
m_stdoutFile->reset(); m_stdoutFile->reset();
QCOMPARE(m_stdoutFile->readLine(), QCOMPARE(m_stdoutFile->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
QByteArray("Enter password to encrypt database (optional): \n"));
QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully created new database.\n")); QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully created new database.\n"));
Utils::Test::setNextPassword("a"); Utils::Test::setNextPassword("a");
@ -578,8 +577,7 @@ void TestCli::testCreate()
m_stdoutFile->seek(pos); m_stdoutFile->seek(pos);
m_stderrFile->seek(errPos); m_stderrFile->seek(errPos);
QCOMPARE(m_stdoutFile->readLine(), QCOMPARE(m_stdoutFile->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
QByteArray("Enter password to encrypt database (optional): \n"));
QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully created new database.\n")); QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully created new database.\n"));
Utils::Test::setNextPassword("a"); Utils::Test::setNextPassword("a");
@ -596,8 +594,7 @@ void TestCli::testCreate()
m_stdoutFile->seek(pos); m_stdoutFile->seek(pos);
m_stderrFile->seek(errPos); m_stderrFile->seek(errPos);
QCOMPARE(m_stdoutFile->readLine(), QCOMPARE(m_stdoutFile->readLine(), QByteArray("Enter password to encrypt database (optional): \n"));
QByteArray("Enter password to encrypt database (optional): \n"));
QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully created new database.\n")); QCOMPARE(m_stdoutFile->readLine(), QByteArray("Successfully created new database.\n"));
Utils::Test::setNextPassword("a"); Utils::Test::setNextPassword("a");
@ -1041,8 +1038,7 @@ void TestCli::testImport()
importCmd.execute({"import", "-q", m_xmlFile->fileName(), databaseFilenameQuiet}); importCmd.execute({"import", "-q", m_xmlFile->fileName(), databaseFilenameQuiet});
m_stdoutFile->seek(pos); m_stdoutFile->seek(pos);
QCOMPARE(m_stdoutFile->readAll(), QCOMPARE(m_stdoutFile->readAll(), QByteArray("Enter password to encrypt database (optional): \n"));
QByteArray("Enter password to encrypt database (optional): \n"));
Utils::Test::setNextPassword("a"); Utils::Test::setNextPassword("a");
auto dbQuiet = QSharedPointer<Database>(Utils::unlockDatabase(databaseFilenameQuiet, true, "", "", Utils::DEVNULL)); 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()); QVERIFY(entryCloneResetTime->timeInfo().creationTime() != entryOrg->timeInfo().creationTime());
// Date back history of original entry // Date back history of original entry
Entry * firstHistoryItem = entryOrg->historyItems()[0]; Entry* firstHistoryItem = entryOrg->historyItems()[0];
TimeInfo entryOrgHistoryTimeInfo = firstHistoryItem->timeInfo(); TimeInfo entryOrgHistoryTimeInfo = firstHistoryItem->timeInfo();
QDateTime datedBackEntryOrgModificationTime = entryOrgHistoryTimeInfo.lastModificationTime().addMSecs(-10); QDateTime datedBackEntryOrgModificationTime = entryOrgHistoryTimeInfo.lastModificationTime().addMSecs(-10);
entryOrgHistoryTimeInfo.setLastModificationTime(datedBackEntryOrgModificationTime); entryOrgHistoryTimeInfo.setLastModificationTime(datedBackEntryOrgModificationTime);
@ -140,9 +140,8 @@ void TestEntry::testClone()
// Timeinfo of history items should not be modified // Timeinfo of history items should not be modified
QList<Entry*> entryOrgHistory = entryOrg->historyItems(), clonedHistory = entryCloneHistory->historyItems(); QList<Entry*> entryOrgHistory = entryOrg->historyItems(), clonedHistory = entryCloneHistory->historyItems();
auto entryOrgHistoryItem = entryOrgHistory.constBegin(); auto entryOrgHistoryItem = entryOrgHistory.constBegin();
for(auto entryCloneHistoryItem = clonedHistory.constBegin() for (auto entryCloneHistoryItem = clonedHistory.constBegin(); entryCloneHistoryItem != clonedHistory.constEnd();
;entryCloneHistoryItem != clonedHistory.constEnd() ++entryCloneHistoryItem, ++entryOrgHistoryItem) {
;++entryCloneHistoryItem, ++entryOrgHistoryItem) {
QCOMPARE((*entryOrgHistoryItem)->timeInfo(), (*entryCloneHistoryItem)->timeInfo()); QCOMPARE((*entryOrgHistoryItem)->timeInfo(), (*entryCloneHistoryItem)->timeInfo());
} }

View File

@ -25,6 +25,8 @@
#include <QDebug> #include <QDebug>
#include <QDialogButtonBox> #include <QDialogButtonBox>
#include <QLineEdit> #include <QLineEdit>
#include <QListView>
#include <QPlainTextEdit>
#include <QPushButton> #include <QPushButton>
#include <QTableView> #include <QTableView>
#include <QToolBar> #include <QToolBar>
@ -128,6 +130,9 @@ void TestGuiBrowser::cleanupTestCase()
void TestGuiBrowser::testEntrySettings() void TestGuiBrowser::testEntrySettings()
{ {
// Enable the Browser Integration
config()->set("Browser/Enabled", true);
auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar"); auto* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
auto* entryView = m_dbWidget->findChild<EntryView*>("entryView"); auto* entryView = m_dbWidget->findChild<EntryView*>("entryView");
@ -146,7 +151,7 @@ void TestGuiBrowser::testEntrySettings()
auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget"); auto* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
// Switch to Properties page and select all rows from the custom data table // 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"); auto customDataTableView = editEntryWidget->findChild<QTableView*>("customDataTable");
QVERIFY(customDataTableView); QVERIFY(customDataTableView);
QTest::mouseClick(customDataTableView, Qt::LeftButton); QTest::mouseClick(customDataTableView, Qt::LeftButton);
@ -171,6 +176,56 @@ void TestGuiBrowser::testEntrySettings()
QCOMPARE(entry->customData()->size(), 0); 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) void TestGuiBrowser::triggerAction(const QString& name)
{ {
auto* action = m_mainWindow->findChild<QAction*>(name); auto* action = m_mainWindow->findChild<QAction*>(name);

View File

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