Compare commits

...

15 Commits

Author SHA1 Message Date
Juzu-O
dc9510ec64 Add URL double-click action option to Settings (#12322)
* Closes #4717

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: juzu-o <3142026+juzu-o@users.noreply.github.com>
Co-authored-by: Jonathan White <support@dmapps.us>
2025-11-02 12:35:38 -05:00
MNarath
a98d3b7c64 Fix KeeShare entries with references not updating (#11809)
A Entry that gets shared containing a reference Attribute would not write a history entry upon resolving said Attribute resulting in the import into the target database not beeing triggered despite the changes beeing written to the keeshare db.
2025-11-02 12:35:36 -05:00
Isaac Elliott
6809c4da1b Allow read-only native message files (#12236)
* Allow read-only native message files

It's possible[^1] for a native message file to be both correct and read-only.
When current versions of `keepassxc` encounter this, it fails, because
it can't write to the file. In this situation it should only fail if
the read-only file's contents are different to those it's trying to
write.

[^1]: e.g. when using an immutable OS management system like NixOS or
   home-manager.

---------

Co-authored-by: Jonathan White <support@dmapps.us>
2025-11-02 10:19:28 -05:00
Jonathan White
aeec2b8a98 Fix Auto-Type Empty Window Behavior
* Fixes #9282
* Also improve documentation for window title matching behavior
2025-11-02 10:19:27 -05:00
Sebastian Livoni
d7c7ce4cc4 Add Window menu for macOS and specify Help menu to AppKit (#12357)
* Add Window menu for macOS and specify Help menu to AppKit
* Fix potential NSString dangling pointers of temporary QStrings
2025-11-02 10:19:26 -05:00
renner
daf23b65ad feat: refresh appdata.xml
* Adds donation, contact and up-to-date transifex URL
* Add features to appdata.xml for FlatHub
* Remove old releases to reduce file size
* Improve summary and description text
2025-11-02 10:19:25 -05:00
renner
d60826ad7f chore: reformat xml with GUI tool 2025-11-02 10:19:08 -05:00
Chris Bednarski
e19e7d2c43 Change StartupNotify to false
StartupNotify causes KeepassXC to hang on startup until the notification timeout is reached, making the KeepassXC window unavailable in the application switcher (i.e. alt-tab) on various Linux distros.

Fixes https://github.com/keepassxreboot/keepassxc/issues/6423
Fixes https://github.com/keepassxreboot/keepassxc/issues/11664
2025-11-02 10:18:12 -05:00
Jonathan White
abeb231bc3 Fix saving "Search Wait for Enter" setting 2025-11-02 10:18:11 -05:00
Sertonix
1705c2c94a Fix uninitialized memory when --pw-stdin is used with a pipe 2025-11-02 10:18:09 -05:00
Siddhant Shekhar
12a623f7d7 Sanitize username to prevent single-instance detection failure (#12559)
---------

Co-authored-by: Jonathan White <support@dmapps.us>
2025-11-01 10:33:34 -04:00
Janek Bevendorff
6eff15c9ec Fix AppImage launcher error, fixes #12608 2025-11-01 10:19:27 -04:00
Jonathan White
41b2a9bb60 Fix out-of-memory crash with malformed ssh keys
* Reported by Oblivionsage - thank you!
2025-11-01 10:18:58 -04:00
Jonathan White
1a7992cc57 Remove Last Accessed from GUI
* Closes #2005
2025-11-01 10:17:59 -04:00
Xì Gà
7dfeb47212 Open TOTP setup dialog if entry has no valid TOTP set (#12584)
---------

Co-authored-by: x <a@b.c>
Co-authored-by: Jonathan White <support@dmapps.us>
2025-11-01 10:17:52 -04:00
38 changed files with 1086 additions and 1363 deletions

View File

@@ -32,7 +32,9 @@ To configure Auto-Type sequences for your entries, perform the following steps:
1. Navigate to the entries list and open the desired entry for editing. Click the _Auto-Type_ item from the left-hand menu bar *(1)*. Press the kbd:[+] button *(2)* to add a new sequence entry. Select the desired window using the drop-down menu, or simply type a window title in the box *(3)*.
+
TIP: You can use an asterisk (`\*`) to match any value (e.g., when a window title contains a dynamic filename or website name). Set the window title to `*` to match all windows. Leave the window title blank to offer additional default Auto-Type sequences, such as custom attributes.
TIP: You can use an asterisk (`\*`) as a wildcard (e.g., when a window title contains a dynamic file or website name). Set the window title to `*` to match all windows. Leave the window title blank to offer additional sequences for every matching window. This is useful for typing individual custom attributes, for example.
+
TIP: To use a standard regular expression for window title matching, the window title must start and end with two forward slashes (e.g., `//^Secure Login - .*$//`).
+
.Auto-Type entry sequences
image::autotype_entry_sequences.png[]

View File

@@ -3,7 +3,7 @@
if [ "$1" == "cli" ] || [ "$(basename "$ARGV0")" == "keepassxc-cli" ] || [ "$(basename "$ARGV0")" == "keepassxc-cli.AppImage" ]; then
[ "$1" == "cli" ] && shift
exec keepassxc-cli "$@"
elif [ "$1" == "proxy" ] || [ "$(basename "$ARGV0")" == "keepassxc-proxy" ] || [ "$(basename "$ARGV0")" == "keepassxc-proxy.AppImage" ] \\
elif [ "$1" == "proxy" ] || [ "$(basename "$ARGV0")" == "keepassxc-proxy" ] || [ "$(basename "$ARGV0")" == "keepassxc-proxy.AppImage" ] \
|| [ -v CHROME_WRAPPER ] || [ -v MOZ_LAUNCHED_CHILD ]; then
[ "$1" == "proxy" ] && shift
exec keepassxc-proxy "$@"

File diff suppressed because it is too large Load Diff

View File

@@ -39,7 +39,7 @@ Exec=keepassxc %f
TryExec=keepassxc
Icon=@APP_ICON_NAME@
StartupWMClass=keepassxc
StartupNotify=true
StartupNotify=false
Terminal=false
Type=Application
Version=1.5

View File

@@ -157,6 +157,25 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>AppKit</name>
<message>
<source>Window</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Minimize</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Zoom</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bring All to Front</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ApplicationSettingsWidget</name>
<message>
@@ -553,10 +572,6 @@
<source>Export settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Open browser on double clicking URL field in entry view</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Font size:</source>
<translation type="unfinished"></translation>
@@ -569,6 +584,26 @@
<source>Skip confirmation for main window Auto-Type actions</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Double-click action for URL:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Double-click action for URL field</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Edit entry</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Open entry URL in browser</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Copy entry URL to clipboard</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Auto-generate password for new entries</source>
<translation type="unfinished"></translation>
@@ -876,6 +911,17 @@ Ctrl+Shift+4 - Copy URL&lt;br/&gt;
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>BinaryStream</name>
<message>
<source>Failed to read string data: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>String length exceeds 10 MiB limit (requested %1)</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>BrowserAccessControlDialog</name>
<message>
@@ -3770,14 +3816,6 @@ Supported extensions are: %1.</source>
<source>Datetime modified</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Accessed:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Datetime accessed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Uuid:</source>
<translation type="unfinished"></translation>
@@ -4151,10 +4189,6 @@ Would you like to overwrite the existing attachment?</source>
<source>Modified</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Accessed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Attachments</source>
<translation type="unfinished"></translation>
@@ -4191,10 +4225,6 @@ Would you like to overwrite the existing attachment?</source>
<source>Last modification date</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Last access date</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Attached files</source>
<translation type="unfinished"></translation>
@@ -6403,10 +6433,6 @@ Expect some bugs and minor issues, this version is meant for testing purposes.</
<source>Found zero keys</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to read public key.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Corrupted key file, reading private key failed</source>
<translation type="unfinished"></translation>
@@ -6491,6 +6517,14 @@ Expect some bugs and minor issues, this version is meant for testing purposes.</
<source>(encrypted)</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to read key file: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to read public key: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>AES-256/GCM is currently not supported</source>
<translation type="unfinished"></translation>

View File

@@ -372,16 +372,30 @@ bool NativeMessageInstaller::createNativeMessageFile(SupportedBrowsers browser)
QFile scriptFile(path);
if (!scriptFile.open(QIODevice::WriteOnly)) {
qWarning() << "Browser Plugin: Failed to open native message file for writing at " << scriptFile.fileName();
qWarning() << scriptFile.errorString();
return false;
if (!scriptFile.open(QIODevice::ReadOnly)) {
qWarning() << "Browser Plugin: Failed to open native message file at " << scriptFile.fileName();
qWarning() << scriptFile.errorString();
return false;
}
// We failed to write to `scriptFile`, but we can read it, so we assume that it's a read-only file.
// Consider success if the read-only file already contains the content we would have written.
QJsonDocument expectedDoc(constructFile(browser));
QJsonDocument actualDoc = QJsonDocument::fromJson(scriptFile.readAll());
if (expectedDoc != actualDoc) {
qWarning() << "Browser Plugin: Unexpected (read-only) native message file at " << scriptFile.fileName();
qWarning() << "Expected contents: " << expectedDoc;
return false;
}
} else {
QJsonDocument doc(constructFile(browser));
if (scriptFile.write(doc.toJson()) < 0) {
qWarning() << "Browser Plugin: Failed to write native message file at " << scriptFile.fileName();
qWarning() << scriptFile.errorString();
return false;
}
}
QJsonDocument doc(constructFile(browser));
if (scriptFile.write(doc.toJson()) < 0) {
qWarning() << "Browser Plugin: Failed to write native message file at " << scriptFile.fileName();
qWarning() << scriptFile.errorString();
return false;
}
return true;
}

View File

@@ -105,7 +105,9 @@ namespace Utils
SetConsoleMode(hIn, mode);
#else
struct termios t;
tcgetattr(STDIN_FILENO, &t);
if (tcgetattr(STDIN_FILENO, &t) < 0) {
return;
}
if (enable) {
t.c_lflag |= ECHO;

View File

@@ -67,6 +67,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
{Config::SearchLimitGroup,{QS("SearchLimitGroup"), Roaming, false}},
{Config::MinimizeOnOpenUrl,{QS("MinimizeOnOpenUrl"), Roaming, false}},
{Config::OpenURLOnDoubleClick, {QS("OpenURLOnDoubleClick"), Roaming, true}},
{Config::URLDoubleClickAction, {QS("URLDoubleClickAction"), Roaming, 0}},
{Config::HideWindowOnCopy,{QS("HideWindowOnCopy"), Roaming, false}},
{Config::MinimizeOnCopy,{QS("MinimizeOnCopy"), Roaming, true}},
{Config::AutoGeneratePasswordForNewEntries,{QS("AutoGeneratePasswordForNewEntries"), Roaming, false}},
@@ -118,6 +119,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
{Config::GUI_CheckForUpdates, {QS("GUI/CheckForUpdates"), Roaming, true}},
{Config::GUI_CheckForUpdatesNextCheck, {QS("GUI/CheckForUpdatesNextCheck"), Local, 0}},
{Config::GUI_CheckForUpdatesIncludeBetas, {QS("GUI/CheckForUpdatesIncludeBetas"), Roaming, false}},
{Config::GUI_SearchWaitForEnter, {QS("GUI/SearchWaitForEnter"), Roaming, false}},
{Config::GUI_ShowExpiredEntriesOnDatabaseUnlock, {QS("GUI/ShowExpiredEntriesOnDatabaseUnlock"), Roaming, true}},
{Config::GUI_ShowExpiredEntriesOnDatabaseUnlockOffsetDays, {QS("GUI/ShowExpiredEntriesOnDatabaseUnlockOffsetDays"), Roaming, 3}},
{Config::GUI_FontSizeOffset, {QS("GUI/FontSizeOffset"), Local, 0}},
@@ -489,6 +491,15 @@ void Config::migrate()
remove(GUI_ListViewState);
}
// Migrate from boolean OpenURLOnDoubleClick to enum URLDoubleClickAction
if (m_settings->contains(configStrings[OpenURLOnDoubleClick].name)
&& !m_settings->contains(configStrings[URLDoubleClickAction].name)) {
bool openUrlOnDoubleClick = get(OpenURLOnDoubleClick).toBool();
// Convert: true (open browser) -> 0, false (edit entry) -> 2
set(URLDoubleClickAction, openUrlOnDoubleClick ? 0 : 2);
// Keep the old setting for now for compatibility
}
m_settings->setValue("ConfigVersion", CONFIG_VERSION);
sync();
}

View File

@@ -49,6 +49,7 @@ public:
SearchLimitGroup,
MinimizeOnOpenUrl,
OpenURLOnDoubleClick,
URLDoubleClickAction,
HideWindowOnCopy,
MinimizeOnCopy,
MinimizeAfterUnlock,
@@ -98,7 +99,7 @@ public:
GUI_CompactMode,
GUI_CheckForUpdates,
GUI_CheckForUpdatesIncludeBetas,
SearchWaitForEnter,
GUI_SearchWaitForEnter,
GUI_ShowExpiredEntriesOnDatabaseUnlock,
GUI_ShowExpiredEntriesOnDatabaseUnlockOffsetDays,
GUI_FontSizeOffset,

View File

@@ -334,12 +334,15 @@ QList<QString> Entry::autoTypeSequences(const QString& windowTitle) const
};
QList<QString> sequenceList;
QList<QString> emptyWindowSequences;
// Add window association matches
const auto assocList = autoTypeAssociations()->getAll();
for (const auto& assoc : assocList) {
auto window = resolveMultiplePlaceholders(assoc.window);
if (!assoc.window.isEmpty() && windowMatches(window)) {
if (assoc.window.isEmpty()) {
emptyWindowSequences << assoc.sequence;
} else if (windowMatches(window)) {
if (!assoc.sequence.isEmpty()) {
sequenceList << assoc.sequence;
} else {
@@ -358,6 +361,11 @@ QList<QString> Entry::autoTypeSequences(const QString& windowTitle) const
sequenceList << effectiveAutoTypeSequence();
}
// If any associations were made, include the empty window associations
if (!sequenceList.isEmpty()) {
sequenceList.append(emptyWindowSequences);
}
return sequenceList;
}

View File

@@ -426,6 +426,29 @@ namespace Tools
return filename.trimmed();
}
QString cleanUsername()
{
#if defined(Q_OS_WIN)
QString userName = qgetenv("USERNAME");
if (userName.isEmpty()) {
userName = qgetenv("USER");
}
#else
QString userName = qgetenv("USER");
if (userName.isEmpty()) {
userName = qgetenv("USERNAME");
}
#endif
// Sanitize username for file safety
userName = userName.trimmed();
// Replace <>:"/\|?* with _
userName.replace(QRegularExpression(R"([<>:\"\/\\|?*])"), "_");
// Remove trailing dots and spaces
userName.replace(QRegularExpression(R"([.\s]+$)"), "");
return userName;
}
QVariantMap qo2qvm(const QObject* object, const QStringList& ignoredProperties)
{
QVariantMap result;

View File

@@ -48,6 +48,7 @@ namespace Tools
QString envSubstitute(const QString& filepath,
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment());
QString cleanFilename(QString filename);
QString cleanUsername();
template <class T> QSet<T> asSet(const QList<T>& a)
{

View File

@@ -20,6 +20,7 @@
#include "Application.h"
#include "core/Bootstrap.h"
#include "core/Tools.h"
#include "gui/MainWindow.h"
#include "gui/MessageBox.h"
#include "gui/osutils/OSUtils.h"
@@ -31,6 +32,7 @@
#include <QLocalSocket>
#include <QLockFile>
#include <QPixmapCache>
#include <QRegularExpression>
#include <QSocketNotifier>
#include <QStandardPaths>
@@ -63,20 +65,19 @@ Application::Application(int& argc, char** argv)
registerUnixSignals();
#endif
QString userName = qgetenv("USER");
if (userName.isEmpty()) {
userName = qgetenv("USERNAME");
}
QString identifier = "keepassxc";
if (!userName.isEmpty()) {
identifier += "-" + userName;
// Build identifier
auto identifier = QStringLiteral("keepassxc");
auto username = Tools::cleanUsername();
if (!username.isEmpty()) {
identifier += QChar('-') + username;
}
#ifdef QT_DEBUG
// In DEBUG mode don't interfere with Release instances
identifier += "-DEBUG";
// In DEBUG mode dont interfere with Release instances
identifier += QStringLiteral("-DEBUG");
#endif
QString lockName = identifier + ".lock";
m_socketName = identifier + ".socket";
QString lockName = identifier + QStringLiteral(".lock");
m_socketName = identifier + QStringLiteral(".socket");
// According to documentation we should use RuntimeLocation on *nixes, but even Qt doesn't respect
// this and creates sockets in TempLocation, so let's be consistent.

View File

@@ -197,7 +197,7 @@ void ApplicationSettingsWidget::loadSettings()
m_generalUi->autoReloadOnChangeCheckBox->setChecked(config()->get(Config::AutoReloadOnChange).toBool());
m_generalUi->minimizeAfterUnlockCheckBox->setChecked(config()->get(Config::MinimizeAfterUnlock).toBool());
m_generalUi->minimizeOnOpenUrlCheckBox->setChecked(config()->get(Config::MinimizeOnOpenUrl).toBool());
m_generalUi->openUrlOnDoubleClick->setChecked(config()->get(Config::OpenURLOnDoubleClick).toBool());
m_generalUi->urlDoubleClickComboBox->setCurrentIndex(config()->get(Config::URLDoubleClickAction).toInt());
m_generalUi->hideWindowOnCopyCheckBox->setChecked(config()->get(Config::HideWindowOnCopy).toBool());
hideWindowOnCopyCheckBoxToggled(m_generalUi->hideWindowOnCopyCheckBox->isChecked());
m_generalUi->minimizeOnCopyRadioButton->setChecked(config()->get(Config::MinimizeOnCopy).toBool());
@@ -382,7 +382,7 @@ void ApplicationSettingsWidget::saveSettings()
config()->set(Config::AutoReloadOnChange, m_generalUi->autoReloadOnChangeCheckBox->isChecked());
config()->set(Config::MinimizeAfterUnlock, m_generalUi->minimizeAfterUnlockCheckBox->isChecked());
config()->set(Config::MinimizeOnOpenUrl, m_generalUi->minimizeOnOpenUrlCheckBox->isChecked());
config()->set(Config::OpenURLOnDoubleClick, m_generalUi->openUrlOnDoubleClick->isChecked());
config()->set(Config::URLDoubleClickAction, m_generalUi->urlDoubleClickComboBox->currentIndex());
config()->set(Config::HideWindowOnCopy, m_generalUi->hideWindowOnCopyCheckBox->isChecked());
config()->set(Config::MinimizeOnCopy, m_generalUi->minimizeOnCopyRadioButton->isChecked());
config()->set(Config::DropToBackgroundOnCopy, m_generalUi->dropToBackgroundOnCopyRadioButton->isChecked());

View File

@@ -554,14 +554,59 @@
</widget>
</item>
<item>
<widget class="QCheckBox" name="openUrlOnDoubleClick">
<property name="text">
<string>Open browser on double clicking URL field in entry view</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
<layout class="QHBoxLayout" name="urlDoubleClickLayout">
<item>
<widget class="QLabel" name="urlDoubleClickLabel">
<property name="text">
<string>Double-click action for URL:</string>
</property>
<property name="buddy">
<cstring>urlDoubleClickComboBox</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="urlDoubleClickComboBox">
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
<property name="accessibleName">
<string>Double-click action for URL field</string>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
<item>
<property name="text">
<string>Open entry URL in browser</string>
</property>
</item>
<item>
<property name="text">
<string>Copy entry URL to clipboard</string>
</property>
</item>
<item>
<property name="text">
<string>Edit entry</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="urlDoubleClickSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="useGroupIconOnEntryCreationCheckBox">
@@ -1439,7 +1484,7 @@
<tabstop>alternativeSaveComboBox</tabstop>
<tabstop>ConfirmMoveEntryToRecycleBinCheckBox</tabstop>
<tabstop>EnableCopyOnDoubleClickCheckBox</tabstop>
<tabstop>openUrlOnDoubleClick</tabstop>
<tabstop>urlDoubleClickComboBox</tabstop>
<tabstop>useGroupIconOnEntryCreationCheckBox</tabstop>
<tabstop>minimizeOnOpenUrlCheckBox</tabstop>
<tabstop>hideWindowOnCopyCheckBox</tabstop>

View File

@@ -550,6 +550,13 @@ void DatabaseWidget::copyTotp()
if (!currentEntry) {
return;
}
// If the entry has no TOTP set, open the setup dialog first
if (!currentEntry->hasValidTotp()) {
setupTotp();
return;
}
setClipboardTextAndMinimize(currentEntry->totp());
}
@@ -1426,12 +1433,21 @@ void DatabaseWidget::entryActivationSignalReceived(Entry* entry, EntryModel::Mod
// case EntryModel::Attachments:
// break;
case EntryModel::Url:
if (!entry->url().isEmpty() && config()->get(Config::OpenURLOnDoubleClick).toBool()) {
openUrlForEntry(entry);
break;
if (!entry->url().isEmpty()) {
switch (config()->get(Config::URLDoubleClickAction).toInt()) {
case 2: // Edit entry
switchToEntryEdit(entry);
break;
case 1: // Copy entry URL to clipboard
setClipboardTextAndMinimize(entry->resolveMultiplePlaceholders(entry->url()));
break;
case 0: // Open entry URL in browser (default)
default:
openUrlForEntry(entry);
break;
}
}
// Note, order matters here. We want to fall into the default case.
[[fallthrough]];
break;
default:
switchToEntryEdit(entry);
}

View File

@@ -50,7 +50,6 @@ void EditWidgetProperties::setFields(const TimeInfo& timeInfo, const QUuid& uuid
static const QString timeFormat("d MMM yyyy HH:mm:ss");
m_ui->modifiedEdit->setText(timeInfo.lastModificationTime().toLocalTime().toString(timeFormat));
m_ui->createdEdit->setText(timeInfo.creationTime().toLocalTime().toString(timeFormat));
m_ui->accessedEdit->setText(timeInfo.lastAccessTime().toLocalTime().toString(timeFormat));
m_ui->uuidEdit->setText(uuid.toRfc4122().toHex());
}

View File

@@ -83,27 +83,7 @@
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelAccessed">
<property name="text">
<string>Accessed:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="accessedEdit">
<property name="accessibleName">
<string>Datetime accessed</string>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<item row="2" column="0">
<widget class="QLabel" name="labelUuid">
<property name="text">
<string>Uuid:</string>
@@ -113,7 +93,7 @@
</property>
</widget>
</item>
<item row="3" column="1">
<item row="2" column="1">
<widget class="QLineEdit" name="uuidEdit">
<property name="accessibleName">
<string>Unique ID</string>
@@ -204,7 +184,6 @@
<tabstops>
<tabstop>createdEdit</tabstop>
<tabstop>modifiedEdit</tabstop>
<tabstop>accessedEdit</tabstop>
<tabstop>uuidEdit</tabstop>
</tabstops>
<resources/>

View File

@@ -90,6 +90,10 @@ MainWindow::MainWindow()
m_ui->setupUi(this);
#ifdef Q_OS_MACOS
macUtils()->configureWindowAndHelpMenus(this, m_ui->menuHelp);
#endif
#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) && !defined(QT_NO_DBUS)
new MainWindowAdaptor(this);
QDBusConnection dbus = QDBusConnection::sessionBus();
@@ -1027,7 +1031,7 @@ void MainWindow::updateMenuActionState()
m_ui->actionEntryAutoTypeTOTP->setVisible(singleEntrySelected && dbWidget->currentEntryHasTotp());
m_ui->actionEntryOpenUrl->setEnabled(singleEntryOrEditing && dbWidget->currentEntryHasUrl());
m_ui->actionEntryTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp());
m_ui->actionEntryCopyTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp());
m_ui->actionEntryCopyTotp->setEnabled(singleEntrySelected);
m_ui->actionEntryCopyPasswordTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp());
m_ui->actionEntrySetupTotp->setEnabled(singleEntrySelected);
m_ui->actionEntryTotpQRCode->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp());

View File

@@ -72,10 +72,10 @@ SearchWidget::SearchWidget(QWidget* parent)
m_actionLimitGroup->setChecked(config()->get(Config::SearchLimitGroup).toBool());
m_actionWaitForEnter = m_searchMenu->addAction(
tr("Press Enter to search"), this, [](bool state) { config()->set(Config::SearchWaitForEnter, state); });
tr("Press Enter to search"), this, [](bool state) { config()->set(Config::GUI_SearchWaitForEnter, state); });
m_actionWaitForEnter->setObjectName("actionSearchWaitForEnter");
m_actionWaitForEnter->setCheckable(true);
m_actionWaitForEnter->setChecked(config()->get(Config::SearchWaitForEnter).toBool());
m_actionWaitForEnter->setChecked(config()->get(Config::GUI_SearchWaitForEnter).toBool());
m_ui->searchIcon->setIcon(icons()->icon("system-search"));
m_ui->searchEdit->addAction(m_ui->searchIcon, QLineEdit::LeadingPosition);

View File

@@ -203,9 +203,6 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
case Modified:
result = Clock::toString(entry->timeInfo().lastModificationTime().toLocalTime());
return result;
case Accessed:
result = Clock::toString(entry->timeInfo().lastAccessTime().toLocalTime());
return result;
case Attachments: {
// Display comma-separated list of attachments
QList<QString> attachments = entry->attachments()->keys();
@@ -266,8 +263,6 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
return entry->timeInfo().creationTime();
case Modified:
return entry->timeInfo().lastModificationTime();
case Accessed:
return entry->timeInfo().lastAccessTime();
case Paperclip:
// Display entries with attachments above those without when
// sorting ascendingly (and vice versa when sorting descendingly)
@@ -400,8 +395,6 @@ QVariant EntryModel::headerData(int section, Qt::Orientation orientation, int ro
return tr("Created");
case Modified:
return tr("Modified");
case Accessed:
return tr("Accessed");
case Attachments:
return tr("Attachments");
case Size:
@@ -441,8 +434,6 @@ QVariant EntryModel::headerData(int section, Qt::Orientation orientation, int ro
return tr("Creation date");
case Modified:
return tr("Last modification date");
case Accessed:
return tr("Last access date");
case Attachments:
return tr("Attached files");
case Size:

View File

@@ -43,7 +43,7 @@ public:
Expires = 6,
Created = 7,
Modified = 8,
Accessed = 9,
Accessed = 9, // Kept for compatibility
Paperclip = 10,
Attachments = 11,
Totp = 12,

View File

@@ -105,12 +105,16 @@ EntryView::EntryView(QWidget* parent)
m_columnActions->setExclusive(false);
for (int visualIndex = 0; visualIndex < header()->count(); ++visualIndex) {
int logicalIndex = header()->logicalIndex(visualIndex);
QString caption = m_model->headerData(logicalIndex, Qt::Horizontal, Qt::DisplayRole).toString();
if (caption.isEmpty()) {
caption = m_model->headerData(logicalIndex, Qt::Horizontal, Qt::ToolTipRole).toString();
auto caption = m_model->headerData(logicalIndex, Qt::Horizontal, Qt::DisplayRole);
if (!caption.isValid()) {
caption = m_model->headerData(logicalIndex, Qt::Horizontal, Qt::ToolTipRole);
if (!caption.isValid()) {
// Ignored column, skip it
continue;
}
}
auto action = m_headerMenu->addAction(caption);
auto action = m_headerMenu->addAction(caption.toString());
action->setCheckable(true);
action->setData(logicalIndex);
m_columnActions->addAction(action);
@@ -478,7 +482,6 @@ void EntryView::resetViewToDefaults()
header()->hideSection(EntryModel::Password);
header()->hideSection(EntryModel::Expires);
header()->hideSection(EntryModel::Created);
header()->hideSection(EntryModel::Accessed);
header()->hideSection(EntryModel::Attachments);
header()->hideSection(EntryModel::Size);
header()->hideSection(EntryModel::PasswordStrength);
@@ -515,6 +518,8 @@ void EntryView::resetViewToDefaults()
void EntryView::onHeaderChanged()
{
m_model->setBackgroundColorVisible(isColumnHidden(EntryModel::Color));
// Force hide accessed column
header()->hideSection(EntryModel::Accessed);
}
void EntryView::showEvent(QShowEvent* event)

View File

@@ -21,6 +21,8 @@
#include <QColor>
#include <QObject>
#include <QMenu>
#include <QMainWindow>
#include <unistd.h>
class QWindow;
@@ -45,6 +47,7 @@ public:
bool enableScreenRecording();
void toggleForegroundApp(bool foreground);
void setWindowSecurity(QWindow* window, bool state);
void configureWindowAndHelpMenus(QMainWindow* mainWindow, QMenu* helpMenu);
signals:
void userSwitched();

View File

@@ -42,5 +42,6 @@
- (bool) enableScreenRecording;
- (void) toggleForegroundApp:(bool) foreground;
- (void) setWindowSecurity:(NSWindow*) window state:(bool) state;
- (void) configureWindowAndHelpMenus:(QMainWindow*) mainWindow helpMenu:(QMenu*) helpMenu;
@end

View File

@@ -18,6 +18,8 @@
#import "AppKitImpl.h"
#import <QWindow>
#import <QMenu>
#import <QMenuBar>
#import <Cocoa/Cocoa.h>
#if __clang_major__ >= 13 && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_VERSION_12_3
#import <ScreenCaptureKit/ScreenCaptureKit.h>
@@ -231,8 +233,29 @@
[window setSharingType: state ? NSWindowSharingNone : NSWindowSharingReadOnly];
}
- (void) configureWindowAndHelpMenus:(QMainWindow*) mainWindow helpMenu:(QMenu*) helpMenu
{
QMenu *qtWindowMenu = new QMenu(AppKit::tr("Window"));
NSMenu *nsWindowMenu = qtWindowMenu->toNSMenu();
QString minimizeStr = AppKit::tr("Minimize");
[nsWindowMenu addItemWithTitle:minimizeStr.toNSString() action:@selector(performMiniaturize:) keyEquivalent:@""];
QString zoomStr = AppKit::tr("Zoom");
[nsWindowMenu addItemWithTitle:zoomStr.toNSString() action:@selector(performZoom:) keyEquivalent:@""];
[nsWindowMenu addItem:[NSMenuItem separatorItem]];
QString bringAllToFrontStr = AppKit::tr("Bring All to Front");
[nsWindowMenu addItemWithTitle:bringAllToFrontStr.toNSString() action:@selector(arrangeInFront:) keyEquivalent:@""];
NSApp.windowsMenu = nsWindowMenu;
mainWindow->menuBar()->insertMenu(helpMenu->menuAction(), qtWindowMenu);
NSApp.helpMenu = helpMenu->toNSMenu();
}
@end
//
// ------------------------- C++ Trampolines -------------------------
//
@@ -312,3 +335,8 @@ void AppKit::setWindowSecurity(QWindow* window, bool state)
auto view = reinterpret_cast<NSView*>(window->winId());
[static_cast<id>(self) setWindowSecurity:view.window state:state];
}
void AppKit::configureWindowAndHelpMenus(QMainWindow* window, QMenu* helpMenu)
{
[static_cast<id>(self) configureWindowAndHelpMenus:window helpMenu:helpMenu];
}

View File

@@ -24,6 +24,7 @@
#include <QStandardPaths>
#include <QTimer>
#include <QWindow>
#include <QMenu>
#include <ApplicationServices/ApplicationServices.h>
@@ -202,6 +203,11 @@ void MacUtils::registerNativeEventFilter()
::InstallApplicationEventHandler(MacUtils::hotkeyHandler, 1, &eventSpec, this, nullptr);
}
void MacUtils::configureWindowAndHelpMenus(QMainWindow* mainWindow, QMenu* helpMenu)
{
return m_appkit->configureWindowAndHelpMenus(mainWindow, helpMenu);
}
bool MacUtils::registerGlobalShortcut(const QString& name, Qt::Key key, Qt::KeyboardModifiers modifiers, QString* error)
{
auto keycode = qtToNativeKeyCode(key);

View File

@@ -54,6 +54,8 @@ public:
void registerNativeEventFilter() override;
void configureWindowAndHelpMenus(QMainWindow* mainWindow, QMenu* helpMenu);
bool registerGlobalShortcut(const QString& name,
Qt::Key key,
Qt::KeyboardModifiers modifiers,

View File

@@ -158,7 +158,7 @@ void NixUtils::setLaunchAtStartup(bool enable)
<< QStringLiteral("TryExec=") << executeablePathOrName << '\n'
<< QStringLiteral("Icon=") << QApplication::applicationName().toLower() << '\n'
<< QStringLiteral("StartupWMClass=keepassxc") << '\n'
<< QStringLiteral("StartupNotify=true") << '\n'
<< QStringLiteral("StartupNotify=false") << '\n'
<< QStringLiteral("Terminal=false") << '\n'
<< QStringLiteral("Type=Application") << '\n'
<< QStringLiteral("Version=1.0") << '\n'

View File

@@ -56,9 +56,9 @@ namespace
// but those cases with high propability constructed examples and very rare in real usage
const auto* sourceReference = sourceDb->rootGroup()->findEntryByUuid(targetEntry->uuid());
const auto resolvedValue = sourceReference->resolveMultiplePlaceholders(standardValue);
targetEntry->setUpdateTimeinfo(false);
targetEntry->beginUpdate();
targetEntry->attributes()->set(attribute, resolvedValue, targetEntry->attributes()->isProtected(attribute));
targetEntry->setUpdateTimeinfo(true);
targetEntry->endUpdate();
}
}

View File

@@ -17,6 +17,7 @@
*/
#include "BinaryStream.h"
#include "core/Tools.h"
#include <QtEndian>
BinaryStream::BinaryStream(QIODevice* device)
@@ -116,9 +117,16 @@ bool BinaryStream::readString(QByteArray& ba)
return false;
}
// Don't attempt to read strings over 10 MiB
if (length > 1024 * 1024 * 10) {
m_error = tr("String length exceeds 10 MiB limit (requested %1)").arg(Tools::humanReadableFileSize(length, 0));
return false;
}
ba.resize(length);
if (!read(ba.data(), ba.length())) {
m_error = tr("Failed to read string data: %1").arg(m_device->errorString());
return false;
}

View File

@@ -247,9 +247,10 @@ bool OpenSSHKey::parsePKCS1PEM(const QByteArray& in)
return false;
}
stream.readString(m_cipherName);
stream.readString(m_kdfName);
stream.readString(m_kdfOptions);
if (!stream.readString(m_cipherName) || !stream.readString(m_kdfName) || !stream.readString(m_kdfOptions)) {
m_error = tr("Failed to read key file: %1").arg(stream.errorString());
return false;
}
quint32 numberOfKeys;
stream.read(numberOfKeys);
@@ -262,7 +263,7 @@ bool OpenSSHKey::parsePKCS1PEM(const QByteArray& in)
for (quint32 i = 0; i < numberOfKeys; ++i) {
QByteArray publicKey;
if (!stream.readString(publicKey)) {
m_error = tr("Failed to read public key.");
m_error = tr("Failed to read public key: %1").arg(stream.errorString());
return false;
}

View File

@@ -123,6 +123,20 @@ void TestAutoType::init()
m_entry5->setPassword("example5");
m_entry5->setTitle("some title");
m_entry5->setUrl("http://example.org");
m_entry6 = new Entry();
m_entry6->setGroup(m_group);
m_entry6->setPassword("example6");
m_entry6->setTitle("empty window test");
association.window = "";
association.sequence = "{S:Empty Window}";
m_entry6->autoTypeAssociations()->add(association);
association.window = "non-matching window";
association.sequence = "should not match";
m_entry6->autoTypeAssociations()->add(association);
association.window = "*notepad*";
association.sequence = "{USERNAME}";
m_entry6->autoTypeAssociations()->add(association);
}
void TestAutoType::cleanup()
@@ -444,3 +458,13 @@ void TestAutoType::testAutoTypeEffectiveSequences()
QCOMPARE(entry6->defaultAutoTypeSequence(), sequenceOrphan);
QCOMPARE(entry6->effectiveAutoTypeSequence(), QString());
}
void TestAutoType::testAutoTypeEmptyWindowAssociation()
{
auto assoc = m_entry6->autoTypeSequences("Windows Notepad");
QCOMPARE(assoc.size(), 2);
QVERIFY(assoc.contains("{S:Empty Window}"));
assoc = m_entry6->autoTypeSequences("Some Other Window");
QVERIFY(assoc.isEmpty());
}

View File

@@ -51,6 +51,7 @@ private slots:
void testAutoTypeResults_data();
void testAutoTypeSyntaxChecks();
void testAutoTypeEffectiveSequences();
void testAutoTypeEmptyWindowAssociation();
private:
AutoTypePlatformInterface* m_platform;
@@ -64,6 +65,7 @@ private:
Entry* m_entry3;
Entry* m_entry4;
Entry* m_entry5;
Entry* m_entry6;
};
#endif // KEEPASSX_TESTAUTOTYPE_H

View File

@@ -17,6 +17,7 @@
#include "TestConfig.h"
#include <QSettings>
#include <QTest>
#include "config-keepassx-tests.h"
@@ -40,3 +41,39 @@ void TestConfig::testUpgrade()
tempFile.remove();
}
void TestConfig::testURLDoubleClickMigration()
{
// Test migration from OpenURLOnDoubleClick to URLDoubleClickAction
TemporaryFile tempFile;
tempFile.open();
// Create a config with old setting = true (open browser)
QSettings oldConfig(tempFile.fileName(), QSettings::IniFormat);
oldConfig.setValue("OpenURLOnDoubleClick", true);
oldConfig.sync();
tempFile.close();
Config::createConfigFromFile(tempFile.fileName());
// Should migrate to URLDoubleClickAction = 0 (open browser)
QCOMPARE(config()->get(Config::URLDoubleClickAction).toInt(), 0);
tempFile.remove();
// Test migration from OpenURLOnDoubleClick = false (edit entry)
TemporaryFile tempFile2;
tempFile2.open();
QSettings oldConfig2(tempFile2.fileName(), QSettings::IniFormat);
oldConfig2.setValue("OpenURLOnDoubleClick", false);
oldConfig2.sync();
tempFile2.close();
Config::createConfigFromFile(tempFile2.fileName());
// Should migrate to URLDoubleClickAction = 2 (edit entry)
QCOMPARE(config()->get(Config::URLDoubleClickAction).toInt(), 2);
tempFile2.remove();
}

View File

@@ -25,6 +25,7 @@ class TestConfig : public QObject
Q_OBJECT
private slots:
void testUpgrade();
void testURLDoubleClickMigration();
};
#endif // KEEPASSX_TESTCONFIG_H

View File

@@ -428,3 +428,26 @@ void TestTools::testIsTextMimeType()
QVERIFY(!Tools::isTextMimeType(noText));
}
}
// Test sanitization logic for Tools::cleanUsername
void TestTools::testCleanUsername()
{
// Test vars
QFETCH(QString, input);
QFETCH(QString, expected);
qputenv("USER", input.toUtf8());
qputenv("USERNAME", input.toUtf8());
QCOMPARE(Tools::cleanUsername(), expected);
}
void TestTools::testCleanUsername_data()
{
QTest::addColumn<QString>("input");
QTest::addColumn<QString>("expected");
QTest::newRow("Leading and trailing spaces") << " user " << "user";
QTest::newRow("Special characters") << R"(user<>:"/\|?*name)" << "user_________name";
QTest::newRow("Trailing dots and spaces") << "username... " << "username";
QTest::newRow("Combination of issues") << R"( user<>:"/\|?*name... )" << "user_________name";
}

View File

@@ -41,6 +41,8 @@ private slots:
void testGetMimeType();
void testGetMimeTypeByFileInfo();
void testIsTextMimeType();
void testCleanUsername();
void testCleanUsername_data();
};
#endif // KEEPASSX_TESTTOOLS_H