Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/keepassxreboot/keepassxc.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonathan White <support@dmapps.us>2020-09-27 19:05:33 +0300
committerJonathan White <support@dmapps.us>2020-09-27 19:11:02 +0300
commite1c2537084651c4cfeb466b28f5bcd71d05dc4ff (patch)
tree2c86608d0537fcf82ed02e9d631e9043357c2c95
parentac5c1af829bd7da63295b60426d3001840a5f158 (diff)
parent9fd9d65995e0c1a7ed96fbd025f42b696c40a3d9 (diff)
Merge branch 'release/2.6.2' into develop
-rw-r--r--docs/styles/dark.css4
-rw-r--r--docs/topics/DownloadInstall.adoc2
-rw-r--r--docs/topics/KeyboardShortcuts.adoc3
-rw-r--r--docs/topics/UserInterface.adoc37
-rw-r--r--src/browser/BrowserAction.cpp8
-rw-r--r--src/browser/BrowserService.cpp281
-rw-r--r--src/browser/BrowserService.h36
-rw-r--r--src/cli/keepassxc-cli.cpp7
-rw-r--r--src/core/Config.cpp90
-rw-r--r--src/core/Config.h7
-rw-r--r--src/core/Tools.cpp3
-rw-r--r--src/gui/DatabaseOpenDialog.cpp6
-rw-r--r--src/gui/DatabaseTabWidget.cpp2
-rw-r--r--src/gui/DatabaseWidget.cpp9
-rw-r--r--src/gui/EditWidget.cpp18
-rw-r--r--src/gui/EntryPreviewWidget.cpp2
-rw-r--r--src/gui/MainWindow.cpp4
-rw-r--r--src/gui/PasswordGeneratorWidget.cpp1
-rw-r--r--src/gui/entry/EditEntryWidget.cpp5
-rw-r--r--src/gui/entry/EditEntryWidget.h3
-rw-r--r--src/gui/entry/EditEntryWidgetMain.ui584
-rw-r--r--src/gui/entry/EntryView.cpp40
-rw-r--r--src/gui/entry/EntryView.h1
-rw-r--r--src/gui/group/EditGroupWidget.cpp2
-rw-r--r--src/gui/group/EditGroupWidget.h3
-rw-r--r--src/gui/group/EditGroupWidgetMain.ui432
-rw-r--r--src/gui/reports/ReportsWidgetHealthcheck.cpp25
-rw-r--r--src/gui/reports/ReportsWidgetHibp.cpp25
-rw-r--r--src/gui/styles/base/basestyle.qss5
-rw-r--r--src/main.cpp53
-rw-r--r--tests/TestBrowser.cpp278
-rw-r--r--tests/TestBrowser.h2
32 files changed, 1118 insertions, 860 deletions
diff --git a/docs/styles/dark.css b/docs/styles/dark.css
index 2c5b50a29..8f7bd67b6 100644
--- a/docs/styles/dark.css
+++ b/docs/styles/dark.css
@@ -455,6 +455,8 @@ p{font-family: "Noto Sans",sans-serif !important}
blockquote{color:var(--quotecolor) !important}
.quoteblock{color:var(--textcolor)}
code{color:var(--textcoloralt);background-color: var(--sidebarbackground) !important}
+pre,pre>code{line-height:1.25; color:var(--textcoloralt);}
+.keyseq{color:var(--textcoloralt);}
/* Table styles */
@@ -531,4 +533,4 @@ a:hover {color: var(--linkhovercolor);}
}
.subtitle {
font-size: 1.5em;
-} \ No newline at end of file
+}
diff --git a/docs/topics/DownloadInstall.adoc b/docs/topics/DownloadInstall.adoc
index 4e17c66bc..cc6f1fcb7 100644
--- a/docs/topics/DownloadInstall.adoc
+++ b/docs/topics/DownloadInstall.adoc
@@ -51,6 +51,8 @@ image::linux_store.png[]
The Snap and Flatpak options are sandboxed applications (more secure). The Native option is installed with the operating system files. Read more about the limitations of these options here: https://keepassxc.org/docs/#faq-appsnap-yubikey[KeePassXC Snap FAQ]
+NOTE: KeePassXC stores a configuration file in `~/.cache` to remember window position, recent files, and other local settings. If you mount this folder to a tmpdisk you will lose settings after reboot.
+
=== macOS
To install the KeePassXC app on macOS, double click on the downloaded DMG file and use the click and drag option as shown:
diff --git a/docs/topics/KeyboardShortcuts.adoc b/docs/topics/KeyboardShortcuts.adoc
index 837fa9608..93baa303d 100644
--- a/docs/topics/KeyboardShortcuts.adoc
+++ b/docs/topics/KeyboardShortcuts.adoc
@@ -3,6 +3,8 @@ include::.sharedheader[]
:imagesdir: ../images
// tag::content[]
+NOTE: On macOS please substitute `Ctrl` with `Cmd` (aka `⌘`).
+
[grid=rows, frame=none, width=75%]
|===
|Action | Keyboard Shortcut
@@ -31,6 +33,7 @@ include::.sharedheader[]
|Hide Window | Ctrl + Shift + M
|Select Next Database Tab | Ctrl + Tab ; Ctrl + PageDn
|Select Previous Database Tab | Ctrl + Shift + Tab ; Ctrl + PageUp
+|Select the nth database | Ctrl + n, where n is the number of the database tab
|Toggle Passwords Hidden | Ctrl + Shift + C
|Toggle Usernames Hidden | Ctrl + Shift + B
|Focus Groups (edit if focused) | F1
diff --git a/docs/topics/UserInterface.adoc b/docs/topics/UserInterface.adoc
index 1fee94608..1f0ca9cf2 100644
--- a/docs/topics/UserInterface.adoc
+++ b/docs/topics/UserInterface.adoc
@@ -48,4 +48,41 @@ image::compact_mode_comparison.png[]
=== Keyboard Shortcuts
include::KeyboardShortcuts.adoc[tag=content, leveloffset=+1]
+
+// tag::advanced[]
+=== Command-Line Options
+You can use the following command line options to tailor the application to your preferences:
+
+----
+Usage: keepassxc.exe [options] [filename(s)]
+KeePassXC - cross-platform password manager
+
+Options:
+ -?, -h, --help Displays help on commandline options.
+ --help-all Displays help including Qt specific options.
+ -v, --version Displays version information.
+ --config <config> path to a custom config file
+ --localconfig <localconfig> path to a custom local config file
+ --keyfile <keyfile> key file of the database
+ --pw-stdin read password of the database from stdin
+ --debug-info Displays debugging information.
+
+Arguments:
+ filename(s) filenames of the password databases to open (*.kdbx)
+----
+
+Additionally, the following environment variables may be useful when running the application:
+
+[grid=rows, frame=none, width=75%]
+|===
+|Env Var | Description
+
+|KPXC_CONFIG | Override default path to roaming configuration file
+|KPXC_CONFIG_LOCAL | Override default path to local configuration file
+|SSH_AUTH_SOCKET | Path of the unix file socket that the agent uses for communication with other processes (SSH Agent)
+|QT_SCALE_FACTOR [numeric] | Defines a global scale factor for the whole application, including point-sized fonts.
+|QT_SCREEN_SCALE_FACTORS [list] | Specifies scale factors for each screen. See https://doc.qt.io/qt-5/highdpi.html#high-dpi-support-in-qt
+|QT_SCALE_FACTOR_ROUNDING_POLICY | Control device pixel ratio rounding to the nearest integer. See https://doc.qt.io/qt-5/highdpi.html#high-dpi-support-in-qt
+|===
+// end::advanced[]
// end::content[]
diff --git a/src/browser/BrowserAction.cpp b/src/browser/BrowserAction.cpp
index 8e0c26909..65fe4cb7b 100644
--- a/src/browser/BrowserAction.cpp
+++ b/src/browser/BrowserAction.cpp
@@ -267,8 +267,8 @@ QJsonObject BrowserAction::handleGetLogins(const QJsonObject& json, const QStrin
return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE);
}
- const QString url = decrypted.value("url").toString();
- if (url.isEmpty()) {
+ const QString siteUrl = decrypted.value("url").toString();
+ if (siteUrl.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_NO_URL_PROVIDED);
}
@@ -281,10 +281,10 @@ QJsonObject BrowserAction::handleGetLogins(const QJsonObject& json, const QStrin
}
const QString id = decrypted.value("id").toString();
- const QString submit = decrypted.value("submitUrl").toString();
+ const QString formUrl = decrypted.value("submitUrl").toString();
const QString auth = decrypted.value("httpAuth").toString();
const bool httpAuth = auth.compare(TRUE_STR, Qt::CaseSensitive) == 0 ? true : false;
- const QJsonArray users = browserService()->findMatchingEntries(id, url, submit, "", keyList, httpAuth);
+ const QJsonArray users = browserService()->findMatchingEntries(id, siteUrl, formUrl, "", keyList, httpAuth);
if (users.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_NO_LOGINS_FOUND);
diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp
index f46472b9c..f1ce19eed 100644
--- a/src/browser/BrowserService.cpp
+++ b/src/browser/BrowserService.cpp
@@ -371,8 +371,8 @@ QString BrowserService::getKey(const QString& id)
}
QJsonArray BrowserService::findMatchingEntries(const QString& dbid,
- const QString& url,
- const QString& submitUrl,
+ const QString& siteUrlStr,
+ const QString& formUrlStr,
const QString& realm,
const StringPairList& keyList,
const bool httpAuth)
@@ -380,13 +380,13 @@ QJsonArray BrowserService::findMatchingEntries(const QString& dbid,
Q_UNUSED(dbid);
const bool alwaysAllowAccess = browserSettings()->alwaysAllowAccess();
const bool ignoreHttpAuth = browserSettings()->httpAuthPermission();
- const QString host = QUrl(url).host();
- const QString submitHost = QUrl(submitUrl).host();
+ const QString siteHost = QUrl(siteUrlStr).host();
+ const QString formHost = QUrl(formUrlStr).host();
// Check entries for authorization
QList<Entry*> pwEntriesToConfirm;
QList<Entry*> pwEntries;
- for (auto* entry : searchEntries(url, submitUrl, keyList)) {
+ for (auto* entry : searchEntries(siteUrlStr, formUrlStr, keyList)) {
if (entry->customData()->contains(BrowserService::OPTION_HIDE_ENTRY)
&& entry->customData()->value(BrowserService::OPTION_HIDE_ENTRY) == TRUE_STR) {
continue;
@@ -403,7 +403,7 @@ QJsonArray BrowserService::findMatchingEntries(const QString& dbid,
continue;
}
- switch (checkAccess(entry, host, submitHost, realm)) {
+ switch (checkAccess(entry, siteHost, formHost, realm)) {
case Denied:
continue;
@@ -422,7 +422,8 @@ QJsonArray BrowserService::findMatchingEntries(const QString& dbid,
}
// Confirm entries
- QList<Entry*> selectedEntriesToConfirm = confirmEntries(pwEntriesToConfirm, url, host, submitHost, realm, httpAuth);
+ QList<Entry*> selectedEntriesToConfirm =
+ confirmEntries(pwEntriesToConfirm, siteUrlStr, siteHost, formHost, realm, httpAuth);
if (!selectedEntriesToConfirm.isEmpty()) {
pwEntries.append(selectedEntriesToConfirm);
}
@@ -437,7 +438,7 @@ QJsonArray BrowserService::findMatchingEntries(const QString& dbid,
}
// Sort results
- pwEntries = sortEntries(pwEntries, host, submitUrl, url);
+ pwEntries = sortEntries(pwEntries, siteUrlStr, formUrlStr);
// Fill the list
QJsonArray result;
@@ -451,8 +452,8 @@ QJsonArray BrowserService::findMatchingEntries(const QString& dbid,
void BrowserService::addEntry(const QString& dbid,
const QString& login,
const QString& password,
- const QString& url,
- const QString& submitUrl,
+ const QString& siteUrlStr,
+ const QString& formUrlStr,
const QString& realm,
const QString& group,
const QString& groupUuid,
@@ -467,8 +468,8 @@ void BrowserService::addEntry(const QString& dbid,
auto* entry = new Entry();
entry->setUuid(QUuid::createUuid());
- entry->setTitle(QUrl(url).host());
- entry->setUrl(url);
+ entry->setTitle(QUrl(siteUrlStr).host());
+ entry->setUrl(siteUrlStr);
entry->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON);
entry->setUsername(login);
entry->setPassword(password);
@@ -487,8 +488,8 @@ void BrowserService::addEntry(const QString& dbid,
entry->setGroup(getDefaultEntryGroup(db));
}
- const QString host = QUrl(url).host();
- const QString submitHost = QUrl(submitUrl).host();
+ const QString host = QUrl(siteUrlStr).host();
+ const QString submitHost = QUrl(formUrlStr).host();
BrowserEntryConfig config;
config.allow(host);
@@ -505,8 +506,8 @@ bool BrowserService::updateEntry(const QString& dbid,
const QString& uuid,
const QString& login,
const QString& password,
- const QString& url,
- const QString& submitUrl)
+ const QString& siteUrlStr,
+ const QString& formUrlStr)
{
// TODO: select database based on this key id
Q_UNUSED(dbid);
@@ -518,7 +519,7 @@ bool BrowserService::updateEntry(const QString& dbid,
Entry* entry = db->rootGroup()->findEntryByUuid(Tools::hexToUuid(uuid));
if (!entry) {
// If entry is not found for update, add a new one to the selected database
- addEntry(dbid, login, password, url, submitUrl, "", "", "", db);
+ addEntry(dbid, login, password, siteUrlStr, formUrlStr, "", "", "", db);
return true;
}
@@ -547,7 +548,7 @@ bool BrowserService::updateEntry(const QString& dbid,
dialogResult = MessageBox::question(
nullptr,
tr("KeePassXC: Update Entry"),
- tr("Do you want to update the information in %1 - %2?").arg(QUrl(url).host(), username),
+ tr("Do you want to update the information in %1 - %2?").arg(QUrl(siteUrlStr).host(), username),
MessageBox::Save | MessageBox::Cancel,
MessageBox::Cancel,
MessageBox::Raise);
@@ -570,7 +571,7 @@ bool BrowserService::updateEntry(const QString& dbid,
}
QList<Entry*>
-BrowserService::searchEntries(const QSharedPointer<Database>& db, const QString& url, const QString& submitUrl)
+BrowserService::searchEntries(const QSharedPointer<Database>& db, const QString& siteUrlStr, const QString& formUrlStr)
{
QList<Entry*> entries;
auto* rootGroup = db->rootGroup();
@@ -590,25 +591,29 @@ BrowserService::searchEntries(const QSharedPointer<Database>& db, const QString&
// Search for additional URL's starting with KP2A_URL
for (const auto& key : entry->attributes()->keys()) {
- if (key.startsWith(ADDITIONAL_URL) && handleURL(entry->attributes()->value(key), url, submitUrl)
+ if (key.startsWith(ADDITIONAL_URL) && handleURL(entry->attributes()->value(key), siteUrlStr, formUrlStr)
&& !entries.contains(entry)) {
entries.append(entry);
continue;
}
}
- if (!handleEntry(entry, url, submitUrl)) {
+ if (!handleEntry(entry, siteUrlStr, formUrlStr)) {
continue;
}
- entries.append(entry);
+ // Additional URL check may have already inserted the entry to the list
+ if (!entries.contains(entry)) {
+ entries.append(entry);
+ }
}
}
return entries;
}
-QList<Entry*> BrowserService::searchEntries(const QString& url, const QString& submitUrl, const StringPairList& keyList)
+QList<Entry*>
+BrowserService::searchEntries(const QString& siteUrlStr, const QString& formUrlStr, const StringPairList& keyList)
{
// Check if database is connected with KeePassXC-Browser
auto databaseConnected = [&](const QSharedPointer<Database>& db) {
@@ -638,11 +643,11 @@ QList<Entry*> BrowserService::searchEntries(const QString& url, const QString& s
}
// Search entries matching the hostname
- QString hostname = QUrl(url).host();
+ QString hostname = QUrl(siteUrlStr).host();
QList<Entry*> entries;
do {
for (const auto& db : databases) {
- entries << searchEntries(db, url, submitUrl);
+ entries << searchEntries(db, siteUrlStr, formUrlStr);
}
} while (entries.isEmpty() && removeFirstDomain(hostname));
@@ -722,47 +727,30 @@ void BrowserService::convertAttributesToCustomData(QSharedPointer<Database> db)
}
}
-QList<Entry*> BrowserService::sortEntries(QList<Entry*>& pwEntries,
- const QString& host,
- const QString& entryUrl,
- const QString& fullUrl)
+QList<Entry*>
+BrowserService::sortEntries(QList<Entry*>& pwEntries, const QString& siteUrlStr, const QString& formUrlStr)
{
- QUrl url(entryUrl);
- if (url.scheme().isEmpty()) {
- url.setScheme("https");
- }
-
- const QString submitUrl = url.toString(QUrl::StripTrailingSlash);
- const QString baseSubmitUrl =
- url.toString(QUrl::StripTrailingSlash | QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment);
-
// Build map of prioritized entries
QMultiMap<int, Entry*> priorities;
for (auto* entry : pwEntries) {
- priorities.insert(sortPriority(entry, host, submitUrl, baseSubmitUrl, fullUrl), entry);
+ priorities.insert(sortPriority(getEntryURLs(entry), siteUrlStr, formUrlStr), entry);
}
+ auto keys = priorities.uniqueKeys();
+ std::sort(keys.begin(), keys.end(), [](int l, int r) { return l > r; });
+
QList<Entry*> results;
- QString field = browserSettings()->sortByTitle() ? "Title" : "UserName";
- for (int i = 100; i >= 0; i -= 5) {
- if (priorities.count(i) > 0) {
- // Sort same priority entries by Title or UserName
- auto entries = priorities.values(i);
- std::sort(entries.begin(), entries.end(), [&field](Entry* left, Entry* right) {
- return (QString::localeAwareCompare(left->attributes()->value(field), right->attributes()->value(field))
- < 0)
- || ((QString::localeAwareCompare(left->attributes()->value(field),
- right->attributes()->value(field))
- == 0)
- && (QString::localeAwareCompare(left->attributes()->value("UserName"),
- right->attributes()->value("UserName"))
- < 0));
- });
- results << entries;
- if (browserSettings()->bestMatchOnly() && !pwEntries.isEmpty()) {
- // Early out once we find the highest batch of matches
- break;
- }
+ auto sortField = browserSettings()->sortByTitle() ? EntryAttributes::TitleKey : EntryAttributes::UserNameKey;
+ for (auto key : keys) {
+ // Sort same priority entries by Title or UserName
+ auto entries = priorities.values(key);
+ std::sort(entries.begin(), entries.end(), [&sortField](Entry* left, Entry* right) {
+ return QString::localeAwareCompare(left->attribute(sortField), right->attribute(sortField));
+ });
+ results << entries;
+ if (browserSettings()->bestMatchOnly() && !results.isEmpty()) {
+ // Early out once we find the highest batch of matches
+ break;
}
}
@@ -770,9 +758,9 @@ QList<Entry*> BrowserService::sortEntries(QList<Entry*>& pwEntries,
}
QList<Entry*> BrowserService::confirmEntries(QList<Entry*>& pwEntriesToConfirm,
- const QString& url,
- const QString& host,
- const QString& submitHost,
+ const QString& siteUrlStr,
+ const QString& siteHost,
+ const QString& formUrlStr,
const QString& realm,
const bool httpAuth)
{
@@ -790,9 +778,9 @@ QList<Entry*> BrowserService::confirmEntries(QList<Entry*>& pwEntriesToConfirm,
auto entry = pwEntriesToConfirm[item->row()];
BrowserEntryConfig config;
config.load(entry);
- config.deny(host);
- if (!submitHost.isEmpty() && host != submitHost) {
- config.deny(submitHost);
+ config.deny(siteHost);
+ if (!formUrlStr.isEmpty() && siteHost != formUrlStr) {
+ config.deny(formUrlStr);
}
if (!realm.isEmpty()) {
config.setRealm(realm);
@@ -800,7 +788,7 @@ QList<Entry*> BrowserService::confirmEntries(QList<Entry*>& pwEntriesToConfirm,
config.save(entry);
});
- accessControlDialog.setItems(pwEntriesToConfirm, url, httpAuth);
+ accessControlDialog.setItems(pwEntriesToConfirm, siteUrlStr, httpAuth);
QList<Entry*> allowedEntries;
if (accessControlDialog.exec() == QDialog::Accepted) {
@@ -810,9 +798,9 @@ QList<Entry*> BrowserService::confirmEntries(QList<Entry*>& pwEntriesToConfirm,
if (accessControlDialog.remember()) {
BrowserEntryConfig config;
config.load(entry);
- config.allow(host);
- if (!submitHost.isEmpty() && host != submitHost) {
- config.allow(submitHost);
+ config.allow(siteHost);
+ if (!formUrlStr.isEmpty() && siteHost != formUrlStr) {
+ config.allow(formUrlStr);
}
if (!realm.isEmpty()) {
config.setRealm(realm);
@@ -871,7 +859,7 @@ QJsonObject BrowserService::prepareEntry(const Entry* entry)
}
BrowserService::Access
-BrowserService::checkAccess(const Entry* entry, const QString& host, const QString& submitHost, const QString& realm)
+BrowserService::checkAccess(const Entry* entry, const QString& siteHost, const QString& formHost, const QString& realm)
{
if (entry->isExpired()) {
return browserSettings()->allowExpiredCredentials() ? Allowed : Denied;
@@ -881,10 +869,10 @@ BrowserService::checkAccess(const Entry* entry, const QString& host, const QStri
if (!config.load(entry)) {
return Unknown;
}
- if ((config.isAllowed(host)) && (submitHost.isEmpty() || config.isAllowed(submitHost))) {
+ if ((config.isAllowed(siteHost)) && (formHost.isEmpty() || config.isAllowed(formHost))) {
return Allowed;
}
- if ((config.isDenied(host)) || (!submitHost.isEmpty() && config.isDenied(submitHost))) {
+ if ((config.isDenied(siteHost)) || (!formHost.isEmpty() && config.isDenied(formHost))) {
return Denied;
}
if (!realm.isEmpty() && config.realm() != realm) {
@@ -919,66 +907,72 @@ Group* BrowserService::getDefaultEntryGroup(const QSharedPointer<Database>& sele
return group;
}
-int BrowserService::sortPriority(const Entry* entry,
- const QString& host,
- const QString& submitUrl,
- const QString& baseSubmitUrl,
- const QString& fullUrl) const
+// Returns the maximum sort priority given a set of match urls and the
+// extension provided site and form url.
+int BrowserService::sortPriority(const QStringList& urls, const QString& siteUrlStr, const QString& formUrlStr)
{
- QUrl url(entry->url());
- if (url.scheme().isEmpty()) {
- url.setScheme("https");
- }
+ QList<int> priorityList;
+ // NOTE: QUrl::matches is utterly broken in Qt < 5.11, so we work around that
+ // by removing parts of the url that we don't match and direct matching others
+ const auto stdOpts = QUrl::RemoveFragment | QUrl::RemoveUserInfo;
+ const auto siteUrl = QUrl(siteUrlStr).adjusted(stdOpts);
+ const auto formUrl = QUrl(formUrlStr).adjusted(stdOpts);
+
+ auto getPriority = [&](const QString& givenUrl) {
+ auto url = QUrl::fromUserInput(givenUrl).adjusted(stdOpts);
+
+ // Default to https scheme if undefined
+ if (url.scheme().isEmpty() || !givenUrl.contains("://")) {
+ url.setScheme("https");
+ }
- // Add the empty path to the URL if it's missing
- if (url.path().isEmpty() && !url.hasFragment() && !url.hasQuery()) {
- url.setPath("/");
- }
+ // Add the empty path to the URL if it's missing.
+ // URL's from the extension always have a path set, entry URL's can be without.
+ if (url.path().isEmpty() && !url.hasFragment() && !url.hasQuery()) {
+ url.setPath("/");
+ }
- const QString entryURL = url.toString(QUrl::StripTrailingSlash);
- const QString baseEntryURL =
- url.toString(QUrl::StripTrailingSlash | QUrl::RemovePath | QUrl::RemoveQuery | QUrl::RemoveFragment);
+ // Reject invalid urls and hosts, except 'localhost', and scheme mismatch
+ if (!url.isValid() || (!url.host().contains(".") && url.host() != "localhost")
+ || url.scheme() != siteUrl.scheme()) {
+ return 0;
+ }
- if (!url.host().contains(".") && url.host() != "localhost") {
+ // Exact match with site url or form url
+ if (url.matches(siteUrl, QUrl::None) || url.matches(formUrl, QUrl::None)) {
+ return 100;
+ }
+
+ // Exact match without the query string
+ if (url.matches(siteUrl, QUrl::RemoveQuery) || url.matches(formUrl, QUrl::RemoveQuery)) {
+ return 90;
+ }
+
+ // Match without path (ie, FQDN match), form url prioritizes lower than site url
+ if (url.host() == siteUrl.host()) {
+ return 80;
+ }
+ if (url.host() == formUrl.host()) {
+ return 70;
+ }
+
+ // Site/form url ends with given url (subdomain mismatch)
+ if (siteUrl.host().endsWith(url.host())) {
+ return 60;
+ }
+ if (formUrl.host().endsWith(url.host())) {
+ return 50;
+ }
+
+ // No valid match found
return 0;
+ };
+
+ for (const auto& entryUrl : urls) {
+ priorityList << getPriority(entryUrl);
}
- if (fullUrl == entryURL) {
- return 100;
- }
- if (submitUrl == entryURL) {
- return 95;
- }
- if (submitUrl.startsWith(entryURL) && entryURL != host && baseSubmitUrl != entryURL) {
- return 90;
- }
- if (submitUrl.startsWith(baseEntryURL) && entryURL != host && baseSubmitUrl != baseEntryURL) {
- return 80;
- }
- if (entryURL == host) {
- return 70;
- }
- if (entryURL == baseSubmitUrl) {
- return 60;
- }
- if (entryURL.startsWith(submitUrl)) {
- return 50;
- }
- if (entryURL.startsWith(baseSubmitUrl) && baseSubmitUrl != host) {
- return 40;
- }
- if (submitUrl.startsWith(entryURL)) {
- return 30;
- }
- if (submitUrl.startsWith(baseEntryURL)) {
- return 20;
- }
- if (entryURL.startsWith(host)) {
- return 10;
- }
- if (host.startsWith(entryURL)) {
- return 5;
- }
- return 0;
+
+ return *std::max_element(priorityList.begin(), priorityList.end());
}
bool BrowserService::schemeFound(const QString& url)
@@ -1015,7 +1009,7 @@ bool BrowserService::handleEntry(Entry* entry, const QString& url, const QString
return handleURL(entry->url(), url, submitUrl);
}
-bool BrowserService::handleURL(const QString& entryUrl, const QString& url, const QString& submitUrl)
+bool BrowserService::handleURL(const QString& entryUrl, const QString& siteUrlStr, const QString& formUrlStr)
{
if (entryUrl.isEmpty()) {
return false;
@@ -1033,8 +1027,8 @@ bool BrowserService::handleURL(const QString& entryUrl, const QString& url, cons
}
// Make a direct compare if a local file is used
- if (url.startsWith("file://")) {
- return entryUrl == submitUrl;
+ if (siteUrlStr.startsWith("file://")) {
+ return entryUrl == formUrlStr;
}
// URL host validation fails
@@ -1043,7 +1037,7 @@ bool BrowserService::handleURL(const QString& entryUrl, const QString& url, cons
}
// Match port, if used
- QUrl siteQUrl(url);
+ QUrl siteQUrl(siteUrlStr);
if (entryQUrl.port() > 0 && entryQUrl.port() != siteQUrl.port()) {
return false;
}
@@ -1067,17 +1061,7 @@ bool BrowserService::handleURL(const QString& entryUrl, const QString& url, cons
// Match the subdomains with the limited wildcard
if (siteQUrl.host().endsWith(entryQUrl.host())) {
- if (!browserSettings()->bestMatchOnly()) {
- return true;
- }
-
- // Match the exact subdomain and path, or start of the path when entry's path is longer than plain "/"
- if (siteQUrl.host() == entryQUrl.host()) {
- if (siteQUrl.path() == entryQUrl.path()
- || (entryQUrl.path().size() > 1 && siteQUrl.path().startsWith(entryQUrl.path()))) {
- return true;
- }
- }
+ return true;
}
return false;
@@ -1223,6 +1207,21 @@ bool BrowserService::checkLegacySettings(QSharedPointer<Database> db)
return dialogResult == MessageBox::Yes;
}
+QStringList BrowserService::getEntryURLs(const Entry* entry)
+{
+ QStringList urlList;
+ urlList << entry->url();
+
+ // Handle additional URL's
+ for (const auto& key : entry->attributes()->keys()) {
+ if (key.startsWith(ADDITIONAL_URL)) {
+ urlList << entry->attributes()->value(key);
+ }
+ }
+
+ return urlList;
+}
+
void BrowserService::hideWindow() const
{
if (m_prevWindowState == WindowState::Minimized) {
diff --git a/src/browser/BrowserService.h b/src/browser/BrowserService.h
index 8c8e3460f..2c7a7c95c 100644
--- a/src/browser/BrowserService.h
+++ b/src/browser/BrowserService.h
@@ -63,8 +63,8 @@ public:
void addEntry(const QString& dbid,
const QString& login,
const QString& password,
- const QString& url,
- const QString& submitUrl,
+ const QString& siteUrlStr,
+ const QString& formUrlStr,
const QString& realm,
const QString& group,
const QString& groupUuid,
@@ -73,12 +73,12 @@ public:
const QString& uuid,
const QString& login,
const QString& password,
- const QString& url,
- const QString& submitUrl);
+ const QString& siteUrlStr,
+ const QString& formUrlStr);
QJsonArray findMatchingEntries(const QString& dbid,
- const QString& url,
- const QString& submitUrl,
+ const QString& siteUrlStr,
+ const QString& formUrlStr,
const QString& realm,
const StringPairList& keyList,
const bool httpAuth = false);
@@ -118,36 +118,32 @@ private:
Hidden
};
- QList<Entry*> searchEntries(const QSharedPointer<Database>& db, const QString& url, const QString& submitUrl);
- QList<Entry*> searchEntries(const QString& url, const QString& submitUrl, const StringPairList& keyList);
QList<Entry*>
- sortEntries(QList<Entry*>& pwEntries, const QString& host, const QString& submitUrl, const QString& fullUrl);
+ searchEntries(const QSharedPointer<Database>& db, const QString& siteUrlStr, const QString& formUrlStr);
+ QList<Entry*> searchEntries(const QString& siteUrlStr, const QString& formUrlStr, const StringPairList& keyList);
+ QList<Entry*> sortEntries(QList<Entry*>& pwEntries, const QString& siteUrlStr, const QString& formUrlStr);
QList<Entry*> confirmEntries(QList<Entry*>& pwEntriesToConfirm,
- const QString& url,
- const QString& host,
- const QString& submitUrl,
+ const QString& siteUrlStr,
+ const QString& siteHost,
+ const QString& formUrlStr,
const QString& realm,
const bool httpAuth);
QJsonObject prepareEntry(const Entry* entry);
QJsonArray getChildrenFromGroup(Group* group);
- Access checkAccess(const Entry* entry, const QString& host, const QString& submitHost, const QString& realm);
+ Access checkAccess(const Entry* entry, const QString& siteHost, const QString& formHost, const QString& realm);
Group* getDefaultEntryGroup(const QSharedPointer<Database>& selectedDb = {});
- int sortPriority(const Entry* entry,
- const QString& host,
- const QString& submitUrl,
- const QString& baseSubmitUrl,
- const QString& fullUrl) const;
+ int sortPriority(const QStringList& urls, const QString& siteUrlStr, const QString& formUrlStr);
bool schemeFound(const QString& url);
bool removeFirstDomain(QString& hostname);
bool handleEntry(Entry* entry, const QString& url, const QString& submitUrl);
- bool handleURL(const QString& entryUrl, const QString& url, const QString& submitUrl);
+ bool handleURL(const QString& entryUrl, const QString& siteUrlStr, const QString& formUrlStr);
QString baseDomain(const QString& hostname) const;
QSharedPointer<Database> getDatabase();
QSharedPointer<Database> selectedDatabase();
QString getDatabaseRootUuid();
QString getDatabaseRecycleBinUuid();
-
bool checkLegacySettings(QSharedPointer<Database> db);
+ QStringList getEntryURLs(const Entry* entry);
void hideWindow() const;
void raiseWindow(const bool force = false);
diff --git a/src/cli/keepassxc-cli.cpp b/src/cli/keepassxc-cli.cpp
index a9b276fda..2f8294446 100644
--- a/src/cli/keepassxc-cli.cpp
+++ b/src/cli/keepassxc-cli.cpp
@@ -158,7 +158,7 @@ void enterInteractiveMode(const QStringList& arguments)
auto cmd = Commands::getCommand(args[0]);
if (!cmd) {
- err << QObject::tr("Unknown command %1").arg(args[0]) << "\n";
+ err << QObject::tr("Unknown command %1").arg(args[0]) << endl;
continue;
} else if (cmd->name == "quit" || cmd->name == "exit") {
break;
@@ -167,6 +167,7 @@ void enterInteractiveMode(const QStringList& arguments)
cmd->currentDatabase = currentDatabase;
cmd->execute(args);
currentDatabase = cmd->currentDatabase;
+ cmd->currentDatabase.reset();
}
if (currentDatabase) {
@@ -246,6 +247,10 @@ int main(int argc, char** argv)
arguments.removeFirst();
int exitCode = command->execute(arguments);
+ if (command->currentDatabase) {
+ command->currentDatabase.reset();
+ }
+
#if defined(WITH_ASAN) && defined(WITH_LSAN)
// do leak check here to prevent massive tail of end-of-process leak errors from third-party libraries
__lsan_do_leak_check();
diff --git a/src/core/Config.cpp b/src/core/Config.cpp
index e29b42d43..e9f5f1a77 100644
--- a/src/core/Config.cpp
+++ b/src/core/Config.cpp
@@ -22,6 +22,7 @@
#include <QCoreApplication>
#include <QDir>
#include <QHash>
+#include <QProcessEnvironment>
#include <QSettings>
#include <QSize>
#include <QStandardPaths>
@@ -419,49 +420,17 @@ void Config::migrate()
sync();
}
-Config::Config(const QString& fileName, QObject* parent)
+Config::Config(const QString& configFileName, const QString& localConfigFileName, QObject* parent)
: QObject(parent)
{
- init(fileName);
+ init(configFileName, localConfigFileName);
}
Config::Config(QObject* parent)
: QObject(parent)
{
- // Check if we are running in portable mode, if so store the config files local to the app
- auto portablePath = QCoreApplication::applicationDirPath().append("/%1");
- if (QFile::exists(portablePath.arg(".portable"))) {
- init(portablePath.arg("config/keepassxc.ini"), portablePath.arg("config/keepassxc_local.ini"));
- return;
- }
-
- QString configPath;
- QString localConfigPath;
-
-#if defined(Q_OS_WIN)
- configPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
- localConfigPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
-#elif defined(Q_OS_MACOS)
- configPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
- localConfigPath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
-#else
- // On case-sensitive Operating Systems, force use of lowercase app directories
- configPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/keepassxc";
- localConfigPath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/keepassxc";
-#endif
-
- configPath += "/keepassxc";
- localConfigPath += "/keepassxc";
-
-#ifdef QT_DEBUG
- configPath += "_debug";
- localConfigPath += "_debug";
-#endif
-
- configPath += ".ini";
- localConfigPath += ".ini";
-
- init(QDir::toNativeSeparators(configPath), QDir::toNativeSeparators(localConfigPath));
+ auto configFiles = defaultConfigFiles();
+ init(configFiles.first, configFiles.second);
}
Config::~Config()
@@ -489,6 +458,45 @@ void Config::init(const QString& configFileName, const QString& localConfigFileN
connect(qApp, &QCoreApplication::aboutToQuit, this, &Config::sync);
}
+QPair<QString, QString> Config::defaultConfigFiles()
+{
+ // Check if we are running in portable mode, if so store the config files local to the app
+ auto portablePath = QCoreApplication::applicationDirPath().append("/%1");
+ if (QFile::exists(portablePath.arg(".portable"))) {
+ return {portablePath.arg("config/keepassxc.ini"), portablePath.arg("config/keepassxc_local.ini")};
+ }
+
+ QString configPath;
+ QString localConfigPath;
+
+#if defined(Q_OS_WIN)
+ configPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
+ localConfigPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
+#elif defined(Q_OS_MACOS)
+ configPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
+ localConfigPath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
+#else
+ // On case-sensitive Operating Systems, force use of lowercase app directories
+ configPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/keepassxc";
+ localConfigPath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/keepassxc";
+#endif
+
+ QString suffix;
+#ifdef QT_DEBUG
+ suffix = "_debug";
+#endif
+
+ configPath += QString("/keepassxc%1.ini").arg(suffix);
+ localConfigPath += QString("/keepassxc%1.ini").arg(suffix);
+
+ // Allow overriding the default location with env vars
+ const auto& env = QProcessEnvironment::systemEnvironment();
+ configPath = env.value("KPXC_CONFIG", configPath);
+ localConfigPath = env.value("KPXC_CONFIG_LOCAL", localConfigPath);
+
+ return {QDir::toNativeSeparators(configPath), QDir::toNativeSeparators(localConfigPath)};
+}
+
Config* Config::instance()
{
if (!m_instance) {
@@ -498,12 +506,16 @@ Config* Config::instance()
return m_instance;
}
-void Config::createConfigFromFile(const QString& file)
+void Config::createConfigFromFile(const QString& configFileName, const QString& localConfigFileName)
{
if (m_instance) {
delete m_instance;
}
- m_instance = new Config(file, qApp);
+
+ auto defaultFiles = defaultConfigFiles();
+ m_instance = new Config(configFileName.isEmpty() ? defaultFiles.first : configFileName,
+ localConfigFileName.isEmpty() ? defaultFiles.second : localConfigFileName,
+ qApp);
}
void Config::createTempFileInstance()
@@ -515,7 +527,7 @@ void Config::createTempFileInstance()
bool openResult = tmpFile->open();
Q_ASSERT(openResult);
Q_UNUSED(openResult);
- m_instance = new Config(tmpFile->fileName(), qApp);
+ m_instance = new Config(tmpFile->fileName(), "", qApp);
tmpFile->setParent(m_instance);
}
diff --git a/src/core/Config.h b/src/core/Config.h
index 28a75082e..1c3f1e941 100644
--- a/src/core/Config.h
+++ b/src/core/Config.h
@@ -199,17 +199,18 @@ public:
void resetToDefaults();
static Config* instance();
- static void createConfigFromFile(const QString& file);
+ static void createConfigFromFile(const QString& configFileName, const QString& localConfigFileName = {});
static void createTempFileInstance();
signals:
void changed(ConfigKey key);
private:
- Config(const QString& fileName, QObject* parent = nullptr);
+ Config(const QString& configFileName, const QString& localConfigFileName, QObject* parent);
explicit Config(QObject* parent);
- void init(const QString& configFileName, const QString& localConfigFileName = "");
+ void init(const QString& configFileName, const QString& localConfigFileName);
void migrate();
+ static QPair<QString, QString> defaultConfigFiles();
static QPointer<Config> m_instance;
diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp
index d29e92bff..7e2b65bcd 100644
--- a/src/core/Tools.cpp
+++ b/src/core/Tools.cpp
@@ -263,7 +263,8 @@ namespace Tools
bool checkUrlValid(const QString& urlField)
{
- if (urlField.isEmpty() || urlField.startsWith("cmd://", Qt::CaseInsensitive)) {
+ if (urlField.isEmpty() || urlField.startsWith("cmd://", Qt::CaseInsensitive)
+ || urlField.startsWith("{REF:A", Qt::CaseInsensitive)) {
return true;
}
diff --git a/src/gui/DatabaseOpenDialog.cpp b/src/gui/DatabaseOpenDialog.cpp
index 5e6e41b7a..e7194b7e2 100644
--- a/src/gui/DatabaseOpenDialog.cpp
+++ b/src/gui/DatabaseOpenDialog.cpp
@@ -25,11 +25,7 @@ DatabaseOpenDialog::DatabaseOpenDialog(QWidget* parent)
, m_view(new DatabaseOpenWidget(this))
{
setWindowTitle(tr("Unlock Database - KeePassXC"));
-#ifdef Q_OS_MACOS
- setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
-#else
- setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint | Qt::ForeignWindow);
-#endif
+ setWindowFlags(Qt::Dialog | Qt::WindowStaysOnTopHint);
connect(m_view, SIGNAL(dialogFinished(bool)), this, SLOT(complete(bool)));
auto* layout = new QVBoxLayout();
layout->setMargin(0);
diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp
index 34fe4db72..2683cecec 100644
--- a/src/gui/DatabaseTabWidget.cpp
+++ b/src/gui/DatabaseTabWidget.cpp
@@ -275,7 +275,7 @@ void DatabaseTabWidget::importKeePass1Database()
void DatabaseTabWidget::importOpVaultDatabase()
{
-#ifdef Q_MACOS
+#ifdef Q_OS_MACOS
QString fileName = fileDialog()->getOpenFileName(this, tr("Open OPVault"), {}, "OPVault (*.opvault)");
#else
QString fileName = fileDialog()->getExistingDirectory(this, tr("Open OPVault"));
diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp
index d5ac7eb3e..5cfe26e03 100644
--- a/src/gui/DatabaseWidget.cpp
+++ b/src/gui/DatabaseWidget.cpp
@@ -2061,7 +2061,7 @@ void DatabaseWidget::processAutoOpen()
// negated using '!'
auto ifDevice = entry->attribute("IfDevice");
if (!ifDevice.isEmpty()) {
- bool loadDb = true;
+ bool loadDb = false;
auto hostName = QHostInfo::localHostName();
for (auto& device : ifDevice.split(",")) {
device = device.trimmed();
@@ -2070,12 +2070,13 @@ void DatabaseWidget::processAutoOpen()
// Machine name matched an exclusion, don't load this database
loadDb = false;
break;
+ } else {
+ // Not matching an exclusion allows loading on all machines
+ loadDb = true;
}
} else if (device.compare(hostName, Qt::CaseInsensitive) == 0) {
+ // Explicitly named for loading
loadDb = true;
- } else {
- // Don't load the database if there are devices not starting with '!'
- loadDb = false;
}
}
if (!loadDb) {
diff --git a/src/gui/EditWidget.cpp b/src/gui/EditWidget.cpp
index 68a8d7d4a..72742b5ac 100644
--- a/src/gui/EditWidget.cpp
+++ b/src/gui/EditWidget.cpp
@@ -59,12 +59,18 @@ void EditWidget::addPage(const QString& labelText, const QIcon& icon, QWidget* w
* from automatic resizing and it now should be able to fit into a user's monitor even if the monitor is only 768
* pixels high.
*/
- auto* scrollArea = new QScrollArea(m_ui->stackedWidget);
- scrollArea->setFrameShape(QFrame::NoFrame);
- scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
- scrollArea->setWidget(widget);
- scrollArea->setWidgetResizable(true);
- m_ui->stackedWidget->addWidget(scrollArea);
+ if (widget->inherits("QScrollArea")) {
+ m_ui->stackedWidget->addWidget(widget);
+ } else {
+ auto* scrollArea = new QScrollArea(m_ui->stackedWidget);
+ scrollArea->setFrameShape(QFrame::NoFrame);
+ scrollArea->setFrameShadow(QFrame::Plain);
+ scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+ scrollArea->setSizeAdjustPolicy(QScrollArea::AdjustToContents);
+ scrollArea->setWidgetResizable(true);
+ scrollArea->setWidget(widget);
+ m_ui->stackedWidget->addWidget(scrollArea);
+ }
m_ui->categoryList->addCategory(labelText, icon);
}
diff --git a/src/gui/EntryPreviewWidget.cpp b/src/gui/EntryPreviewWidget.cpp
index b873800a8..1dc05c3b7 100644
--- a/src/gui/EntryPreviewWidget.cpp
+++ b/src/gui/EntryPreviewWidget.cpp
@@ -50,7 +50,6 @@ EntryPreviewWidget::EntryPreviewWidget(QWidget* parent)
// Entry
m_ui->entryTotpButton->setIcon(resources()->icon("chronometer"));
m_ui->entryCloseButton->setIcon(resources()->icon("dialog-close"));
- m_ui->entryPasswordLabel->setFont(Font::fixedFont());
m_ui->togglePasswordButton->setIcon(resources()->onOffIcon("password-show"));
m_ui->toggleEntryNotesButton->setIcon(resources()->onOffIcon("password-show"));
m_ui->toggleGroupNotesButton->setIcon(resources()->onOffIcon("password-show"));
@@ -194,6 +193,7 @@ void EntryPreviewWidget::setPasswordVisible(bool state)
if (state) {
m_ui->entryPasswordLabel->setText(password);
m_ui->entryPasswordLabel->setCursorPosition(0);
+ m_ui->entryPasswordLabel->setFont(Font::fixedFont());
} else if (password.isEmpty() && !config()->get(Config::Security_PasswordEmptyPlaceholder).toBool()) {
m_ui->entryPasswordLabel->setText("");
} else {
diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp
index 64fbd9345..b0be4b55b 100644
--- a/src/gui/MainWindow.cpp
+++ b/src/gui/MainWindow.cpp
@@ -1274,6 +1274,8 @@ bool MainWindow::saveLastDatabases()
void MainWindow::updateTrayIcon()
{
if (isTrayIconEnabled()) {
+ QApplication::setQuitOnLastWindowClosed(false);
+
if (!m_trayIcon) {
m_trayIcon = new QSystemTrayIcon(this);
auto* menu = new QMenu(this);
@@ -1312,6 +1314,8 @@ void MainWindow::updateTrayIcon()
m_trayIcon->setIcon(resources()->trayIconLocked());
}
} else {
+ QApplication::setQuitOnLastWindowClosed(true);
+
if (m_trayIcon) {
m_trayIcon->hide();
delete m_trayIcon;
diff --git a/src/gui/PasswordGeneratorWidget.cpp b/src/gui/PasswordGeneratorWidget.cpp
index f76e3e3cf..5cb671144 100644
--- a/src/gui/PasswordGeneratorWidget.cpp
+++ b/src/gui/PasswordGeneratorWidget.cpp
@@ -170,6 +170,7 @@ void PasswordGeneratorWidget::loadSettings()
// Set advanced mode
m_ui->buttonAdvancedMode->setChecked(advanced);
setAdvancedMode(advanced);
+ updateGenerator();
}
void PasswordGeneratorWidget::saveSettings()
diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp
index 32482a9dd..84b230c20 100644
--- a/src/gui/entry/EditEntryWidget.cpp
+++ b/src/gui/entry/EditEntryWidget.cpp
@@ -76,7 +76,7 @@ EditEntryWidget::EditEntryWidget(QWidget* parent)
, m_historyUi(new Ui::EditEntryWidgetHistory())
, m_browserUi(new Ui::EditEntryWidgetBrowser())
, m_customData(new CustomData())
- , m_mainWidget(new QWidget())
+ , m_mainWidget(new QScrollArea())
, m_advancedWidget(new QWidget())
, m_iconsWidget(new EditWidgetIcons())
, m_autoTypeWidget(new QWidget())
@@ -178,6 +178,9 @@ void EditEntryWidget::setupMain()
m_mainUi->expirePresets->setMenu(createPresetsMenu());
connect(m_mainUi->expirePresets->menu(), SIGNAL(triggered(QAction*)), this, SLOT(useExpiryPreset(QAction*)));
+
+ // HACK: Align username text with other line edits. Qt does not let you do this with an application stylesheet.
+ m_mainUi->usernameComboBox->lineEdit()->setStyleSheet("padding-left: 8px;");
}
void EditEntryWidget::setupAdvanced()
diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h
index 3d1835396..e359d1029 100644
--- a/src/gui/entry/EditEntryWidget.h
+++ b/src/gui/entry/EditEntryWidget.h
@@ -24,6 +24,7 @@
#include <QModelIndex>
#include <QPointer>
#include <QScopedPointer>
+#include <QScrollArea>
#include <QTimer>
#include "config-keepassx.h"
@@ -174,7 +175,7 @@ private:
const QScopedPointer<Ui::EditEntryWidgetBrowser> m_browserUi;
const QScopedPointer<CustomData> m_customData;
- QWidget* const m_mainWidget;
+ QScrollArea* const m_mainWidget;
QWidget* const m_advancedWidget;
EditWidgetIcons* const m_iconsWidget;
QWidget* const m_autoTypeWidget;
diff --git a/src/gui/entry/EditEntryWidgetMain.ui b/src/gui/entry/EditEntryWidgetMain.ui
index f96481a3f..183ca0388 100644
--- a/src/gui/entry/EditEntryWidgetMain.ui
+++ b/src/gui/entry/EditEntryWidgetMain.ui
@@ -1,278 +1,306 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<ui version="4.0">
- <class>EditEntryWidgetMain</class>
- <widget class="QWidget" name="EditEntryWidgetMain">
- <property name="geometry">
- <rect>
- <x>0</x>
- <y>0</y>
- <width>496</width>
- <height>420</height>
- </rect>
- </property>
- <layout class="QGridLayout" name="gridLayout">
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <property name="horizontalSpacing">
- <number>10</number>
- </property>
- <property name="verticalSpacing">
- <number>8</number>
- </property>
- <item row="6" column="1">
- <layout class="QVBoxLayout" name="verticalLayout_2">
- <item>
- <widget class="QPlainTextEdit" name="notesEdit">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
- <horstretch>0</horstretch>
- <verstretch>1</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>100</height>
- </size>
- </property>
- <property name="accessibleName">
- <string>Notes field</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="notesHint">
- <property name="visible">
- <bool>true</bool>
- </property>
- <property name="text">
- <string>Toggle the checkbox to reveal the notes section.</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignTop</set>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item row="1" column="1">
- <widget class="QComboBox" name="usernameComboBox">
- <property name="accessibleName">
- <string>Username field</string>
- </property>
- </widget>
- </item>
- <item row="6" column="0">
- <layout class="QVBoxLayout" name="verticalLayout">
- <item>
- <widget class="QCheckBox" name="notesEnabled">
- <property name="toolTip">
- <string>Toggle notes visible</string>
- </property>
- <property name="accessibleName">
- <string>Toggle notes visible</string>
- </property>
- <property name="text">
- <string>Notes:</string>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="verticalSpacer">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>40</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
- </item>
- <item row="5" column="1">
- <layout class="QHBoxLayout" name="horizontalLayout_2">
- <property name="spacing">
- <number>8</number>
- </property>
- <item>
- <widget class="QDateTimeEdit" name="expireDatePicker">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="accessibleName">
- <string>Expiration field</string>
- </property>
- <property name="calendarPopup">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="expirePresets">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="toolTip">
- <string>Expiration Presets</string>
- </property>
- <property name="accessibleName">
- <string>Expiration presets</string>
- </property>
- <property name="text">
- <string>Presets</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item row="2" column="0">
- <widget class="QLabel" name="passwordLabel">
- <property name="text">
- <string>Password:</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- </widget>
- </item>
- <item row="3" column="0">
- <widget class="QLabel" name="urlLabel">
- <property name="text">
- <string>URL:</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- </widget>
- </item>
- <item row="3" column="1">
- <layout class="QHBoxLayout" name="horizontalLayout_6">
- <property name="spacing">
- <number>8</number>
- </property>
- <item>
- <widget class="URLEdit" name="urlEdit">
- <property name="accessibleName">
- <string>Url field</string>
- </property>
- <property name="placeholderText">
- <string>https://example.com</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QToolButton" name="fetchFaviconButton">
- <property name="toolTip">
- <string>Download favicon for URL</string>
- </property>
- <property name="accessibleName">
- <string>Download favicon for URL</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item row="0" column="0">
- <widget class="QLabel" name="titleLabel">
- <property name="text">
- <string>Title:</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- </widget>
- </item>
- <item row="0" column="1">
- <widget class="QLineEdit" name="titleEdit">
- <property name="accessibleName">
- <string>Title field</string>
- </property>
- </widget>
- </item>
- <item row="1" column="0">
- <widget class="QLabel" name="usernameLabel">
- <property name="text">
- <string>Username:</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- </widget>
- </item>
- <item row="2" column="1">
- <widget class="PasswordEdit" name="passwordEdit">
- <property name="accessibleName">
- <string>Password field</string>
- </property>
- <property name="echoMode">
- <enum>QLineEdit::Password</enum>
- </property>
- </widget>
- </item>
- <item row="5" column="0">
- <layout class="QHBoxLayout" name="horizontalLayout">
- <property name="spacing">
- <number>0</number>
- </property>
- <item>
- <widget class="QCheckBox" name="expireCheck">
- <property name="toolTip">
- <string>Toggle expiration</string>
- </property>
- <property name="accessibleName">
- <string>Toggle expiration</string>
- </property>
- <property name="text">
- <string>Expires:</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- <customwidgets>
- <customwidget>
- <class>PasswordEdit</class>
- <extends>QLineEdit</extends>
- <header>gui/PasswordEdit.h</header>
- <container>1</container>
- </customwidget>
- <customwidget>
- <class>URLEdit</class>
- <extends>QLineEdit</extends>
- <header>gui/URLEdit.h</header>
- <container>1</container>
- </customwidget>
- </customwidgets>
- <tabstops>
- <tabstop>titleEdit</tabstop>
- <tabstop>usernameComboBox</tabstop>
- <tabstop>passwordEdit</tabstop>
- <tabstop>urlEdit</tabstop>
- <tabstop>fetchFaviconButton</tabstop>
- <tabstop>expireCheck</tabstop>
- <tabstop>expireDatePicker</tabstop>
- <tabstop>expirePresets</tabstop>
- <tabstop>notesEnabled</tabstop>
- <tabstop>notesEdit</tabstop>
- </tabstops>
- <resources/>
- <connections/>
-</ui>
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>EditEntryWidgetMain</class>
+ <widget class="QScrollArea" name="EditEntryWidgetMain">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>539</width>
+ <height>523</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Edit Entry</string>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Plain</enum>
+ </property>
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOff</enum>
+ </property>
+ <property name="sizeAdjustPolicy">
+ <enum>QAbstractScrollArea::AdjustToContents</enum>
+ </property>
+ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+ <widget class="QWidget" name="container">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>539</width>
+ <height>523</height>
+ </rect>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <property name="horizontalSpacing">
+ <number>10</number>
+ </property>
+ <property name="verticalSpacing">
+ <number>8</number>
+ </property>
+ <item row="6" column="1">
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QPlainTextEdit" name="notesEdit">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>1</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>100</height>
+ </size>
+ </property>
+ <property name="accessibleName">
+ <string>Notes field</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="notesHint">
+ <property name="visible">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>Toggle the checkbox to reveal the notes section.</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignTop</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="1" column="1">
+ <widget class="QComboBox" name="usernameComboBox">
+ <property name="accessibleName">
+ <string>Username field</string>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="0">
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QCheckBox" name="notesEnabled">
+ <property name="toolTip">
+ <string>Toggle notes visible</string>
+ </property>
+ <property name="accessibleName">
+ <string>Toggle notes visible</string>
+ </property>
+ <property name="text">
+ <string>Notes:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item row="5" column="1">
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <property name="spacing">
+ <number>8</number>
+ </property>
+ <item>
+ <widget class="QDateTimeEdit" name="expireDatePicker">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="accessibleName">
+ <string>Expiration field</string>
+ </property>
+ <property name="calendarPopup">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="expirePresets">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="toolTip">
+ <string>Expiration Presets</string>
+ </property>
+ <property name="accessibleName">
+ <string>Expiration presets</string>
+ </property>
+ <property name="text">
+ <string>Presets</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="passwordLabel">
+ <property name="text">
+ <string>Password:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="urlLabel">
+ <property name="text">
+ <string>URL:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <layout class="QHBoxLayout" name="horizontalLayout_6">
+ <property name="spacing">
+ <number>8</number>
+ </property>
+ <item>
+ <widget class="URLEdit" name="urlEdit">
+ <property name="accessibleName">
+ <string>Url field</string>
+ </property>
+ <property name="placeholderText">
+ <string>https://example.com</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QToolButton" name="fetchFaviconButton">
+ <property name="toolTip">
+ <string>Download favicon for URL</string>
+ </property>
+ <property name="accessibleName">
+ <string>Download favicon for URL</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="titleLabel">
+ <property name="text">
+ <string>Title:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="titleEdit">
+ <property name="accessibleName">
+ <string>Title field</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="usernameLabel">
+ <property name="text">
+ <string>Username:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="PasswordEdit" name="passwordEdit">
+ <property name="accessibleName">
+ <string>Password field</string>
+ </property>
+ <property name="echoMode">
+ <enum>QLineEdit::Password</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="0">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QCheckBox" name="expireCheck">
+ <property name="toolTip">
+ <string>Toggle expiration</string>
+ </property>
+ <property name="accessibleName">
+ <string>Toggle expiration</string>
+ </property>
+ <property name="text">
+ <string>Expires:</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>PasswordEdit</class>
+ <extends>QLineEdit</extends>
+ <header>gui/PasswordEdit.h</header>
+ <container>1</container>
+ </customwidget>
+ <customwidget>
+ <class>URLEdit</class>
+ <extends>QLineEdit</extends>
+ <header>gui/URLEdit.h</header>
+ <container>1</container>
+ </customwidget>
+ </customwidgets>
+ <tabstops>
+ <tabstop>titleEdit</tabstop>
+ <tabstop>usernameComboBox</tabstop>
+ <tabstop>passwordEdit</tabstop>
+ <tabstop>urlEdit</tabstop>
+ <tabstop>fetchFaviconButton</tabstop>
+ <tabstop>expireCheck</tabstop>
+ <tabstop>expireDatePicker</tabstop>
+ <tabstop>expirePresets</tabstop>
+ <tabstop>notesEnabled</tabstop>
+ <tabstop>notesEdit</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/gui/entry/EntryView.cpp b/src/gui/entry/EntryView.cpp
index 18a69687d..53f03b989 100644
--- a/src/gui/entry/EntryView.cpp
+++ b/src/gui/entry/EntryView.cpp
@@ -109,21 +109,12 @@ EntryView::EntryView(QWidget* parent)
header()->setContextMenuPolicy(Qt::CustomContextMenu);
connect(header(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(showHeaderMenu(QPoint)));
- // clang-format off
- connect(header(), SIGNAL(sectionCountChanged(int,int)), SIGNAL(viewStateChanged()));
- // clang-format on
-
- // clang-format off
- connect(header(), SIGNAL(sectionMoved(int,int,int)), SIGNAL(viewStateChanged()));
- // clang-format on
-
- // clang-format off
- connect(header(), SIGNAL(sectionResized(int,int,int)), SIGNAL(viewStateChanged()));
- // clang-format on
+ connect(header(), SIGNAL(sectionCountChanged(int, int)), SIGNAL(viewStateChanged()));
+ connect(header(), SIGNAL(sectionMoved(int, int, int)), SIGNAL(viewStateChanged()));
+ connect(header(), SIGNAL(sectionResized(int, int, int)), SIGNAL(viewStateChanged()));
+ connect(header(), SIGNAL(sortIndicatorChanged(int, Qt::SortOrder)), SLOT(sortIndicatorChanged(int, Qt::SortOrder)));
// clang-format off
- connect(header(), SIGNAL(sortIndicatorChanged(int,Qt::SortOrder)), SLOT(sortIndicatorChanged(int,Qt::SortOrder)));
- // clang-format on
}
void EntryView::contextMenuShortcutPressed()
@@ -358,6 +349,8 @@ QByteArray EntryView::viewState() const
*/
bool EntryView::setViewState(const QByteArray& state)
{
+ // Reset to unsorted first (https://bugreports.qt.io/browse/QTBUG-86694)
+ header()->setSortIndicator(-1, Qt::AscendingOrder);
bool status = header()->restoreState(state);
resetFixedColumns();
m_columnsNeedRelayout = state.isEmpty();
@@ -379,8 +372,7 @@ void EntryView::showHeaderMenu(const QPoint& position)
continue;
}
int columnIndex = action->data().toInt();
- bool hidden = header()->isSectionHidden(columnIndex) || (header()->sectionSize(columnIndex) == 0);
- action->setChecked(!hidden);
+ action->setChecked(!isColumnHidden(columnIndex));
}
m_headerMenu->popup(mapToGlobal(position));
@@ -408,6 +400,7 @@ void EntryView::toggleColumnVisibility(QAction* action)
if (header()->sectionSize(columnIndex) == 0) {
header()->resizeSection(columnIndex, header()->defaultSectionSize());
}
+ resetFixedColumns();
return;
}
if ((header()->count() - header()->hiddenSectionCount()) > 1) {
@@ -460,11 +453,15 @@ void EntryView::fitColumnsToContents()
*/
void EntryView::resetFixedColumns()
{
- header()->setSectionResizeMode(EntryModel::Paperclip, QHeaderView::Fixed);
- header()->resizeSection(EntryModel::Paperclip, header()->minimumSectionSize());
+ if (!isColumnHidden(EntryModel::Paperclip)) {
+ header()->setSectionResizeMode(EntryModel::Paperclip, QHeaderView::Fixed);
+ header()->resizeSection(EntryModel::Paperclip, header()->minimumSectionSize());
+ }
- header()->setSectionResizeMode(EntryModel::Totp, QHeaderView::Fixed);
- header()->resizeSection(EntryModel::Totp, header()->minimumSectionSize());
+ if (!isColumnHidden(EntryModel::Totp)) {
+ header()->setSectionResizeMode(EntryModel::Totp, QHeaderView::Fixed);
+ header()->resizeSection(EntryModel::Totp, header()->minimumSectionSize());
+ }
}
/**
@@ -533,3 +530,8 @@ void EntryView::showEvent(QShowEvent* event)
m_columnsNeedRelayout = false;
}
}
+
+bool EntryView::isColumnHidden(int logicalIndex)
+{
+ return header()->isSectionHidden(logicalIndex) || header()->sectionSize(logicalIndex) == 0;
+}
diff --git a/src/gui/entry/EntryView.h b/src/gui/entry/EntryView.h
index e32aa4729..65cbf104a 100644
--- a/src/gui/entry/EntryView.h
+++ b/src/gui/entry/EntryView.h
@@ -80,6 +80,7 @@ private slots:
private:
void resetFixedColumns();
+ bool isColumnHidden(int logicalIndex);
EntryModel* const m_model;
SortFilterHideProxyModel* const m_sortModel;
diff --git a/src/gui/group/EditGroupWidget.cpp b/src/gui/group/EditGroupWidget.cpp
index b77e49864..ba79cce18 100644
--- a/src/gui/group/EditGroupWidget.cpp
+++ b/src/gui/group/EditGroupWidget.cpp
@@ -62,7 +62,7 @@ private:
EditGroupWidget::EditGroupWidget(QWidget* parent)
: EditWidget(parent)
, m_mainUi(new Ui::EditGroupWidgetMain())
- , m_editGroupWidgetMain(new QWidget())
+ , m_editGroupWidgetMain(new QScrollArea())
, m_editGroupWidgetIcons(new EditWidgetIcons())
, m_editWidgetProperties(new EditWidgetProperties())
, m_group(nullptr)
diff --git a/src/gui/group/EditGroupWidget.h b/src/gui/group/EditGroupWidget.h
index cc8738d8c..ed1bb0179 100644
--- a/src/gui/group/EditGroupWidget.h
+++ b/src/gui/group/EditGroupWidget.h
@@ -20,6 +20,7 @@
#include <QComboBox>
#include <QScopedPointer>
+#include <QScrollArea>
#include "core/Group.h"
#include "gui/EditWidget.h"
@@ -78,7 +79,7 @@ private:
const QScopedPointer<Ui::EditGroupWidgetMain> m_mainUi;
- QPointer<QWidget> m_editGroupWidgetMain;
+ QPointer<QScrollArea> m_editGroupWidgetMain;
QPointer<EditWidgetIcons> m_editGroupWidgetIcons;
QPointer<EditWidgetProperties> m_editWidgetProperties;
diff --git a/src/gui/group/EditGroupWidgetMain.ui b/src/gui/group/EditGroupWidgetMain.ui
index 9531cc847..faa8a30ff 100644
--- a/src/gui/group/EditGroupWidgetMain.ui
+++ b/src/gui/group/EditGroupWidgetMain.ui
@@ -1,215 +1,243 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EditGroupWidgetMain</class>
- <widget class="QWidget" name="EditGroupWidgetMain">
+ <widget class="QScrollArea" name="EditGroupWidgetMain">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
- <width>410</width>
- <height>430</height>
+ <width>539</width>
+ <height>523</height>
</rect>
</property>
- <layout class="QGridLayout" name="gridLayout" rowstretch="0,0,0,0,0,0,0,0,0,1" rowminimumheight="0,0,0,0,0,0,0,0,0,1">
- <property name="leftMargin">
- <number>0</number>
- </property>
- <property name="topMargin">
- <number>0</number>
- </property>
- <property name="rightMargin">
- <number>0</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
- </property>
- <property name="horizontalSpacing">
- <number>10</number>
- </property>
- <property name="verticalSpacing">
- <number>8</number>
+ <property name="windowTitle">
+ <string>Edit Group</string>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Plain</enum>
+ </property>
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOff</enum>
+ </property>
+ <property name="sizeAdjustPolicy">
+ <enum>QAbstractScrollArea::AdjustToContents</enum>
+ </property>
+ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+ <widget class="QWidget" name="container">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>539</width>
+ <height>523</height>
+ </rect>
</property>
- <item row="3" column="0">
- <widget class="QCheckBox" name="expireCheck">
- <property name="accessibleName">
- <string>Toggle expiration</string>
- </property>
- <property name="text">
- <string>Expires:</string>
- </property>
- </widget>
- </item>
- <item row="0" column="1">
- <widget class="QLineEdit" name="editName">
- <property name="accessibleName">
- <string>Name field</string>
- </property>
- </widget>
- </item>
- <item row="3" column="1">
- <widget class="QDateTimeEdit" name="expireDatePicker">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="accessibleName">
- <string>Expiration field</string>
- </property>
- <property name="calendarPopup">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item row="6" column="1">
- <widget class="QRadioButton" name="autoTypeSequenceInherit">
- <property name="text">
- <string>Use default Auto-Type sequence of parent group</string>
- </property>
- </widget>
- </item>
- <item row="5" column="0">
- <widget class="QLabel" name="autotypeLabel">
- <property name="text">
- <string>Auto-Type:</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- </widget>
- </item>
- <item row="4" column="0">
- <widget class="QLabel" name="searchLabel">
- <property name="text">
- <string>Search:</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- </widget>
- </item>
- <item row="5" column="1">
- <widget class="QComboBox" name="autotypeComboBox">
- <property name="accessibleName">
- <string>Auto-Type toggle for this and sub groups</string>
- </property>
- </widget>
- </item>
- <item row="1" column="0">
- <layout class="QVBoxLayout" name="verticalLayout">
- <item>
- <widget class="QLabel" name="labelNotes">
- <property name="text">
- <string>Notes:</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="verticalSpacer_2">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>0</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
- </item>
- <item row="8" column="1">
- <layout class="QHBoxLayout" name="horizontalLayout_2">
- <item>
- <spacer name="horizontalSpacer_2">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeType">
- <enum>QSizePolicy::Fixed</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>30</width>
- <height>0</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QLineEdit" name="autoTypeSequenceCustomEdit">
- <property name="enabled">
- <bool>false</bool>
- </property>
- <property name="accessibleName">
- <string>Default auto-type sequence field</string>
- </property>
- <property name="accessibleDescription">
- <string/>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item row="1" column="1">
- <widget class="QPlainTextEdit" name="editNotes">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>120</height>
- </size>
- </property>
- <property name="accessibleName">
- <string>Notes field</string>
- </property>
- </widget>
- </item>
- <item row="0" column="0">
- <widget class="QLabel" name="labelName">
- <property name="text">
- <string>Name:</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
- </property>
- </widget>
- </item>
- <item row="7" column="1">
- <widget class="QRadioButton" name="autoTypeSequenceCustomRadio">
- <property name="text">
- <string>Set default Auto-Type sequence</string>
- </property>
- </widget>
- </item>
- <item row="4" column="1">
- <widget class="QComboBox" name="searchComboBox">
- <property name="accessibleName">
- <string>Search toggle for this and sub groups</string>
- </property>
- </widget>
- </item>
- <item row="9" column="0">
- <spacer name="verticalSpacer_4">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>40</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
+ <layout class="QGridLayout" name="gridLayout" rowstretch="0,0,0,0,0,0,0,0,0,1" rowminimumheight="0,0,0,0,0,0,0,0,0,1">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <property name="horizontalSpacing">
+ <number>10</number>
+ </property>
+ <property name="verticalSpacing">
+ <number>8</number>
+ </property>
+ <item row="3" column="0">
+ <widget class="QCheckBox" name="expireCheck">
+ <property name="accessibleName">
+ <string>Toggle expiration</string>
+ </property>
+ <property name="text">
+ <string>Expires:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="editName">
+ <property name="accessibleName">
+ <string>Name field</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QDateTimeEdit" name="expireDatePicker">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="accessibleName">
+ <string>Expiration field</string>
+ </property>
+ <property name="calendarPopup">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="1">
+ <widget class="QRadioButton" name="autoTypeSequenceInherit">
+ <property name="text">
+ <string>Use default Auto-Type sequence of parent group</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="0">
+ <widget class="QLabel" name="autotypeLabel">
+ <property name="text">
+ <string>Auto-Type:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="0">
+ <widget class="QLabel" name="searchLabel">
+ <property name="text">
+ <string>Search:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="1">
+ <widget class="QComboBox" name="autotypeComboBox">
+ <property name="accessibleName">
+ <string>Auto-Type toggle for this and sub groups</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="labelNotes">
+ <property name="text">
+ <string>Notes:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item row="8" column="1">
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>30</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="autoTypeSequenceCustomEdit">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="accessibleName">
+ <string>Default auto-type sequence field</string>
+ </property>
+ <property name="accessibleDescription">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="1" column="1">
+ <widget class="QPlainTextEdit" name="editNotes">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>120</height>
+ </size>
+ </property>
+ <property name="accessibleName">
+ <string>Notes field</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="labelName">
+ <property name="text">
+ <string>Name:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="7" column="1">
+ <widget class="QRadioButton" name="autoTypeSequenceCustomRadio">
+ <property name="text">
+ <string>Set default Auto-Type sequence</string>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="1">
+ <widget class="QComboBox" name="searchComboBox">
+ <property name="accessibleName">
+ <string>Search toggle for this and sub groups</string>
+ </property>
+ </widget>
+ </item>
+ <item row="9" column="0">
+ <spacer name="verticalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
</widget>
<tabstops>
<tabstop>editName</tabstop>
diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp
index 5f502b16b..bc42b1e01 100644
--- a/src/gui/reports/ReportsWidgetHealthcheck.cpp
+++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp
@@ -76,6 +76,27 @@ namespace
QList<QSharedPointer<Item>> m_items;
bool m_anyKnownBad = false;
};
+
+ class ReportSortProxyModel : public QSortFilterProxyModel
+ {
+ public:
+ ReportSortProxyModel(QObject* parent)
+ : QSortFilterProxyModel(parent){};
+ ~ReportSortProxyModel() override = default;
+
+ protected:
+ bool lessThan(const QModelIndex& left, const QModelIndex& right) const override
+ {
+ // Check if the display data is a number, convert and compare if so
+ bool ok = false;
+ int leftInt = sourceModel()->data(left).toString().toInt(&ok);
+ if (ok) {
+ return leftInt < sourceModel()->data(right).toString().toInt();
+ }
+ // Otherwise use default sorting
+ return QSortFilterProxyModel::lessThan(left, right);
+ }
+ };
} // namespace
Health::Health(QSharedPointer<Database> db)
@@ -121,11 +142,12 @@ ReportsWidgetHealthcheck::ReportsWidgetHealthcheck(QWidget* parent)
, m_ui(new Ui::ReportsWidgetHealthcheck())
, m_errorIcon(Resources::instance()->icon("dialog-error"))
, m_referencesModel(new QStandardItemModel(this))
- , m_modelProxy(new QSortFilterProxyModel(this))
+ , m_modelProxy(new ReportSortProxyModel(this))
{
m_ui->setupUi(this);
m_modelProxy->setSourceModel(m_referencesModel.data());
+ m_modelProxy->setSortLocaleAware(true);
m_ui->healthcheckTableView->setModel(m_modelProxy.data());
m_ui->healthcheckTableView->setSelectionMode(QAbstractItemView::NoSelection);
m_ui->healthcheckTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
@@ -256,6 +278,7 @@ void ReportsWidgetHealthcheck::calculateHealth()
} else {
m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("") << tr("Title") << tr("Path") << tr("Score")
<< tr("Reason"));
+ m_ui->healthcheckTableView->sortByColumn(0, Qt::AscendingOrder);
}
m_ui->healthcheckTableView->resizeRowsToContents();
diff --git a/src/gui/reports/ReportsWidgetHibp.cpp b/src/gui/reports/ReportsWidgetHibp.cpp
index 48e36518d..406c465b9 100644
--- a/src/gui/reports/ReportsWidgetHibp.cpp
+++ b/src/gui/reports/ReportsWidgetHibp.cpp
@@ -45,17 +45,38 @@ namespace
return entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
&& entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR;
}
+
+ class ReportSortProxyModel : public QSortFilterProxyModel
+ {
+ public:
+ ReportSortProxyModel(QObject* parent)
+ : QSortFilterProxyModel(parent){};
+ ~ReportSortProxyModel() override = default;
+
+ protected:
+ bool lessThan(const QModelIndex& left, const QModelIndex& right) const override
+ {
+ // Sort count column by user data
+ if (left.column() == 2) {
+ return sourceModel()->data(left, Qt::UserRole).toInt()
+ < sourceModel()->data(right, Qt::UserRole).toInt();
+ }
+ // Otherwise use default sorting
+ return QSortFilterProxyModel::lessThan(left, right);
+ }
+ };
} // namespace
ReportsWidgetHibp::ReportsWidgetHibp(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::ReportsWidgetHibp())
, m_referencesModel(new QStandardItemModel(this))
- , m_modelProxy(new QSortFilterProxyModel(this))
+ , m_modelProxy(new ReportSortProxyModel(this))
{
m_ui->setupUi(this);
m_modelProxy->setSourceModel(m_referencesModel.data());
+ m_modelProxy->setSortLocaleAware(true);
m_ui->hibpTableView->setModel(m_modelProxy.data());
m_ui->hibpTableView->setSelectionMode(QAbstractItemView::NoSelection);
m_ui->hibpTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
@@ -167,6 +188,7 @@ void ReportsWidgetHibp::makeHibpTable()
}
row[2]->setForeground(red);
+ row[2]->setData(count, Qt::UserRole);
m_referencesModel->appendRow(row);
// Store entry pointer per table row (used in double click handler)
@@ -198,6 +220,7 @@ void ReportsWidgetHibp::makeHibpTable()
}
m_ui->hibpTableView->resizeRowsToContents();
+ m_ui->hibpTableView->sortByColumn(2, Qt::DescendingOrder);
m_ui->stackedWidget->setCurrentIndex(1);
}
diff --git a/src/gui/styles/base/basestyle.qss b/src/gui/styles/base/basestyle.qss
index 012c7cd0e..ff5d915bb 100644
--- a/src/gui/styles/base/basestyle.qss
+++ b/src/gui/styles/base/basestyle.qss
@@ -64,3 +64,8 @@ DatabaseWidget #SearchBanner, DatabaseWidget #KeeShareBanner {
border: 1px solid palette(dark);
padding: 2px;
}
+
+QPlainTextEdit, QTextEdit {
+ background-color: palette(base);
+ padding-left: 4px;
+}
diff --git a/src/main.cpp b/src/main.cpp
index 7e340da4d..b88dc41e0 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -56,51 +56,47 @@ int main(int argc, char** argv)
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
#endif
- Application app(argc, argv);
- Application::setApplicationName("KeePassXC");
- Application::setApplicationVersion(KEEPASSXC_VERSION);
- app.setProperty("KPXC_QUALIFIED_APPNAME", "org.keepassxc.KeePassXC");
- app.applyTheme();
-#if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)
- QGuiApplication::setDesktopFileName(app.property("KPXC_QUALIFIED_APPNAME").toString() + QStringLiteral(".desktop"));
-#endif
-
- // don't set organizationName as that changes the return value of
- // QStandardPaths::writableLocation(QDesktopServices::DataLocation)
- Bootstrap::bootstrapApplication();
-
QCommandLineParser parser;
parser.setApplicationDescription(QObject::tr("KeePassXC - cross-platform password manager"));
parser.addPositionalArgument(
- "filename", QObject::tr("filenames of the password databases to open (*.kdbx)"), "[filename(s)]");
+ "filename(s)", QObject::tr("filenames of the password databases to open (*.kdbx)"), "[filename(s)]");
QCommandLineOption configOption("config", QObject::tr("path to a custom config file"), "config");
+ QCommandLineOption localConfigOption(
+ "localconfig", QObject::tr("path to a custom local config file"), "localconfig");
QCommandLineOption keyfileOption("keyfile", QObject::tr("key file of the database"), "keyfile");
QCommandLineOption pwstdinOption("pw-stdin", QObject::tr("read password of the database from stdin"));
- // This is needed under Windows where clients send --parent-window parameter with Native Messaging connect method
- QCommandLineOption parentWindowOption(QStringList() << "pw"
- << "parent-window",
- QObject::tr("Parent window handle"),
- "handle");
QCommandLineOption helpOption = parser.addHelpOption();
QCommandLineOption versionOption = parser.addVersionOption();
QCommandLineOption debugInfoOption(QStringList() << "debug-info", QObject::tr("Displays debugging information."));
parser.addOption(configOption);
+ parser.addOption(localConfigOption);
parser.addOption(keyfileOption);
parser.addOption(pwstdinOption);
- parser.addOption(parentWindowOption);
parser.addOption(debugInfoOption);
+ Application app(argc, argv);
+ // don't set organizationName as that changes the return value of
+ // QStandardPaths::writableLocation(QDesktopServices::DataLocation)
+ Application::setApplicationName("KeePassXC");
+ Application::setApplicationVersion(KEEPASSXC_VERSION);
+ app.setProperty("KPXC_QUALIFIED_APPNAME", "org.keepassxc.KeePassXC");
+
parser.process(app);
- // Don't try and do anything with the application if we're only showing the help / version
+ // Exit early if we're only showing the help / version
if (parser.isSet(versionOption) || parser.isSet(helpOption)) {
return EXIT_SUCCESS;
}
- const QStringList fileNames = parser.positionalArguments();
+ // Process config file options early
+ if (parser.isSet(configOption) || parser.isSet(localConfigOption)) {
+ Config::createConfigFromFile(parser.value(configOption), parser.value(localConfigOption));
+ }
+ // Process single instance and early exit if already running
+ const QStringList fileNames = parser.positionalArguments();
if (app.isAlreadyRunning()) {
if (!fileNames.isEmpty()) {
app.sendFileNamesToRunningInstance(fileNames);
@@ -109,7 +105,14 @@ int main(int argc, char** argv)
return EXIT_SUCCESS;
}
- QApplication::setQuitOnLastWindowClosed(false);
+ // Apply the configured theme before creating any GUI elements
+ app.applyTheme();
+
+#if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)
+ QGuiApplication::setDesktopFileName(app.property("KPXC_QUALIFIED_APPNAME").toString() + QStringLiteral(".desktop"));
+#endif
+
+ Bootstrap::bootstrapApplication();
if (!Crypto::init()) {
QString error = QObject::tr("Fatal error while testing the cryptographic functions.");
@@ -128,10 +131,6 @@ int main(int argc, char** argv)
return EXIT_SUCCESS;
}
- if (parser.isSet(configOption)) {
- Config::createConfigFromFile(parser.value(configOption));
- }
-
MainWindow mainWindow;
QObject::connect(&app, SIGNAL(anotherInstanceStarted()), &mainWindow, SLOT(bringToFront()));
QObject::connect(&app, SIGNAL(applicationActivated()), &mainWindow, SLOT(bringToFront()));
diff --git a/tests/TestBrowser.cpp b/tests/TestBrowser.cpp
index 1255da4cf..05f315531 100644
--- a/tests/TestBrowser.cpp
+++ b/tests/TestBrowser.cpp
@@ -128,59 +128,52 @@ void TestBrowser::testBaseDomain()
void TestBrowser::testSortPriority()
{
- QString host = "github.com";
- QString submitUrl = "https://github.com/session";
- QString baseSubmitUrl = "https://github.com";
- QString fullUrl = "https://github.com/login";
-
- QScopedPointer<Entry> entry1(new Entry());
- QScopedPointer<Entry> entry2(new Entry());
- QScopedPointer<Entry> entry3(new Entry());
- QScopedPointer<Entry> entry4(new Entry());
- QScopedPointer<Entry> entry5(new Entry());
- QScopedPointer<Entry> entry6(new Entry());
- QScopedPointer<Entry> entry7(new Entry());
- QScopedPointer<Entry> entry8(new Entry());
- QScopedPointer<Entry> entry9(new Entry());
- QScopedPointer<Entry> entry10(new Entry());
- QScopedPointer<Entry> entry11(new Entry());
-
- entry1->setUrl("https://github.com/login");
- entry2->setUrl("https://github.com/login");
- entry3->setUrl("https://github.com/");
- entry4->setUrl("github.com/login");
- entry5->setUrl("http://github.com");
- entry6->setUrl("http://github.com/login");
- entry7->setUrl("github.com");
- entry8->setUrl("github.com/login");
- entry9->setUrl("https://github"); // Invalid URL
- entry10->setUrl("github.com");
- entry11->setUrl("https://github.com/login"); // Exact match
-
- // The extension uses the submitUrl as default for comparison
- auto res1 = m_browserService->sortPriority(entry1.data(), host, "https://github.com/login", baseSubmitUrl, fullUrl);
- auto res2 = m_browserService->sortPriority(entry2.data(), host, submitUrl, baseSubmitUrl, baseSubmitUrl);
- auto res3 = m_browserService->sortPriority(entry3.data(), host, submitUrl, baseSubmitUrl, fullUrl);
- auto res4 = m_browserService->sortPriority(entry4.data(), host, submitUrl, baseSubmitUrl, fullUrl);
- auto res5 = m_browserService->sortPriority(entry5.data(), host, submitUrl, baseSubmitUrl, fullUrl);
- auto res6 = m_browserService->sortPriority(entry6.data(), host, submitUrl, baseSubmitUrl, fullUrl);
- auto res7 = m_browserService->sortPriority(entry7.data(), host, submitUrl, baseSubmitUrl, fullUrl);
- auto res8 = m_browserService->sortPriority(entry8.data(), host, submitUrl, baseSubmitUrl, fullUrl);
- auto res9 = m_browserService->sortPriority(entry9.data(), host, submitUrl, baseSubmitUrl, fullUrl);
- auto res10 = m_browserService->sortPriority(entry10.data(), host, submitUrl, baseSubmitUrl, fullUrl);
- auto res11 = m_browserService->sortPriority(entry11.data(), host, submitUrl, baseSubmitUrl, fullUrl);
-
- QCOMPARE(res1, 100);
- QCOMPARE(res2, 40);
- QCOMPARE(res3, 90);
- QCOMPARE(res4, 0);
- QCOMPARE(res5, 0);
- QCOMPARE(res6, 0);
- QCOMPARE(res7, 0);
- QCOMPARE(res8, 0);
- QCOMPARE(res9, 0);
- QCOMPARE(res10, 0);
- QCOMPARE(res11, 100);
+ QFETCH(QString, entryUrl);
+ QFETCH(QString, siteUrl);
+ QFETCH(QString, formUrl);
+ QFETCH(int, expectedScore);
+
+ QScopedPointer<Entry> entry(new Entry());
+ entry->setUrl(entryUrl);
+
+ QCOMPARE(m_browserService->sortPriority(m_browserService->getEntryURLs(entry.data()), siteUrl, formUrl),
+ expectedScore);
+}
+
+void TestBrowser::testSortPriority_data()
+{
+ const QString siteUrl = "https://github.com/login";
+ const QString formUrl = "https://github.com/session";
+
+ QTest::addColumn<QString>("entryUrl");
+ QTest::addColumn<QString>("siteUrl");
+ QTest::addColumn<QString>("formUrl");
+ QTest::addColumn<int>("expectedScore");
+
+ QTest::newRow("Exact Match") << siteUrl << siteUrl << siteUrl << 100;
+ QTest::newRow("Exact Match (site)") << siteUrl << siteUrl << formUrl << 100;
+ QTest::newRow("Exact Match (form)") << siteUrl << "https://github.net" << siteUrl << 100;
+ QTest::newRow("Exact Match No Trailing Slash") << "https://github.com"
+ << "https://github.com/" << formUrl << 100;
+ QTest::newRow("Exact Match No Scheme") << "github.com/login" << siteUrl << formUrl << 100;
+ QTest::newRow("Exact Match with Query") << "https://github.com/login?test=test#fragment"
+ << "https://github.com/login?test=test" << formUrl << 100;
+
+ QTest::newRow("Site Query Mismatch") << siteUrl << siteUrl + "?test=test" << formUrl << 90;
+
+ QTest::newRow("Path Mismatch (site)") << "https://github.com/" << siteUrl << formUrl << 80;
+ QTest::newRow("Path Mismatch (site) No Scheme") << "github.com" << siteUrl << formUrl << 80;
+ QTest::newRow("Path Mismatch (form)") << "https://github.com/"
+ << "https://github.net" << formUrl << 70;
+
+ QTest::newRow("Subdomain Mismatch (site)") << siteUrl << "https://sub.github.com/"
+ << "https://github.net/" << 60;
+ QTest::newRow("Subdomain Mismatch (form)") << siteUrl << "https://github.net/"
+ << "https://sub.github.com/" << 50;
+
+ QTest::newRow("Scheme Mismatch") << "http://github.com" << siteUrl << formUrl << 0;
+ QTest::newRow("Scheme Mismatch w/path") << "http://github.com/login" << siteUrl << formUrl << 0;
+ QTest::newRow("Invalid URL") << "http://github" << siteUrl << formUrl << 0;
}
void TestBrowser::testSearchEntries()
@@ -393,14 +386,14 @@ void TestBrowser::testSubdomainsAndPaths()
createEntries(entryURLs, root);
- result = m_browserService->searchEntries(db, "https://accounts.example.com", "https://accounts.example.com");
+ result = m_browserService->searchEntries(db, "https://accounts.example.com/", "https://accounts.example.com/");
QCOMPARE(result.length(), 3);
QCOMPARE(result[0]->url(), QString("https://accounts.example.com"));
QCOMPARE(result[1]->url(), QString("https://accounts.example.com/path"));
QCOMPARE(result[2]->url(), QString("https://example.com/")); // Accepts any subdomain
result = m_browserService->searchEntries(
- db, "https://another.accounts.example.com", "https://another.accounts.example.com");
+ db, "https://another.accounts.example.com/", "https://another.accounts.example.com/");
QCOMPARE(result.length(), 4);
QCOMPARE(result[0]->url(),
QString("https://accounts.example.com")); // Accepts any subdomain under accounts.example.com
@@ -430,33 +423,32 @@ void TestBrowser::testSortEntries()
"http://github.com",
"http://github.com/login",
"github.com",
- "github.com/login",
+ "github.com/login?test=test",
"https://github", // Invalid URL
"github.com"};
auto entries = createEntries(urls, root);
browserSettings()->setBestMatchOnly(false);
- auto result = m_browserService->sortEntries(
- entries, "github.com", "https://github.com/session", "https://github.com"); // entries, host, submitUrl
+ browserSettings()->setSortByUsername(true);
+ auto result = m_browserService->sortEntries(entries, "https://github.com/login", "https://github.com/session");
QCOMPARE(result.size(), 10);
- QCOMPARE(result[0]->username(), QString("User 2"));
- QCOMPARE(result[0]->url(), QString("https://github.com/"));
- QCOMPARE(result[1]->username(), QString("User 0"));
- QCOMPARE(result[1]->url(), QString("https://github.com/login_page"));
- QCOMPARE(result[2]->username(), QString("User 1"));
- QCOMPARE(result[2]->url(), QString("https://github.com/login"));
- QCOMPARE(result[3]->username(), QString("User 3"));
- QCOMPARE(result[3]->url(), QString("github.com/login"));
+ QCOMPARE(result[0]->username(), QString("User 1"));
+ QCOMPARE(result[0]->url(), urls[1]);
+ QCOMPARE(result[1]->username(), QString("User 3"));
+ QCOMPARE(result[1]->url(), urls[3]);
+ QCOMPARE(result[2]->username(), QString("User 7"));
+ QCOMPARE(result[2]->url(), urls[7]);
+ QCOMPARE(result[3]->username(), QString("User 0"));
+ QCOMPARE(result[3]->url(), urls[0]);
// Test with a perfect match. That should be first in the list.
- result = m_browserService->sortEntries(
- entries, "github.com", "https://github.com/session", "https://github.com/login_page");
+ result = m_browserService->sortEntries(entries, "https://github.com/login_page", "https://github.com/session");
QCOMPARE(result.size(), 10);
QCOMPARE(result[0]->username(), QString("User 0"));
QCOMPARE(result[0]->url(), QString("https://github.com/login_page"));
- QCOMPARE(result[1]->username(), QString("User 2"));
- QCOMPARE(result[1]->url(), QString("https://github.com/"));
+ QCOMPARE(result[1]->username(), QString("User 1"));
+ QCOMPARE(result[1]->url(), QString("https://github.com/login"));
}
QList<Entry*> TestBrowser::createEntries(QStringList& urls, Group* root) const
@@ -487,6 +479,7 @@ void TestBrowser::testValidURLs()
urls["http:/example.com"] = false;
urls["cmd://C:/Toolchains/msys2/usr/bin/mintty \"ssh jon@192.168.0.1:22\""] = true;
urls["file:///Users/testUser/Code/test.html"] = true;
+ urls["{REF:A@I:46C9B1FFBD4ABC4BBB260C6190BAD20C} "] = true;
QHashIterator<QString, bool> i(urls);
while (i.hasNext()) {
@@ -507,45 +500,128 @@ void TestBrowser::testBestMatchingCredentials()
browserSettings()->setBestMatchOnly(true);
- auto result = m_browserService->searchEntries(db, "https://github.com/loginpage", "https://github.com/loginpage");
- QCOMPARE(result.size(), 1);
- QCOMPARE(result[0]->url(), QString("https://github.com/loginpage"));
-
- result = m_browserService->searchEntries(db, "https://github.com/justsomepage", "https://github.com/justsomepage");
- QCOMPARE(result.size(), 1);
- QCOMPARE(result[0]->url(), QString("https://github.com/justsomepage"));
-
- result = m_browserService->searchEntries(db, "https://github.com/", "https://github.com/");
- m_browserService->sortEntries(entries, "github.com", "https://github.com/", "https://github.com/");
- QCOMPARE(result.size(), 1);
- QCOMPARE(result[0]->url(), QString("https://github.com/"));
-
+ QString siteUrl = "https://github.com/loginpage";
+ auto result = m_browserService->searchEntries(db, siteUrl, siteUrl);
+ auto sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
+ QCOMPARE(sorted.size(), 1);
+ QCOMPARE(sorted[0]->url(), siteUrl);
+
+ siteUrl = "https://github.com/justsomepage";
+ result = m_browserService->searchEntries(db, siteUrl, siteUrl);
+ sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
+ QCOMPARE(sorted.size(), 1);
+ QCOMPARE(sorted[0]->url(), siteUrl);
+
+ siteUrl = "https://github.com/";
+ result = m_browserService->searchEntries(db, siteUrl, siteUrl);
+ sorted = m_browserService->sortEntries(entries, siteUrl, siteUrl);
+ QCOMPARE(sorted.size(), 1);
+ QCOMPARE(sorted[0]->url(), siteUrl);
+
+ // Without best-matching the URL with the path should be returned first
browserSettings()->setBestMatchOnly(false);
- result = m_browserService->searchEntries(db, "https://github.com/loginpage", "https://github.com/loginpage");
- QCOMPARE(result.size(), 3);
- QCOMPARE(result[0]->url(), QString("https://github.com/loginpage"));
+ siteUrl = "https://github.com/loginpage";
+ result = m_browserService->searchEntries(db, siteUrl, siteUrl);
+ sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
+ QCOMPARE(sorted.size(), 3);
+ QCOMPARE(sorted[0]->url(), siteUrl);
// Test with subdomains
QStringList subdomainsUrls = {"https://sub.github.com/loginpage",
"https://sub.github.com/justsomepage",
- "https://bus.github.com/justsomepage"};
+ "https://bus.github.com/justsomepage",
+ "https://subdomain.example.com/",
+ "https://subdomain.example.com",
+ "https://example.com"};
entries = createEntries(subdomainsUrls, root);
browserSettings()->setBestMatchOnly(true);
+ siteUrl = "https://sub.github.com/justsomepage";
+ result = m_browserService->searchEntries(db, siteUrl, siteUrl);
+ sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
+ QCOMPARE(sorted.size(), 1);
+ QCOMPARE(sorted[0]->url(), siteUrl);
+
+ siteUrl = "https://github.com/justsomepage";
+ result = m_browserService->searchEntries(db, siteUrl, siteUrl);
+ sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
+ QCOMPARE(sorted.size(), 1);
+ QCOMPARE(sorted[0]->url(), siteUrl);
+
+ siteUrl = "https://sub.github.com/justsomepage?wehavesomeextra=here";
+ result = m_browserService->searchEntries(db, siteUrl, siteUrl);
+ sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
+ QCOMPARE(sorted.size(), 1);
+ QCOMPARE(sorted[0]->url(), QString("https://sub.github.com/justsomepage"));
+
+ // The matching should not care if there's a / path or not.
+ siteUrl = "https://subdomain.example.com/";
+ result = m_browserService->searchEntries(db, siteUrl, siteUrl);
+ sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
+ QCOMPARE(sorted.size(), 2);
+ QCOMPARE(sorted[0]->url(), QString("https://subdomain.example.com/"));
+ QCOMPARE(sorted[1]->url(), QString("https://subdomain.example.com"));
+
+ // Entries with https://example.com should be still returned even if the site URL has a subdomain. Those have the
+ // best match.
+ db = QSharedPointer<Database>::create();
+ root = db->rootGroup();
+ QStringList domainUrls = {"https://example.com", "https://example.com", "https://other.example.com"};
+ entries = createEntries(domainUrls, root);
+ siteUrl = "https://subdomain.example.com";
+ result = m_browserService->searchEntries(db, siteUrl, siteUrl);
+ sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
+
+ QCOMPARE(sorted.size(), 2);
+ QCOMPARE(sorted[0]->url(), QString("https://example.com"));
+ QCOMPARE(sorted[1]->url(), QString("https://example.com"));
+
+ // https://github.com/keepassxreboot/keepassxc/issues/4754
+ db = QSharedPointer<Database>::create();
+ root = db->rootGroup();
+ QStringList fooUrls = {"https://example.com/foo", "https://example.com/bar"};
+ entries = createEntries(fooUrls, root);
+
+ for (const auto& url : fooUrls) {
+ result = m_browserService->searchEntries(db, url, url);
+ sorted = m_browserService->sortEntries(result, url, url);
+ QCOMPARE(sorted.size(), 1);
+ QCOMPARE(sorted[0]->url(), QString(url));
+ }
- result = m_browserService->searchEntries(
- db, "https://sub.github.com/justsomepage", "https://sub.github.com/justsomepage");
- QCOMPARE(result.size(), 1);
- QCOMPARE(result[0]->url(), QString("https://sub.github.com/justsomepage"));
-
- result = m_browserService->searchEntries(db, "https://github.com/justsomepage", "https://github.com/justsomepage");
- QCOMPARE(result.size(), 1);
- QCOMPARE(result[0]->url(), QString("https://github.com/justsomepage"));
-
- result = m_browserService->searchEntries(db,
- "https://sub.github.com/justsomepage?wehavesomeextra=here",
- "https://sub.github.com/justsomepage?wehavesomeextra=here");
- QCOMPARE(result.size(), 1);
- QCOMPARE(result[0]->url(), QString("https://sub.github.com/justsomepage"));
+ // https://github.com/keepassxreboot/keepassxc/issues/4734
+ db = QSharedPointer<Database>::create();
+ root = db->rootGroup();
+ QStringList testUrls = {"http://some.domain.tld/somePath", "http://some.domain.tld/otherPath"};
+ entries = createEntries(testUrls, root);
+
+ for (const auto& url : testUrls) {
+ result = m_browserService->searchEntries(db, url, url);
+ sorted = m_browserService->sortEntries(result, url, url);
+ QCOMPARE(sorted.size(), 1);
+ QCOMPARE(sorted[0]->url(), QString(url));
+ }
+}
+
+void TestBrowser::testBestMatchingWithAdditionalURLs()
+{
+ auto db = QSharedPointer<Database>::create();
+ auto* root = db->rootGroup();
+
+ QStringList urls = {"https://github.com/loginpage", "https://test.github.com/", "https://github.com/"};
+
+ auto entries = createEntries(urls, root);
+ browserSettings()->setBestMatchOnly(true);
+
+ // Add an additional URL to the first entry
+ entries.first()->attributes()->set(BrowserService::ADDITIONAL_URL, "https://test.github.com/anotherpage");
+
+ // The first entry should be triggered
+ auto result = m_browserService->searchEntries(
+ db, "https://test.github.com/anotherpage", "https://test.github.com/anotherpage");
+ auto sorted = m_browserService->sortEntries(
+ result, "https://test.github.com/anotherpage", "https://test.github.com/anotherpage");
+ QCOMPARE(sorted.length(), 1);
+ QCOMPARE(sorted[0]->url(), urls[0]);
}
diff --git a/tests/TestBrowser.h b/tests/TestBrowser.h
index 37daffe74..bcdc609dd 100644
--- a/tests/TestBrowser.h
+++ b/tests/TestBrowser.h
@@ -40,6 +40,7 @@ private slots:
void testBaseDomain();
void testSortPriority();
+ void testSortPriority_data();
void testSearchEntries();
void testSearchEntriesByUUID();
void testSearchEntriesWithPort();
@@ -49,6 +50,7 @@ private slots:
void testSortEntries();
void testValidURLs();
void testBestMatchingCredentials();
+ void testBestMatchingWithAdditionalURLs();
private:
QList<Entry*> createEntries(QStringList& urls, Group* root) const;