Compare commits

..

3 Commits

Author SHA1 Message Date
Jonathan White
66ff42c78b Final fixes 2025-11-23 18:26:38 +01:00
copilot-swe-agent[bot]
fc5f504509 Implement fix for auto-closing database unlock dialog when file is unavailable
Co-authored-by: droidmonkey <2809491+droidmonkey@users.noreply.github.com>
2025-11-23 18:26:35 +01:00
copilot-swe-agent[bot]
6c963e0000 Initial plan for issue 2025-11-23 18:26:32 +01:00
14 changed files with 137 additions and 252 deletions

View File

@@ -3,75 +3,6 @@
## 2.8.0 (Pending)
* Placeholder for future release notes
## 2.7.11 (2025-11-23)
### Changes
- Add image, HTML, Markdown preview, and text editing support to inline attachment viewer [#12085, #12244, #12654]
- Add database merge confirmation dialog [#10173]
- Add option to auto-generate a password for new entries [#12593]
- Add support for group sync in KeeShare [#11593]
- Add {UUID} placeholder for use in references [#12511]
- Add “Wait for Enter” search option [#12263]
- Add keyboard shortcut to “Jump to Group” from search results [#12225]
- Add predefined search for TOTP entries [#12199]
- Add confirmation when closing database via ESC key [#11963]
- Add support for escaping placeholder expressions [#11904]
- Reduce tab indentation width in notes fields [#11919]
- Cap default Argon2 parallelism when creating a new database [#11853]
- Database lock after inactivity now enabled by default and set to 900 seconds [#12689, #12609]
- Copying TOTP now opens setup dialog if none is configured for entry [#12584]
- Make double click action configurable [#12322]
- Remove unused “Last Accessed” from GUI [#12602]
- Auto-Type: Add more granular confirmation settings [#12370]
- Auto-Type: Add URL typing preset and add copy options to menu [#12341]
- Browser: Do not allow sites automatically if entry added from browser extension [#12413]
- Browser: Add options to restrict exposed groups [#9852, #12119]
- Bitwarden Import: Add support for timestamps and password history [#12588]
- macOS: Add Liquid Glass icon [#12642]
- macOS: Remove theme-based menubar icon toggle [#12685]
- macOS: Add Window and Help menus [#12357]
- Windows: Add option to add KeePassXC to PATH during installation [#12171]
### Fixes
- Fix window geometry not being restored properly when KeePassXC starts in tray [#12683]
- Fix potential database truncation when using direct write save method with YubiKeys [#11841]
- Fix issue with database backup saving [#11874]
- Fix UI lockups during startup with multiple tabs [#12053]
- Fix keyboard shortcuts when menubar is hidden [#12431]
- Fix clipboard being cleared on exit even if no password was copied [#12603]
- Fix single-instance detection when username contains invalid filename characters [#12559]
- Fix “Search Wait for Enter” setting not being save [#12614]
- Fix hotkey accelerators not being escaped properly on database tabs [#12630]
- Fix confusing error if user cancels out of key file edit dialog [#12639]
- Fix issues with saved searches and “Press Enter to Search” option [#12314]
- Fix URL wildcard matching [#12257]
- Fix TOTP visibility on unlock and settings change [#12220]
- Fix KeeShare entries with reference attributes not updating [#11809]
- Fix sort order not being maintained when toggling filters in database reports [#11849]
- Fix several UI font and layout issues [#11967, #12102]
- Prevent mouse wheel scroll on edit username field [#12398]
- Improve base translation consistency [#12432]
- Improve inactivity timer [#12246]
- Documentation improvements [#12373, #12506]
- Browser: Fix ordering of clientDataJSON in Passkey response object [#12120]
- Browser: Fix URL matching for additional URLs [#12196]
- Browser: Fix group settings inheritance [#12368]
- Browser: Allow read-only native messaging config files [#12236]
- Browser: Optimise entry iteration in browser access control dialog [#11817]
- Browser: Fix “Do not ask permission for HTTP Basic Auth” option [#11871]
- Browser: Fix native messaging path for Tor Browser launcher on Linux [#12005]
- Auto-Type: Fix empty window behaviour [#12622]
- Auto-Type: Take delays into account when typing TOTP [#12691]
- SSH Agent: Fix out-of-memory crash with malformed SSH keys [#12606]
- CSV Import: Fix modified and creation time import [#12379]
- CSV Import: Fix duplication of root groups on import [#12240]
- Proton Pass Import: Fix email addresses not being imported when no username set [#11888]
- macOS: Fix secure input getting stuck [#11928]
- Windows: Prevent launch as SYSTEM user from MSI installer [#12705]
- Windows: Remove broken check for MSVC Redistributable from MSI installer [#11950]
- Linux: Fix startup delay due to StartupNotify setting in desktop file [#12306]
- Linux: Fix memory initialisation when --pw-stdin is used with a pipe [#12050]
## 2.7.10 (2025-03-02)
### Changes

View File

@@ -31,7 +31,7 @@ if(NOT CPACK_PACKAGE_FILES) # PRE_BUILD: Sign binaries
# Sign all binaries
execute_process(
COMMAND xcrun codesign --sign=${CODESIGN_IDENTITY} --force --options=runtime --deep "${APP_DIR}"
COMMAND xcrun codesign --sign=${CODESIGN_IDENTITY} --force --options=runtime --deep ${APP_DIR}
RESULT_VARIABLE SIGN_RESULT
OUTPUT_VARIABLE SIGN_OUTPUT
ERROR_VARIABLE SIGN_ERROR
@@ -45,7 +45,7 @@ if(NOT CPACK_PACKAGE_FILES) # PRE_BUILD: Sign binaries
# (Re-)Sign main executable with --entitlements
execute_process(
COMMAND xcrun codesign --sign=${CODESIGN_IDENTITY} --force --options=runtime --entitlements=${ENTITLEMENTS} "${APP_DIR}/Contents/MacOS/${PROGNAME}"
COMMAND xcrun codesign --sign=${CODESIGN_IDENTITY} --force --options=runtime --deep --entitlements=${ENTITLEMENTS} ${APP_DIR}
RESULT_VARIABLE SIGN_RESULT
OUTPUT_VARIABLE SIGN_OUTPUT
ERROR_VARIABLE SIGN_ERROR
@@ -61,41 +61,42 @@ if(NOT CPACK_PACKAGE_FILES) # PRE_BUILD: Sign binaries
else() # POST_BUILD: Notarize DMG
set(KEYCHAIN_PROFILE "@WITH_XC_NOTARY_KEYCHAIN_PROFILE@")
file(GLOB_RECURSE DMG_FILE "${CPACK_PACKAGE_DIRECTORY}/${CPACK_PACKAGE_FILE_NAME}.dmg")
if(NOT KEYCHAIN_PROFILE)
message(FATAL_ERROR "No notarization credentials keychain profile specified.")
endif()
foreach(DMG_FILE ${CPACK_PACKAGE_FILES})
# Submit for notarization
message(STATUS "Submitting DMG bundle for notarization, this may take while...")
execute_process(
COMMAND xcrun notarytool submit --keychain-profile=${KEYCHAIN_PROFILE} --wait "${DMG_FILE}"
RESULT_VARIABLE NOTARIZE_RESULT
OUTPUT_VARIABLE NOTARIZE_OUTPUT
ERROR_VARIABLE NOTARIZE_ERROR
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
ECHO_OUTPUT_VARIABLE
)
if (NOT NOTARIZE_RESULT EQUAL 0)
message(FATAL_ERROR "Notarization failed: ${NOTARIZE_ERROR}")
endif()
message(STATUS "DMG bundle notarized successfully.")
# Submit for notarization
message(STATUS "Submitting DMG bundle for notarization, this may take while...")
execute_process(
COMMAND xcrun notarytool submit --keychain-profile=${KEYCHAIN_PROFILE} --wait ${DMG_FILE}
RESULT_VARIABLE NOTARIZE_RESULT
OUTPUT_VARIABLE NOTARIZE_OUTPUT
ERROR_VARIABLE NOTARIZE_ERROR
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
ECHO_OUTPUT_VARIABLE
)
if (NOT NOTARIZE_RESULT EQUAL 0)
message(FATAL_ERROR "Notarization failed: ${NOTARIZE_ERROR}")
endif()
message(STATUS "DMG bundle notarized successfully.")
# Staple tickets
message(STATUS "Stapling notarization ticket...")
execute_process(
COMMAND xcrun stapler staple ${DMG_FILE} && xcrun stapler validate ${DMG_FILE}
RESULT_VARIABLE STAPLE_RESULT
OUTPUT_VARIABLE STAPLE_OUTPUT
ERROR_VARIABLE STAPLE_ERROR
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
ECHO_OUTPUT_VARIABLE
)
if (NOT STAPLE_RESULT EQUAL 0)
message(FATAL_ERROR "Stapling failed: ${STAPLE_ERROR}")
endif()
message(STATUS "DMG bundle notarization ticket stapled successfully.")
# Staple tickets
message(STATUS "Stapling notarization ticket...")
execute_process(
COMMAND xcrun stapler staple "${DMG_FILE}" && xcrun stapler validate "${DMG_FILE}"
RESULT_VARIABLE STAPLE_RESULT
OUTPUT_VARIABLE STAPLE_OUTPUT
ERROR_VARIABLE STAPLE_ERROR
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_STRIP_TRAILING_WHITESPACE
ECHO_OUTPUT_VARIABLE
)
if (NOT STAPLE_RESULT EQUAL 0)
message(FATAL_ERROR "Stapling failed: ${STAPLE_ERROR}")
endif()
message(STATUS "DMG bundle notarization ticket stapled successfully.")
endforeach()
endif()

View File

@@ -25,10 +25,12 @@ import lzma
import os
from pathlib import Path
import platform
import random
import re
import signal
import shutil
import stat
import string
import subprocess
import sys
import tarfile
@@ -41,7 +43,7 @@ from urllib.request import urlretrieve
###########################################################################################
# class Check(Command)
# class Tag(Command)
# class Merge(Command)
# class Build(Command)
# class BuildSrc(Command)
# class AppSign(Command)
@@ -129,8 +131,7 @@ fmt = LogFormatter()
console_handler = logging.StreamHandler()
console_handler.setFormatter(fmt)
logger = logging.getLogger(__file__)
logger.setLevel(os.getenv('LOGLEVEL')
if type(logging.getLevelName(os.environ.get('LOGLEVEL'))) is int else logging.INFO)
logger.setLevel(os.getenv('LOGLEVEL') if 'LOGLEVEL' in os.environ else logging.INFO)
logger.addHandler(console_handler)
###########################################################################################
@@ -187,12 +188,11 @@ def _run(cmd, *args, cwd, path=None, env=None, input=None, capture_output=True,
env['FORCE_COLOR'] = '1'
if docker_image:
cwd2 = Path(cwd or '.').absolute()
docker_cmd = ['docker', 'run', '--rm', '--tty=true', f'--workdir={cwd2}', f'--user={os.getuid()}:{os.getgid()}']
docker_cmd = ['docker', 'run', '--rm', '--tty=true', f'--workdir={cwd}', f'--user={os.getuid()}:{os.getgid()}']
docker_cmd.extend([f'--env={k}={v}' for k, v in env.items() if k in ['FORCE_COLOR', 'CC', 'CXX']])
if path:
docker_cmd.append(f'--env=PATH={path}')
docker_cmd.append(f'--volume={cwd2}:{cwd2}:rw')
docker_cmd.append(f'--volume={Path(cwd).absolute()}:{Path(cwd).absolute()}:rw')
if docker_mounts:
docker_cmd.extend([f'--volume={Path(d).absolute()}:{Path(d).absolute()}:rw' for d in docker_mounts])
if docker_privileged:
@@ -203,7 +203,7 @@ def _run(cmd, *args, cwd, path=None, env=None, input=None, capture_output=True,
cmd = docker_cmd + cmd
try:
logger.debug('Running command: %s', ' '.join(map(str, cmd)))
logger.debug('Running command: %s', ' '.join(cmd))
return subprocess.run(
cmd, *args,
input=input,
@@ -450,7 +450,6 @@ class Check(Command):
cls.check_version_in_cmake(version, src_dir)
cls.check_changelog(version, src_dir)
cls.check_app_stream_info(version, src_dir)
return git_ref
@staticmethod
def check_src_dir_exists(src_dir):
@@ -491,7 +490,7 @@ class Check(Command):
cmakelists = Path(cwd) / cmakelists
if not cmakelists.is_file():
raise Error('File not found: %s', cmakelists)
cmakelists_text = cmakelists.read_text("UTF-8")
cmakelists_text = cmakelists.read_text()
major = re.search(r'^set\(KEEPASSXC_VERSION_MAJOR "(\d+)"\)$', cmakelists_text, re.MULTILINE).group(1)
minor = re.search(r'^set\(KEEPASSXC_VERSION_MINOR "(\d+)"\)$', cmakelists_text, re.MULTILINE).group(1)
patch = re.search(r'^set\(KEEPASSXC_VERSION_PATCH "(\d+)"\)$', cmakelists_text, re.MULTILINE).group(1)
@@ -507,7 +506,7 @@ class Check(Command):
if not changelog.is_file():
raise Error('File not found: %s', changelog)
major, minor, patch = _split_version(version)
if not re.search(rf'^## {major}\.{minor}\.{patch} \(.+?\)\n+', changelog.read_text("UTF-8"), re.MULTILINE):
if not re.search(rf'^## {major}\.{minor}\.{patch} \(.+?\)\n+', changelog.read_text(), re.MULTILINE):
raise Error(f'{changelog} has not been updated to the "%s" release.', version)
@staticmethod
@@ -542,8 +541,8 @@ class Check(Command):
raise Error('xcrun command not found! Please check that you have correctly installed Xcode.')
class Tag(Command):
"""Update translations and tag release."""
class Merge(Command):
"""Merge release branch into main branch and create release tags."""
@classmethod
def setup_arg_parser(cls, parser: argparse.ArgumentParser):
@@ -565,7 +564,7 @@ class Tag(Command):
skip_translations, tx_resource, tx_min_perc):
major, minor, patch = _split_version(version)
Check.perform_basic_checks(src_dir)
release_branch = Check.perform_version_checks(version, src_dir, release_branch)
Check.perform_version_checks(version, src_dir, release_branch)
Check.check_gnupg()
sign_key = GPGSign.get_secret_key(sign_key)
@@ -576,7 +575,7 @@ class Tag(Command):
commit=True, yes=yes)
changelog = re.search(rf'^## ({major}\.{minor}\.{patch} \(.*?\)\n\n+.+?)\n\n+## ',
(Path(src_dir) / 'CHANGELOG.md').read_text("UTF-8"), re.MULTILINE | re.DOTALL)
(Path(src_dir) / 'CHANGELOG.md').read_text(), re.MULTILINE | re.DOTALL)
if not changelog:
raise Error(f'No changelog entry found for version {version}.')
changelog = 'Release ' + changelog.group(1)
@@ -828,8 +827,6 @@ class Build(Command):
if appimage:
cmake_opts.append('-DKEEPASSXC_DIST_TYPE=AppImage')
# Force install prefix to ensure proper AppDir structure for linuxdeploy
install_prefix = '/usr'
with tempfile.TemporaryDirectory() as build_dir:
logger.info('Configuring build...')
@@ -847,7 +844,7 @@ class Build(Command):
_run(['cmake', '--install', '.', '--strip',
'--prefix', (app_dir.absolute() / install_prefix.lstrip('/')).as_posix()],
cwd=build_dir, capture_output=False, **docker_args)
shutil.copytree(app_dir, output_dir / app_dir.name, symlinks=True, dirs_exist_ok=True)
shutil.copytree(app_dir, output_dir / app_dir.name, symlinks=True)
if appimage:
self._build_linux_appimage(
@@ -888,7 +885,7 @@ class Build(Command):
_run(['linuxdeploy', '--plugin=qt', f'--appdir={app_dir}', f'--custom-apprun={app_run}',
f'--desktop-file={desktop_file}', f'--icon-file={icon_file}',
*[f'--executable={ex}' for ex in executables]],
cwd=build_dir, capture_output=False, path=env_path, **docker_args, docker_privileged=True)
cwd=build_dir, capture_output=False, path=env_path, **docker_args)
logger.debug('Running appimagetool...')
appimage_name = f'KeePassXC-{version}-{platform_target}.AppImage'
@@ -1199,9 +1196,9 @@ def main():
Check.setup_arg_parser(check_parser)
check_parser.set_defaults(_cmd=Check)
merge_parser = subparsers.add_parser('tag', help=Tag.__doc__)
Tag.setup_arg_parser(merge_parser)
merge_parser.set_defaults(_cmd=Tag)
merge_parser = subparsers.add_parser('merge', help=Merge.__doc__)
Merge.setup_arg_parser(merge_parser)
merge_parser.set_defaults(_cmd=Merge)
build_parser = subparsers.add_parser('build', help=Build.__doc__)
Build.setup_arg_parser(build_parser)

View File

@@ -46,78 +46,9 @@
</screenshots>
<releases>
<release version="2.8.0" date="2025-01-01" type="development">
<description>
<ul>
<li>Placeholder for future release notes</li>
</ul>
</description>
</release>
<release version="2.7.11" date="2025-11-23" type="stable">
<description>
<ul>
<li>Add image, HTML, Markdown preview, and text editing support to inline attachment viewer [#12085, #12244, #12654]</li>
<li>Add database merge confirmation dialog [#10173]</li>
<li>Add option to auto-generate a password for new entries [#12593]</li>
<li>Add support for group sync in KeeShare [#11593]</li>
<li>Add {UUID} placeholder for use in references [#12511]</li>
<li>Add “Wait for Enter” search option [#12263]</li>
<li>Add keyboard shortcut to “Jump to Group” from search results [#12225]</li>
<li>Add predefined search for TOTP entries [#12199]</li>
<li>Add confirmation when closing database via ESC key [#11963]</li>
<li>Add support for escaping placeholder expressions [#11904]</li>
<li>Reduce tab indentation width in notes fields [#11919]</li>
<li>Cap default Argon2 parallelism when creating a new database [#11853]</li>
<li>Database lock after inactivity now enabled by default and set to 900 seconds [#12689, #12609]</li>
<li>Copying TOTP now opens setup dialog if none is configured for entry [#12584]</li>
<li>Make double click action configurable [#12322]</li>
<li>Remove unused “Last Accessed” from GUI [#12602]</li>
<li>Auto-Type: Add more granular confirmation settings [#12370]</li>
<li>Auto-Type: Add URL typing preset and add copy options to menu [#12341]</li>
<li>Browser: Do not allow sites automatically if entry added from browser extension [#12413]</li>
<li>Browser: Add options to restrict exposed groups [#9852, #12119]</li>
<li>Bitwarden Import: Add support for timestamps and password history [#12588]</li>
<li>macOS: Add Liquid Glass icon [#12642]</li>
<li>macOS: Remove theme-based menubar icon toggle [#12685]</li>
<li>macOS: Add Window and Help menus [#12357]</li>
<li>Windows: Add option to add KeePassXC to PATH during installation [#12171]</li>
<li>Fix window geometry not being restored properly when KeePassXC starts in tray [#12683]</li>
<li>Fix potential database truncation when using direct write save method with YubiKeys [#11841]</li>
<li>Fix issue with database backup saving [#11874]</li>
<li>Fix UI lockups during startup with multiple tabs [#12053]</li>
<li>Fix keyboard shortcuts when menubar is hidden [#12431]</li>
<li>Fix clipboard being cleared on exit even if no password was copied [#12603]</li>
<li>Fix single-instance detection when username contains invalid filename characters [#12559]</li>
<li>Fix “Search Wait for Enter” setting not being save [#12614]</li>
<li>Fix hotkey accelerators not being escaped properly on database tabs [#12630]</li>
<li>Fix confusing error if user cancels out of key file edit dialog [#12639]</li>
<li>Fix issues with saved searches and “Press Enter to Search” option [#12314]</li>
<li>Fix URL wildcard matching [#12257]</li>
<li>Fix TOTP visibility on unlock and settings change [#12220]</li>
<li>Fix KeeShare entries with reference attributes not updating [#11809]</li>
<li>Fix sort order not being maintained when toggling filters in database reports [#11849]</li>
<li>Fix several UI font and layout issues [#11967, #12102]</li>
<li>Prevent mouse wheel scroll on edit username field [#12398]</li>
<li>Improve base translation consistency [#12432]</li>
<li>Improve inactivity timer [#12246]</li>
<li>Documentation improvements [#12373, #12506]</li>
<li>Browser: Fix ordering of clientDataJSON in Passkey response object [#12120]</li>
<li>Browser: Fix URL matching for additional URLs [#12196]</li>
<li>Browser: Fix group settings inheritance [#12368]</li>
<li>Browser: Allow read-only native messaging config files [#12236]</li>
<li>Browser: Optimise entry iteration in browser access control dialog [#11817]</li>
<li>Browser: Fix “Do not ask permission for HTTP Basic Auth” option [#11871]</li>
<li>Browser: Fix native messaging path for Tor Browser launcher on Linux [#12005]</li>
<li>Auto-Type: Fix empty window behaviour [#12622]</li>
<li>Auto-Type: Take delays into account when typing TOTP [#12691]</li>
<li>SSH Agent: Fix out-of-memory crash with malformed SSH keys [#12606]</li>
<li>CSV Import: Fix modified and creation time import [#12379]</li>
<li>CSV Import: Fix duplication of root groups on import [#12240]</li>
<li>Proton Pass Import: Fix email addresses not being imported when no username set [#11888]</li>
<li>macOS: Fix secure input getting stuck [#11928]</li>
<li>Windows: Prevent launch as SYSTEM user from MSI installer [#12705]</li>
<li>Windows: Remove broken check for MSVC Redistributable from MSI installer [#11950]</li>
<li>Linux: Fix startup delay due to StartupNotify setting in desktop file [#12306]</li>
<li>Linux: Fix memory initialisation when --pw-stdin is used with a pipe [#12050]</li>
<li>Placeholder for future release notes</li>
</ul>
</description>
</release>

View File

@@ -1799,6 +1799,10 @@ Are you sure you want to continue with this file?.</source>
<source>Press ESC again to close this database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>The database file does not exist or is not accessible.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DatabaseSettingWidgetMetaData</name>
@@ -2608,10 +2612,6 @@ This is definitely a bug, please report it to the developers.</source>
<source>Open database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to open %1. It either does not exist or is not accessible.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>CSV file</source>
<translation type="unfinished"></translation>

View File

@@ -32,7 +32,6 @@
#include "core/Global.h"
#include "core/Resources.h"
#include "core/Tools.h"
#include "core/Totp.h"
#include "gui/MainWindow.h"
#include "gui/MessageBox.h"
#include "gui/osutils/OSUtils.h"
@@ -312,6 +311,9 @@ void AutoType::executeAutoTypeActions(const Entry* entry,
// Restore executor mode
m_executor->mode = mode;
int delay = qMax(100, config()->get(Config::AutoTypeStartDelay).toInt());
Tools::wait(delay);
// Grab the current active window after everything settles
if (window == 0) {
window = m_plugin->activeWindow();
@@ -343,8 +345,7 @@ void AutoType::executeAutoTypeActions(const Entry* entry,
break;
}
// Retry wait delay
Tools::wait(100);
Tools::wait(delay);
}
// Last action failed to complete, cancel the rest of the sequence
@@ -545,14 +546,10 @@ AutoType::parseSequence(const QString& entrySequence, const Entry* entry, QStrin
const int maxTypeDelay = 500;
const int maxWaitDelay = 10000;
const int maxRepetition = 100;
int currentTypingDelay = qBound(0, config()->get(Config::AutoTypeDelay).toInt(), maxTypeDelay);
int cumulativeDelay = qBound(0, config()->get(Config::AutoTypeStartDelay).toInt(), maxWaitDelay);
// Initial actions include start delay and initial inter-key delay
QList<QSharedPointer<AutoTypeAction>> actions;
actions << QSharedPointer<AutoTypeBegin>::create();
actions << QSharedPointer<AutoTypeDelay>::create(currentTypingDelay, true);
actions << QSharedPointer<AutoTypeDelay>::create(cumulativeDelay);
actions << QSharedPointer<AutoTypeDelay>::create(qMax(0, config()->get(Config::AutoTypeDelay).toInt()), true);
// Replace escaped braces with a template for easier regex
QString sequence = entrySequence;
@@ -568,7 +565,7 @@ AutoType::parseSequence(const QString& entrySequence, const Entry* entry, QStrin
// Group 1 = modifier key (opt)
// Group 2 = full placeholder
// Group 3 = inner placeholder (allows nested placeholders)
// Group 4 = repeat / delay time (opt)
// Group 4 = repeat (opt)
// Group 5 = character
QRegularExpression regex("([+%^#]*)(?:({((?>[^{}]+?|(?2))+?)(?:\\s+(\\d+))?})|(.))");
auto results = regex.globalMatch(sequence);
@@ -630,23 +627,19 @@ AutoType::parseSequence(const QString& entrySequence, const Entry* entry, QStrin
}
actions << QSharedPointer<AutoTypeDelay>::create(qBound(0, delay, maxTypeDelay), true);
} else if (placeholder == "delay") {
// Mid typing delay (wait), repeat represents the desired delay in milliseconds
// Mid typing delay (wait)
if (repeat > maxWaitDelay) {
error = tr("Very long delay detected, max is %1: %2").arg(maxWaitDelay).arg(fullPlaceholder);
return {};
}
cumulativeDelay += repeat;
actions << QSharedPointer<AutoTypeDelay>::create(qBound(0, repeat, maxWaitDelay));
} else if (placeholder == "clearfield") {
// Platform-specific field clearing
actions << QSharedPointer<AutoTypeClearField>::create();
} else if (placeholder == "totp") {
if (entry->hasValidTotp()) {
// Calculate TOTP at the time of typing including delays
bool isValid = false;
auto time =
Clock::currentSecondsSinceEpoch() + (cumulativeDelay + currentTypingDelay * actions.count()) / 1000;
auto totp = Totp::generateTotp(entry->totpSettings(), &isValid, time);
// Entry totp (requires special handling)
QString totp = entry->totp();
for (const auto& ch : totp) {
actions << QSharedPointer<AutoTypeKey>::create(ch);
}

View File

@@ -38,6 +38,7 @@
namespace
{
constexpr int clearFormsDelay = 30000;
constexpr int fileExistsCheckInterval = 5000;
bool isQuickUnlockAvailable()
{
@@ -68,6 +69,16 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
m_ui->editPassword->setShowPassword(false);
});
m_fileExistsTimer.setInterval(fileExistsCheckInterval);
m_fileExistsTimer.setSingleShot(false);
connect(&m_fileExistsTimer, &QTimer::timeout, this, [this] {
if (!QFile::exists(m_filename)) {
m_ui->messageWidget->showMessage(tr("The database file does not exist or is not accessible."),
MessageWidget::Warning,
fileExistsCheckInterval + 500);
}
});
QFont font;
font.setPointSize(font.pointSize() + 4);
font.setBold(true);
@@ -215,6 +226,7 @@ bool DatabaseOpenWidget::event(QEvent* event)
}
if (isVisible()) {
m_fileExistsTimer.start();
m_hideTimer.stop();
pollHardwareKey();
}
@@ -226,6 +238,8 @@ bool DatabaseOpenWidget::event(QEvent* event)
m_hideTimer.start();
}
m_fileExistsTimer.stop();
#ifdef WITH_XC_YUBIKEY
if (type == QEvent::Hide) {
m_deviceListener->deregisterAllHotplugCallbacks();

View File

@@ -97,6 +97,7 @@ private:
bool m_triedToQuit = false;
QTimer m_hideTimer;
QTimer m_hideNoHardwareKeysFoundTimer;
QTimer m_fileExistsTimer;
Q_DISABLE_COPY(DatabaseOpenWidget)
};

View File

@@ -163,22 +163,29 @@ void DatabaseTabWidget::addDatabaseTab(const QString& filePath,
QString canonicalFilePath = fileInfo.canonicalFilePath();
if (canonicalFilePath.isEmpty()) {
emit messageGlobal(tr("Failed to open %1. It either does not exist or is not accessible.").arg(cleanFilePath),
MessageWidget::Error);
return;
// The file does not exist, revert back to the cleaned path for comparison
canonicalFilePath = cleanFilePath;
}
// Try to find an existing tab with the same file path
for (int i = 0, c = count(); i < c; ++i) {
auto* dbWidget = databaseWidgetFromIndex(i);
Q_ASSERT(dbWidget);
if (dbWidget
&& dbWidget->database()->canonicalFilePath().compare(canonicalFilePath, FILE_CASE_SENSITIVE) == 0) {
dbWidget->performUnlockDatabase(password, keyfile);
if (!inBackground) {
// switch to existing tab if file is already open
setCurrentIndex(indexOf(dbWidget));
if (dbWidget) {
auto dbFilePath = dbWidget->database()->canonicalFilePath();
if (dbFilePath.isEmpty()) {
// The file does not exist, revert back to the cleaned path for comparison
dbFilePath = QDir::toNativeSeparators(dbWidget->database()->filePath());
}
if (dbFilePath.compare(canonicalFilePath, FILE_CASE_SENSITIVE) == 0) {
// Attempt to unlock the database if password and/or keyfile is provided
dbWidget->performUnlockDatabase(password, keyfile);
if (!inBackground) {
// switch to existing tab if file is already open
setCurrentIndex(indexOf(dbWidget));
}
// Prevent opening a new tab for this file
return;
}
return;
}
}

View File

@@ -714,7 +714,7 @@ void MainWindow::restoreConfigState()
if (config()->get(Config::OpenPreviousDatabasesOnStartup).toBool()) {
const QStringList fileNames = config()->get(Config::LastOpenedDatabases).toStringList();
for (const QString& filename : fileNames) {
if (!filename.isEmpty() && QFile::exists(filename)) {
if (!filename.isEmpty()) {
openDatabase(filename);
}
}

View File

@@ -27,7 +27,6 @@
#include "core/Config.h"
#include "core/Group.h"
#include "core/Resources.h"
#include "core/Totp.h"
#include "crypto/Crypto.h"
#include "gui/MessageBox.h"
#include "gui/osutils/OSUtils.h"
@@ -76,9 +75,6 @@ void TestAutoType::init()
association.window = "custom window";
association.sequence = "{username}association{password}";
m_entry1->autoTypeAssociations()->add(association);
// Create a totp with a short time step to test delayed typing
auto totpSettings = Totp::createSettings("NNSWK4DBONZXQYZB", Totp::DEFAULT_DIGITS, 2);
m_entry1->setTotp(totpSettings);
m_entry2 = new Entry();
m_entry2->setGroup(m_group);
@@ -474,24 +470,3 @@ void TestAutoType::testAutoTypeEmptyWindowAssociation()
assoc = m_entry6->autoTypeSequences("Some Other Window");
QVERIFY(assoc.isEmpty());
}
void TestAutoType::testAutoTypeTotpDelay()
{
// Get the TOTP time step in milliseconds
auto totpStep = m_entry1->totpSettings()->step * 1000;
auto sequence = QString("{TOTP} {DELAY %1}{TOTP}").arg(QString::number(totpStep * 2));
// Test 1: Sequence with a 3 second delay before TOTP
m_autoType->performAutoTypeWithSequence(m_entry1, sequence);
auto typedChars = m_test->actionChars();
// The typed TOTP should be different between the first and second one
auto totpParts = m_test->actionChars().split(' ');
QCOMPARE(totpParts.size(), 2);
QCOMPARE(totpParts[0].size(), m_entry1->totpSettings()->digits);
QCOMPARE(totpParts[1].size(), m_entry1->totpSettings()->digits);
QVERIFY2(totpParts[0] != totpParts[1],
QString("Typed TOTP (%1) should differ from current TOTP (%2) due to delay")
.arg(totpParts[0], totpParts[1])
.toLatin1());
}

View File

@@ -52,7 +52,6 @@ private slots:
void testAutoTypeSyntaxChecks();
void testAutoTypeEffectiveSequences();
void testAutoTypeEmptyWindowAssociation();
void testAutoTypeTotpDelay();
private:
AutoTypePlatformInterface* m_platform;

View File

@@ -2464,6 +2464,41 @@ void TestGui::testMenuActionStates()
QVERIFY(isActionEnabled("actionPasswordGenerator"));
}
void TestGui::testOpenMissingDatabaseFile()
{
// Test that when trying to open a non-existent database file,
// the unlock dialog is still shown (instead of auto-closing)
// This allows user to retry when the file becomes available (e.g., cloud storage mounting)
const QString nonExistentPath = "/tmp/does_not_exist.kdbx";
// Ensure the file doesn't exist
QFile::remove(nonExistentPath);
QVERIFY(!QFile::exists(nonExistentPath));
// Record initial tab count
int initialTabCount = m_tabWidget->count();
// Try to add database tab with non-existent file
// This should NOT fail but should create a tab and show unlock dialog
m_tabWidget->addDatabaseTab(nonExistentPath);
// Verify that a tab was created (unlock dialog shown)
QCOMPARE(m_tabWidget->count(), initialTabCount + 1);
// Get the database widget for the new tab
auto* dbWidget = m_tabWidget->currentDatabaseWidget();
QVERIFY(dbWidget);
// Verify the database is in a state where it can be unlocked
// (not closed/rejected due to missing file)
QVERIFY(dbWidget->isLocked());
// Close the tab to clean up
m_tabWidget->closeDatabaseTab(m_tabWidget->currentIndex());
QCOMPARE(m_tabWidget->count(), initialTabCount);
}
void TestGui::addCannedEntries()
{
// Find buttons

View File

@@ -71,6 +71,7 @@ private slots:
void testTrayRestoreHide();
void testShortcutConfig();
void testMenuActionStates();
void testOpenMissingDatabaseFile();
private:
void addCannedEntries();