diff --git a/CMakeLists.txt b/CMakeLists.txt
index 969b3727c..536b08d9d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -49,6 +49,7 @@ option(WITH_XC_YUBIKEY "Include YubiKey support." OFF)
option(WITH_XC_SSHAGENT "Include SSH agent support." OFF)
option(WITH_XC_KEESHARE "Sharing integration with KeeShare" OFF)
option(WITH_XC_KEESHARE_SECURE "Sharing integration with secured KeeShare containers" OFF)
+option(WITH_XC_UPDATECHECK "Include automatic update checks; disable for controlled distributions" ON)
if(APPLE)
option(WITH_XC_TOUCHID "Include TouchID support for macOS." OFF)
endif()
@@ -76,10 +77,15 @@ else()
set(WITH_XC_CRYPTO_SSH OFF)
endif()
+if(WITH_XC_UPDATECHECK)
+ set(WITH_XC_NETWORKING ON)
+endif()
+
set(KEEPASSXC_VERSION_MAJOR "2")
set(KEEPASSXC_VERSION_MINOR "4")
set(KEEPASSXC_VERSION_PATCH "0")
set(KEEPASSXC_VERSION "${KEEPASSXC_VERSION_MAJOR}.${KEEPASSXC_VERSION_MINOR}.${KEEPASSXC_VERSION_PATCH}")
+set(OVERRIDE_VERSION "" CACHE STRING "Override the KeePassXC Version for Snapshot builds")
set(KEEPASSXC_BUILD_TYPE "Snapshot" CACHE STRING "Set KeePassXC build type to distinguish between stable releases and snapshots")
set_property(CACHE KEEPASSXC_BUILD_TYPE PROPERTY STRINGS Snapshot Release PreRelease)
@@ -91,8 +97,10 @@ execute_process(COMMAND git rev-parse --short=7 HEAD
OUTPUT_VARIABLE GIT_HEAD
ERROR_QUIET)
string(STRIP "${GIT_HEAD}" GIT_HEAD)
-if(GIT_HEAD STREQUAL "")
+if(GIT_HEAD STREQUAL "" AND NOT GIT_HEAD_OVERRIDE STREQUAL "")
string(SUBSTRING "${GIT_HEAD_OVERRIDE}" 0 7 GIT_HEAD)
+elseif(EXISTS ${CMAKE_SOURCE_DIR}/.gitrev)
+ file(READ ${CMAKE_SOURCE_DIR}/.gitrev GIT_HEAD)
endif()
message(STATUS "Found Git HEAD Revision: ${GIT_HEAD}\n")
@@ -116,13 +124,16 @@ if(OVERRIDE_VERSION)
elseif(OVERRIDE_VERSION MATCHES "^[\\.0-9]+$")
set(KEEPASSXC_BUILD_TYPE Release)
set(KEEPASSXC_VERSION ${OVERRIDE_VERSION})
+ else()
+ set(KEEPASSXC_BUILD_TYPE Snapshot)
+ set(KEEPASSXC_VERSION ${OVERRIDE_VERSION})
+ endif()
+else()
+ if(KEEPASSXC_BUILD_TYPE STREQUAL "PreRelease")
+ set(KEEPASSXC_VERSION "${KEEPASSXC_VERSION}-preview")
+ elseif(KEEPASSXC_BUILD_TYPE STREQUAL "Snapshot")
+ set(KEEPASSXC_VERSION "${KEEPASSXC_VERSION}-snapshot")
endif()
-endif()
-
-if(KEEPASSXC_BUILD_TYPE STREQUAL "PreRelease" AND NOT OVERRIDE_VERSION)
- set(KEEPASSXC_VERSION "${KEEPASSXC_VERSION}-preview")
-elseif(KEEPASSXC_BUILD_TYPE STREQUAL "Snapshot")
- set(KEEPASSXC_VERSION "${KEEPASSXC_VERSION}-snapshot")
endif()
if(KEEPASSXC_BUILD_TYPE STREQUAL "Release")
diff --git a/README.md b/README.md
index 608dfd363..3048e2ea3 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ so please check out your distribution's package list to see if KeePassXC is avai
- Using website favicons as entry icons
- Merging of databases
- Automatic reload when the database changed on disk
-- Browser integration with KeePassXC-Browser using [native messaging](https://developer.chrome.com/extensions/nativeMessaging) for [Mozilla Firefox](https://addons.mozilla.org/en-US/firefox/addon/keepassxc-browser/) and [Google Chrome or Chromium](https://chrome.google.com/webstore/detail/keepassxc-browser/oboonakemofpalcgghocfoadofidjkkk)
+- Browser integration with KeePassXC-Browser using [native messaging](https://developer.chrome.com/extensions/nativeMessaging) for [Mozilla Firefox](https://addons.mozilla.org/en-US/firefox/addon/keepassxc-browser/) and [Google Chrome, Chromium, Vivaldi, or Brave](https://chrome.google.com/webstore/detail/keepassxc-browser/oboonakemofpalcgghocfoadofidjkkk)
- Synchronize passwords using KeeShare. See [Using Sharing](./docs/QUICKSTART.md#using-sharing) for more details.
- Many bug fixes
diff --git a/release-tool b/release-tool
index ab4128a35..821a1e8c1 100755
--- a/release-tool
+++ b/release-tool
@@ -813,13 +813,17 @@ build() {
init
OUTPUT_DIR="$(realpath "$OUTPUT_DIR")"
+ # Resolve appsign key to absolute path if under Windows
+ if [[ "${build_key}" && "$(uname -o)" == "Msys" ]]; then
+ build_key="$(realpath "${build_key}")"
+ fi
if ${build_snapshot}; then
TAG_NAME="HEAD"
local branch=`git rev-parse --abbrev-ref HEAD`
logInfo "Using current branch ${branch} to build..."
RELEASE_NAME="${RELEASE_NAME}-snapshot"
- CMAKE_OPTIONS="${CMAKE_OPTIONS} -DKEEPASSXC_BUILD_TYPE=Snapshot"
+ CMAKE_OPTIONS="${CMAKE_OPTIONS} -DKEEPASSXC_BUILD_TYPE=Snapshot -DOVERRIDE_VERSION=${RELEASE_NAME}"
else
checkWorkingTreeClean
@@ -848,14 +852,13 @@ build() {
git archive --format=tar "$TAG_NAME" --prefix="${prefix}/" --output="${OUTPUT_DIR}/${tarball_name}"
- if ! ${build_snapshot}; then
- # add .version file to tar
- mkdir "${prefix}"
- echo -n ${RELEASE_NAME} > "${prefix}/.version"
- tar --append --file="${OUTPUT_DIR}/${tarball_name}" "${prefix}/.version"
- rm "${prefix}/.version"
- rmdir "${prefix}" 2> /dev/null
- fi
+ # add .version and .gitrev files to tarball
+ mkdir "${prefix}"
+ echo -n ${RELEASE_NAME} > "${prefix}/.version"
+ echo -n `git rev-parse --short=7 HEAD` > "${prefix}/.gitrev"
+ tar --append --file="${OUTPUT_DIR}/${tarball_name}" "${prefix}/.version" "${prefix}/.gitrev"
+ rm "${prefix}/.version" "${prefix}/.gitrev"
+ rmdir "${prefix}" 2> /dev/null
xz -6 "${OUTPUT_DIR}/${tarball_name}"
fi
@@ -881,6 +884,8 @@ build() {
# linuxdeploy requires /usr as install prefix
INSTALL_PREFIX="/usr"
fi
+ # Do not build tests cases
+ CMAKE_OPTIONS="${CMAKE_OPTIONS} -DWITH_TESTS=OFF"
if [ "$COMPILER" == "g++" ]; then
export CC=gcc
@@ -913,14 +918,14 @@ build() {
elif [ "$(uname -o)" == "Msys" ]; then
# Building on Windows with Msys2
logInfo "Configuring build..."
- cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=Off -G"MSYS Makefiles" \
+ cmake -DCMAKE_BUILD_TYPE=Release -G"MSYS Makefiles" \
-DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" ${CMAKE_OPTIONS} "$SRC_DIR"
logInfo "Compiling and packaging sources..."
mingw32-make ${MAKE_OPTIONS} preinstall
# Appsign the executables if desired
- if ${build_appsign} && [ -f ${build_key} ]; then
+ if ${build_appsign} && [ -f "${build_key}" ]; then
logInfo "Signing executable files"
appsign "-f" $(find src | grep -P '\.exe$|\.dll$') "-k" "${build_key}"
fi
@@ -945,7 +950,7 @@ build() {
# Building on Linux without Docker container
logInfo "Configuring build..."
- cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=Off ${CMAKE_OPTIONS} \
+ cmake -DCMAKE_BUILD_TYPE=Release ${CMAKE_OPTIONS} \
-DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}" "$SRC_DIR"
logInfo "Compiling sources..."
@@ -977,7 +982,7 @@ build() {
-v "$(realpath "$OUTPUT_DIR"):/keepassxc/out:rw" \
"$DOCKER_IMAGE" \
bash -c "cd /keepassxc/out/build-release && \
- cmake -DCMAKE_BUILD_TYPE=Release -DWITH_TESTS=Off ${CMAKE_OPTIONS} \
+ cmake -DCMAKE_BUILD_TYPE=Release ${CMAKE_OPTIONS} \
-DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} /keepassxc/src && \
make ${MAKE_OPTIONS} && make DESTDIR=/keepassxc/out/KeePassXC.AppDir install/strip"
fi
@@ -1139,7 +1144,7 @@ appsign() {
fi
logInfo "Signing app using codesign..."
- codesign --sign "${key}" --verbose --deep --entitlements ${orig_dir}/share/macosx/keepassxc.entitlements ./app/KeePassXC.app
+ codesign --sign "${key}" --verbose --deep --entitlements "${SRC_DIR}/share/macosx/keepassxc.entitlements" ./app/KeePassXC.app
if [ 0 -ne $? ]; then
cd "${orig_dir}"
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index b2cd27232..d8eb681e3 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -195,6 +195,7 @@ add_feature_info(SSHAgent WITH_XC_SSHAGENT "SSH agent integration compatible wit
add_feature_info(KeeShare WITH_XC_KEESHARE "Sharing integration with KeeShare")
add_feature_info(KeeShare-Secure WITH_XC_KEESHARE_SECURE "Sharing integration with KeeShare with secure sources")
add_feature_info(YubiKey WITH_XC_YUBIKEY "YubiKey HMAC-SHA1 challenge-response")
+add_feature_info(UpdateCheck WITH_XC_UPDATECHECK "Automatic update checking")
if(APPLE)
add_feature_info(TouchID WITH_XC_TOUCHID "TouchID integration")
endif()
diff --git a/src/autotype/AutoType.cpp b/src/autotype/AutoType.cpp
index 012dee62c..0f772d8d3 100644
--- a/src/autotype/AutoType.cpp
+++ b/src/autotype/AutoType.cpp
@@ -214,7 +214,7 @@ void AutoType::executeAutoTypeActions(const Entry* entry, QWidget* hideWindow, c
if (hideWindow) {
#if defined(Q_OS_MACOS)
- m_plugin->raiseLastActiveWindow();
+ m_plugin->hideOwnWindow();
#else
hideWindow->showMinimized();
#endif
diff --git a/src/autotype/AutoTypePlatformPlugin.h b/src/autotype/AutoTypePlatformPlugin.h
index 68cf99be2..059e7e134 100644
--- a/src/autotype/AutoTypePlatformPlugin.h
+++ b/src/autotype/AutoTypePlatformPlugin.h
@@ -43,7 +43,7 @@ public:
virtual AutoTypeExecutor* createExecutor() = 0;
#if defined(Q_OS_MACOS)
- virtual bool raiseLastActiveWindow() = 0;
+ virtual bool hideOwnWindow() = 0;
virtual bool raiseOwnWindow() = 0;
#endif
diff --git a/src/autotype/mac/AutoTypeMac.cpp b/src/autotype/mac/AutoTypeMac.cpp
index 60cec1144..e73e53777 100644
--- a/src/autotype/mac/AutoTypeMac.cpp
+++ b/src/autotype/mac/AutoTypeMac.cpp
@@ -165,9 +165,9 @@ bool AutoTypePlatformMac::raiseWindow(WId pid)
//
// Activate last active window
//
-bool AutoTypePlatformMac::raiseLastActiveWindow()
+bool AutoTypePlatformMac::hideOwnWindow()
{
- return macUtils()->raiseLastActiveWindow();
+ return macUtils()->hideOwnWindow();
}
//
diff --git a/src/autotype/mac/AutoTypeMac.h b/src/autotype/mac/AutoTypeMac.h
index 875c21764..55963da51 100644
--- a/src/autotype/mac/AutoTypeMac.h
+++ b/src/autotype/mac/AutoTypeMac.h
@@ -44,7 +44,7 @@ public:
bool raiseWindow(WId pid) override;
AutoTypeExecutor* createExecutor() override;
- bool raiseLastActiveWindow() override;
+ bool hideOwnWindow() override;
bool raiseOwnWindow() override;
void sendChar(const QChar& ch, bool isKeyDown);
diff --git a/src/autotype/test/AutoTypeTest.cpp b/src/autotype/test/AutoTypeTest.cpp
index f8754ef3b..9a1b65013 100644
--- a/src/autotype/test/AutoTypeTest.cpp
+++ b/src/autotype/test/AutoTypeTest.cpp
@@ -111,7 +111,7 @@ bool AutoTypePlatformTest::raiseWindow(WId window)
}
#if defined(Q_OS_MACOS)
-bool AutoTypePlatformTest::raiseLastActiveWindow()
+bool AutoTypePlatformTest::hideOwnWindow()
{
return false;
}
diff --git a/src/autotype/test/AutoTypeTest.h b/src/autotype/test/AutoTypeTest.h
index a17028b51..87d19491a 100644
--- a/src/autotype/test/AutoTypeTest.h
+++ b/src/autotype/test/AutoTypeTest.h
@@ -44,7 +44,7 @@ public:
AutoTypeExecutor* createExecutor() override;
#if defined(Q_OS_MACOS)
- bool raiseLastActiveWindow() override;
+ bool hideOwnWindow() override;
bool raiseOwnWindow() override;
#endif
diff --git a/src/browser/BrowserOptionDialog.cpp b/src/browser/BrowserOptionDialog.cpp
index dd91f1594..9eecc63f9 100644
--- a/src/browser/BrowserOptionDialog.cpp
+++ b/src/browser/BrowserOptionDialog.cpp
@@ -47,7 +47,7 @@ BrowserOptionDialog::BrowserOptionDialog(QWidget* parent)
tr("KeePassXC-Browser is needed for the browser integration to work.
Download it for %1 and %2. %3")
.arg("Firefox",
""
- "Google Chrome / Chromium / Vivaldi",
+ "Google Chrome / Chromium / Vivaldi / Brave",
snapInstructions));
// clang-format on
@@ -75,9 +75,11 @@ BrowserOptionDialog::BrowserOptionDialog(QWidget* parent)
connect(m_ui->customProxyLocationBrowseButton, SIGNAL(clicked()), this, SLOT(showProxyLocationFileDialog()));
#ifdef Q_OS_WIN
+ // Brave uses Chrome's registry settings
+ m_ui->braveSupport->setHidden(true);
// Vivaldi uses Chrome's registry settings
m_ui->vivaldiSupport->setHidden(true);
- m_ui->chromeSupport->setText("Chrome and Vivaldi");
+ m_ui->chromeSupport->setText("Chrome, Vivaldi, and Brave");
// Tor Browser uses Firefox's registry settings
m_ui->torBrowserSupport->setHidden(true);
m_ui->firefoxSupport->setText("Firefox and Tor Browser");
@@ -122,6 +124,7 @@ void BrowserOptionDialog::loadSettings()
m_ui->chromiumSupport->setChecked(settings->chromiumSupport());
m_ui->firefoxSupport->setChecked(settings->firefoxSupport());
#ifndef Q_OS_WIN
+ m_ui->braveSupport->setChecked(settings->braveSupport());
m_ui->vivaldiSupport->setChecked(settings->vivaldiSupport());
m_ui->torBrowserSupport->setChecked(settings->torBrowserSupport());
#endif
@@ -183,6 +186,7 @@ void BrowserOptionDialog::saveSettings()
settings->setChromiumSupport(m_ui->chromiumSupport->isChecked());
settings->setFirefoxSupport(m_ui->firefoxSupport->isChecked());
#ifndef Q_OS_WIN
+ settings->setBraveSupport(m_ui->braveSupport->isChecked());
settings->setVivaldiSupport(m_ui->vivaldiSupport->isChecked());
settings->setTorBrowserSupport(m_ui->torBrowserSupport->isChecked());
#endif
diff --git a/src/browser/BrowserOptionDialog.ui b/src/browser/BrowserOptionDialog.ui
index 2b32bb9e8..50fd9d205 100755
--- a/src/browser/BrowserOptionDialog.ui
+++ b/src/browser/BrowserOptionDialog.ui
@@ -150,6 +150,16 @@
+ -
+
+
+ &Brave
+
+
+ false
+
+
+
diff --git a/src/browser/BrowserSettings.cpp b/src/browser/BrowserSettings.cpp
index 9aab68f7e..dd74dc1cb 100644
--- a/src/browser/BrowserSettings.cpp
+++ b/src/browser/BrowserSettings.cpp
@@ -238,6 +238,17 @@ void BrowserSettings::setVivaldiSupport(bool enabled)
HostInstaller::SupportedBrowsers::VIVALDI, enabled, supportBrowserProxy(), customProxyLocation());
}
+bool BrowserSettings::braveSupport()
+{
+ return m_hostInstaller.checkIfInstalled(HostInstaller::SupportedBrowsers::BRAVE);
+}
+
+void BrowserSettings::setBraveSupport(bool enabled)
+{
+ m_hostInstaller.installBrowser(
+ HostInstaller::SupportedBrowsers::BRAVE, enabled, supportBrowserProxy(), customProxyLocation());
+}
+
bool BrowserSettings::torBrowserSupport()
{
return m_hostInstaller.checkIfInstalled(HostInstaller::SupportedBrowsers::TOR_BROWSER);
diff --git a/src/browser/BrowserSettings.h b/src/browser/BrowserSettings.h
index b00c75b71..ba74ff53e 100644
--- a/src/browser/BrowserSettings.h
+++ b/src/browser/BrowserSettings.h
@@ -72,6 +72,8 @@ public:
void setFirefoxSupport(bool enabled);
bool vivaldiSupport();
void setVivaldiSupport(bool enabled);
+ bool braveSupport();
+ void setBraveSupport(bool enabled);
bool torBrowserSupport();
void setTorBrowserSupport(bool enabled);
diff --git a/src/browser/HostInstaller.cpp b/src/browser/HostInstaller.cpp
index 08782fa16..20c554566 100644
--- a/src/browser/HostInstaller.cpp
+++ b/src/browser/HostInstaller.cpp
@@ -39,12 +39,14 @@ HostInstaller::HostInstaller()
, TARGET_DIR_FIREFOX("/Library/Application Support/Mozilla/NativeMessagingHosts")
, TARGET_DIR_VIVALDI("/Library/Application Support/Vivaldi/NativeMessagingHosts")
, TARGET_DIR_TOR_BROWSER("/Library/Application Support/TorBrowser-Data/Browser/Mozilla/NativeMessagingHosts")
+ , TARGET_DIR_BRAVE("/Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts")
#elif defined(Q_OS_LINUX)
, TARGET_DIR_CHROME("/.config/google-chrome/NativeMessagingHosts")
, TARGET_DIR_CHROMIUM("/.config/chromium/NativeMessagingHosts")
, TARGET_DIR_FIREFOX("/.mozilla/native-messaging-hosts")
, TARGET_DIR_VIVALDI("/.config/vivaldi/NativeMessagingHosts")
, TARGET_DIR_TOR_BROWSER("/.tor-browser/app/Browser/TorBrowser/Data/Browser/.mozilla/native-messaging-hosts")
+ , TARGET_DIR_BRAVE("/.config/BraveSoftware/Brave-Browser/NativeMessagingHosts")
#elif defined(Q_OS_WIN)
// clang-format off
, TARGET_DIR_CHROME("HKEY_CURRENT_USER\\Software\\Google\\Chrome\\NativeMessagingHosts\\org.keepassxc.keepassxc_browser")
@@ -53,6 +55,7 @@ HostInstaller::HostInstaller()
, TARGET_DIR_FIREFOX("HKEY_CURRENT_USER\\Software\\Mozilla\\NativeMessagingHosts\\org.keepassxc.keepassxc_browser")
, TARGET_DIR_VIVALDI(TARGET_DIR_CHROME)
, TARGET_DIR_TOR_BROWSER(TARGET_DIR_FIREFOX)
+ , TARGET_DIR_BRAVE(TARGET_DIR_CHROME)
#endif
{
}
@@ -140,7 +143,8 @@ void HostInstaller::installBrowser(SupportedBrowsers browser,
*/
void HostInstaller::updateBinaryPaths(const bool& proxy, const QString& location)
{
- for (int i = 0; i < 4; ++i) {
+ // Where 6 is the number of entries in the SupportedBrowsers enum declared in HostInstaller.h
+ for (int i = 0; i < 6; ++i) {
if (checkIfInstalled(static_cast(i))) {
installBrowser(static_cast(i), true, proxy, location);
}
@@ -166,6 +170,8 @@ QString HostInstaller::getTargetPath(SupportedBrowsers browser) const
return TARGET_DIR_VIVALDI;
case SupportedBrowsers::TOR_BROWSER:
return TARGET_DIR_TOR_BROWSER;
+ case SupportedBrowsers::BRAVE:
+ return TARGET_DIR_BRAVE;
default:
return QString();
}
@@ -188,9 +194,11 @@ QString HostInstaller::getBrowserName(SupportedBrowsers browser) const
case SupportedBrowsers::FIREFOX:
return "firefox";
case SupportedBrowsers::VIVALDI:
- return "vivaldi";
+ return "vivaldi";
case SupportedBrowsers::TOR_BROWSER:
return "tor-browser";
+ case SupportedBrowsers::BRAVE:
+ return "brave";
default:
return QString();
}
diff --git a/src/browser/HostInstaller.h b/src/browser/HostInstaller.h
index ea0c4bd2f..154fe21a9 100644
--- a/src/browser/HostInstaller.h
+++ b/src/browser/HostInstaller.h
@@ -34,7 +34,8 @@ public:
CHROMIUM = 1,
FIREFOX = 2,
VIVALDI = 3,
- TOR_BROWSER = 4
+ TOR_BROWSER = 4,
+ BRAVE = 5
};
public:
@@ -66,6 +67,7 @@ private:
const QString TARGET_DIR_FIREFOX;
const QString TARGET_DIR_VIVALDI;
const QString TARGET_DIR_TOR_BROWSER;
+ const QString TARGET_DIR_BRAVE;
};
#endif // HOSTINSTALLER_H
diff --git a/src/config-keepassx.h.cmake b/src/config-keepassx.h.cmake
index 7d7018861..2acff4466 100644
--- a/src/config-keepassx.h.cmake
+++ b/src/config-keepassx.h.cmake
@@ -20,6 +20,7 @@
#cmakedefine WITH_XC_KEESHARE
#cmakedefine WITH_XC_KEESHARE_INSECURE
#cmakedefine WITH_XC_KEESHARE_SECURE
+#cmakedefine WITH_XC_UPDATECHECK
#cmakedefine WITH_XC_TOUCHID
#cmakedefine KEEPASSXC_BUILD_TYPE "@KEEPASSXC_BUILD_TYPE@"
diff --git a/src/core/Clock.cpp b/src/core/Clock.cpp
index 88ac4fb77..be9e91dcf 100644
--- a/src/core/Clock.cpp
+++ b/src/core/Clock.cpp
@@ -30,6 +30,7 @@ QDateTime Clock::currentDateTime()
uint Clock::currentSecondsSinceEpoch()
{
+ // TODO: change to toSecsSinceEpoch() when min Qt >= 5.8
return instance().currentDateTimeImpl().toTime_t();
}
diff --git a/src/core/Database.cpp b/src/core/Database.cpp
index 6f035b1d1..571406059 100644
--- a/src/core/Database.cpp
+++ b/src/core/Database.cpp
@@ -222,6 +222,7 @@ bool Database::save(const QString& filePath, QString* error, bool atomic, bool b
return true;
}
}
+
if (error) {
*error = saveFile.errorString();
}
@@ -241,27 +242,34 @@ bool Database::save(const QString& filePath, QString* error, bool atomic, bool b
// Delete the original db and move the temp file in place
QFile::remove(filePath);
-#ifdef Q_OS_LINUX
- // workaround to make this workaround work, see: https://bugreports.qt.io/browse/QTBUG-64008
- if (tempFile.copy(filePath)) {
- // successfully saved database file
- return true;
- }
-#else
- if (tempFile.rename(filePath)) {
- // successfully saved database file
+
+ // Note: call into the QFile rename instead of QTemporaryFile
+ // due to an undocumented difference in how the function handles
+ // errors. This prevents errors when saving across file systems.
+ if (tempFile.QFile::rename(filePath)) {
+ // successfully saved the database
tempFile.setAutoRemove(false);
setFilePath(filePath);
return true;
+ } else if (!backup || !restoreDatabase(filePath)) {
+ // Failed to copy new database in place, and
+ // failed to restore from backup or backups disabled
+ tempFile.setAutoRemove(false);
+ if (error) {
+ *error = tr("%1\nBackup database located at %2").arg(tempFile.errorString(), tempFile.fileName());
+ }
+ markAsModified();
+ return false;
}
-#endif
}
+
if (error) {
*error = tempFile.errorString();
}
}
// Saving failed
+ markAsModified();
return false;
}
@@ -333,6 +341,28 @@ bool Database::backupDatabase(const QString& filePath)
return QFile::copy(filePath, backupFilePath);
}
+/**
+ * Restores the database file from the backup file with
+ * name .old. to filePath. This will
+ * overwrite the existing file!
+ *
+ * @param filePath Path to the file to restore
+ * @return true on success
+ */
+bool Database::restoreDatabase(const QString& filePath)
+{
+ static auto re = QRegularExpression("^(.*?)(\\.[^.]+)?$");
+
+ auto match = re.match(filePath);
+ auto backupFilePath = match.captured(1) + ".old" + match.captured(2);
+ // Only try to restore if the backup file actually exists
+ if (QFile::exists(backupFilePath)) {
+ QFile::remove(filePath);
+ return QFile::copy(backupFilePath, filePath);
+ }
+ return false;
+}
+
bool Database::isReadOnly() const
{
return m_data.isReadOnly;
@@ -401,11 +431,31 @@ const Metadata* Database::metadata() const
return m_metadata;
}
+/**
+ * Returns the original file path that was provided for
+ * this database. This path may not exist, may contain
+ * unresolved symlinks, or have malformed slashes.
+ *
+ * @return original file path
+ */
QString Database::filePath() const
{
return m_data.filePath;
}
+/**
+ * Returns the canonical file path of this databases'
+ * set file path. This returns an empty string if the
+ * file does not exist or cannot be resolved.
+ *
+ * @return canonical file path
+ */
+QString Database::canonicalFilePath() const
+{
+ QFileInfo fileInfo(m_data.filePath);
+ return fileInfo.canonicalFilePath();
+}
+
void Database::setFilePath(const QString& filePath)
{
if (filePath == m_data.filePath) {
diff --git a/src/core/Database.h b/src/core/Database.h
index 104d522a3..09602f764 100644
--- a/src/core/Database.h
+++ b/src/core/Database.h
@@ -83,6 +83,7 @@ public:
QUuid uuid() const;
QString filePath() const;
+ QString canonicalFilePath() const;
void setFilePath(const QString& filePath);
Metadata* metadata();
@@ -172,6 +173,7 @@ private:
bool writeDatabase(QIODevice* device, QString* error = nullptr);
bool backupDatabase(const QString& filePath);
+ bool restoreDatabase(const QString& filePath);
Metadata* const m_metadata;
DatabaseData m_data;
diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp
index 09938b82a..46cde95bc 100644
--- a/src/core/Tools.cpp
+++ b/src/core/Tools.cpp
@@ -205,7 +205,7 @@ namespace Tools
bool isBase64(const QByteArray& ba)
{
- constexpr auto pattern = R"(^(?:[a-z0-9+]{4})*(?:[a-z0-9+]{3}=|[a-z0-9+]{2}==)?$)";
+ constexpr auto pattern = R"(^(?:[a-z0-9+/]{4})*(?:[a-z0-9+/]{3}=|[a-z0-9+/]{2}==)?$)";
QRegExp regexp(pattern, Qt::CaseInsensitive, QRegExp::RegExp2);
QString base64 = QString::fromLatin1(ba.constData(), ba.size());
diff --git a/src/format/Kdbx3Reader.cpp b/src/format/Kdbx3Reader.cpp
index 4fec74718..9196bc616 100644
--- a/src/format/Kdbx3Reader.cpp
+++ b/src/format/Kdbx3Reader.cpp
@@ -78,7 +78,8 @@ bool Kdbx3Reader::readDatabaseImpl(QIODevice* device,
QByteArray realStart = cipherStream.read(32);
if (realStart != m_streamStartBytes) {
- raiseError(tr("Wrong key or database file is corrupt."));
+ raiseError(tr("Invalid credentials were provided, please try again.\n"
+ "If this reoccurs, then your database file may be corrupt."));
return false;
}
diff --git a/src/format/Kdbx4Reader.cpp b/src/format/Kdbx4Reader.cpp
index fbdf865bc..4bb0202b1 100644
--- a/src/format/Kdbx4Reader.cpp
+++ b/src/format/Kdbx4Reader.cpp
@@ -71,7 +71,8 @@ bool Kdbx4Reader::readDatabaseImpl(QIODevice* device,
// clang-format off
QByteArray hmacKey = KeePass2::hmacKey(m_masterSeed, db->transformedMasterKey());
if (headerHmac != CryptoHash::hmac(headerData, HmacBlockStream::getHmacKey(UINT64_MAX, hmacKey), CryptoHash::Sha256)) {
- raiseError(tr("Wrong key or database file is corrupt. (HMAC mismatch)"));
+ raiseError(tr("Invalid credentials were provided, please try again.\n"
+ "If this reoccurs, then your database file may be corrupt.") + " " + tr("(HMAC mismatch)"));
return false;
}
HmacBlockStream hmacStream(device, hmacKey);
diff --git a/src/format/KdbxXmlReader.cpp b/src/format/KdbxXmlReader.cpp
index 84d597bdb..ab2b9aeb7 100644
--- a/src/format/KdbxXmlReader.cpp
+++ b/src/format/KdbxXmlReader.cpp
@@ -1028,10 +1028,8 @@ bool KdbxXmlReader::readBool()
QDateTime KdbxXmlReader::readDateTime()
{
- static QRegularExpression b64regex("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$");
QString str = readString();
-
- if (b64regex.match(str).hasMatch()) {
+ if (Tools::isBase64(str.toLatin1())) {
QByteArray secsBytes = QByteArray::fromBase64(str.toUtf8()).leftJustified(8, '\0', true).left(8);
qint64 secs = Endian::bytesToSizedInt(secsBytes, KeePass2::BYTEORDER);
return QDateTime(QDate(1, 1, 1), QTime(0, 0, 0, 0), Qt::UTC).addSecs(secs);
diff --git a/src/format/KeePass1Reader.cpp b/src/format/KeePass1Reader.cpp
index e42449358..0319b1b2d 100644
--- a/src/format/KeePass1Reader.cpp
+++ b/src/format/KeePass1Reader.cpp
@@ -372,7 +372,8 @@ KeePass1Reader::testKeys(const QString& password, const QByteArray& keyfileData,
}
if (!cipherStream) {
- raiseError(tr("Wrong key or database file is corrupt."));
+ raiseError(tr("Invalid credentials were provided, please try again.\n"
+ "If this reoccurs, then your database file may be corrupt."));
}
return cipherStream.take();
diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp
index 849df03ae..22a49dece 100644
--- a/src/gui/ApplicationSettingsWidget.cpp
+++ b/src/gui/ApplicationSettingsWidget.cpp
@@ -92,8 +92,15 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent)
m_secUi->touchIDResetSpinBox, SLOT(setEnabled(bool)));
// clang-format on
-#ifndef WITH_XC_NETWORKING
+#ifdef WITH_XC_UPDATECHECK
+ connect(m_generalUi->checkForUpdatesOnStartupCheckBox, SIGNAL(toggled(bool)), SLOT(checkUpdatesToggled(bool)));
+#else
m_generalUi->checkForUpdatesOnStartupCheckBox->setVisible(false);
+ m_generalUi->checkForUpdatesIncludeBetasCheckBox->setVisible(false);
+ m_generalUi->checkUpdatesSpacer->changeSize(0,0, QSizePolicy::Fixed, QSizePolicy::Fixed);
+#endif
+
+#ifndef WITH_XC_NETWORKING
m_secUi->privacy->setVisible(false);
#endif
@@ -350,3 +357,8 @@ void ApplicationSettingsWidget::rememberDatabasesToggled(bool checked)
m_generalUi->rememberLastKeyFilesCheckBox->setEnabled(checked);
m_generalUi->openPreviousDatabasesOnStartupCheckBox->setEnabled(checked);
}
+
+void ApplicationSettingsWidget::checkUpdatesToggled(bool checked)
+{
+ m_generalUi->checkForUpdatesIncludeBetasCheckBox->setEnabled(checked);
+}
diff --git a/src/gui/ApplicationSettingsWidget.h b/src/gui/ApplicationSettingsWidget.h
index 85b3b4700..dfffbddbd 100644
--- a/src/gui/ApplicationSettingsWidget.h
+++ b/src/gui/ApplicationSettingsWidget.h
@@ -57,6 +57,7 @@ private slots:
void systrayToggled(bool checked);
void toolbarSettingsToggled(bool checked);
void rememberDatabasesToggled(bool checked);
+ void checkUpdatesToggled(bool checked);
private:
QWidget* const m_secWidget;
diff --git a/src/gui/ApplicationSettingsWidgetGeneral.ui b/src/gui/ApplicationSettingsWidgetGeneral.ui
index 8885ef7cb..9f03bbb50 100644
--- a/src/gui/ApplicationSettingsWidgetGeneral.ui
+++ b/src/gui/ApplicationSettingsWidgetGeneral.ui
@@ -141,10 +141,40 @@
-
- Check for updates at application startup
+ Check for updates at application startup once per week
+ -
+
+
+ 0
+
+
-
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 20
+ 20
+
+
+
+
+ -
+
+
+ Include beta releases when checking for updates
+
+
+
+
+
@@ -241,13 +271,6 @@
General
- -
-
-
- Include pre-releases when checking for updates
-
-
-
-
@@ -470,7 +493,7 @@
-
-
+
0
0
@@ -490,6 +513,13 @@
+ -
+
+
+ (restart program to activate)
+
+
+
diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp
index 155846640..ced72485e 100644
--- a/src/gui/DatabaseOpenWidget.cpp
+++ b/src/gui/DatabaseOpenWidget.cpp
@@ -45,7 +45,6 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
m_ui->setupUi(this);
m_ui->messageWidget->setHidden(true);
- m_ui->checkPassword->setChecked(true);
QFont font = m_ui->labelHeadline->font();
font.setBold(true);
@@ -159,7 +158,7 @@ void DatabaseOpenWidget::clearForms()
m_ui->editPassword->setText("");
m_ui->comboKeyFile->clear();
m_ui->comboKeyFile->setEditText("");
- m_ui->checkPassword->setChecked(true);
+ m_ui->checkPassword->setChecked(false);
m_ui->checkKeyFile->setChecked(false);
m_ui->checkChallengeResponse->setChecked(false);
m_ui->checkTouchID->setChecked(false);
@@ -174,13 +173,8 @@ QSharedPointer DatabaseOpenWidget::database()
void DatabaseOpenWidget::enterKey(const QString& pw, const QString& keyFile)
{
- if (!pw.isEmpty()) {
- m_ui->editPassword->setText(pw);
- }
- if (!keyFile.isEmpty()) {
- m_ui->comboKeyFile->setEditText(keyFile);
- }
-
+ m_ui->editPassword->setText(pw);
+ m_ui->comboKeyFile->setEditText(keyFile);
openDatabase();
}
@@ -191,9 +185,7 @@ void DatabaseOpenWidget::openDatabase()
return;
}
- if (!m_ui->editPassword->isPasswordVisible()) {
- m_ui->editPassword->setShowPassword(false);
- }
+ m_ui->editPassword->setShowPassword(false);
QCoreApplication::processEvents();
m_db.reset(new Database());
@@ -202,8 +194,7 @@ void DatabaseOpenWidget::openDatabase()
bool ok = m_db->open(m_filename, masterKey, &error, false);
QApplication::restoreOverrideCursor();
if (!ok) {
- m_ui->messageWidget->showMessage(tr("Unable to open the database:\n%1").arg(error),
- MessageWidget::MessageType::Error);
+ m_ui->messageWidget->showMessage(error, MessageWidget::MessageType::Error);
return;
}
@@ -231,7 +222,7 @@ void DatabaseOpenWidget::openDatabase()
}
emit dialogFinished(true);
} else {
- m_ui->messageWidget->showMessage(tr("Unable to open the database:\n%1").arg(error), MessageWidget::Error);
+ m_ui->messageWidget->showMessage(error, MessageWidget::Error);
m_ui->editPassword->setText("");
#ifdef WITH_XC_TOUCHID
@@ -276,7 +267,7 @@ QSharedPointer DatabaseOpenWidget::databaseKey()
QString keyFilename = m_ui->comboKeyFile->currentText();
QString errorMsg;
if (!key->load(keyFilename, &errorMsg)) {
- m_ui->messageWidget->showMessage(tr("Can't open key file:\n%1").arg(errorMsg), MessageWidget::Error);
+ m_ui->messageWidget->showMessage(tr("Failed to open key file: %1").arg(errorMsg), MessageWidget::Error);
return {};
}
if (key->type() != FileKey::Hashed && !config()->get("Messages/NoLegacyKeyFileWarning").toBool()) {
@@ -339,17 +330,20 @@ void DatabaseOpenWidget::reject()
void DatabaseOpenWidget::activatePassword()
{
- m_ui->checkPassword->setChecked(true);
+ bool hasPassword = !m_ui->editPassword->text().isEmpty();
+ m_ui->checkPassword->setChecked(hasPassword);
}
void DatabaseOpenWidget::activateKeyFile()
{
- m_ui->checkKeyFile->setChecked(true);
+ bool hasKeyFile = !m_ui->comboKeyFile->lineEdit()->text().isEmpty();
+ m_ui->checkKeyFile->setChecked(hasKeyFile);
}
void DatabaseOpenWidget::activateChallengeResponse()
{
- m_ui->checkChallengeResponse->setChecked(true);
+ bool hasCR = m_ui->comboChallengeResponse->currentData().toInt() != -1;
+ m_ui->checkChallengeResponse->setChecked(hasCR);
}
void DatabaseOpenWidget::browseKeyFile()
@@ -372,6 +366,7 @@ void DatabaseOpenWidget::pollYubikey()
m_ui->checkChallengeResponse->setChecked(false);
m_ui->comboChallengeResponse->setEnabled(false);
m_ui->comboChallengeResponse->clear();
+ m_ui->comboChallengeResponse->addItem(tr("Select slot..."), -1);
m_ui->yubikeyProgress->setVisible(true);
// YubiKey init is slow, detect asynchronously to not block the UI
@@ -388,6 +383,7 @@ void DatabaseOpenWidget::yubikeyDetected(int slot, bool blocking)
QHash lastChallengeResponse = config()->get("LastChallengeResponse").toHash();
if (lastChallengeResponse.contains(m_filename)) {
m_ui->checkChallengeResponse->setChecked(true);
+ m_ui->comboChallengeResponse->setCurrentIndex(1);
}
}
}
diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp
index 7693c9016..313bfabb1 100644
--- a/src/gui/DatabaseTabWidget.cpp
+++ b/src/gui/DatabaseTabWidget.cpp
@@ -150,17 +150,16 @@ void DatabaseTabWidget::addDatabaseTab(const QString& filePath,
QFileInfo fileInfo(filePath);
QString canonicalFilePath = fileInfo.canonicalFilePath();
if (canonicalFilePath.isEmpty()) {
- emit messageGlobal(tr("The database file does not exist or is not accessible."), MessageWidget::Error);
+ emit messageGlobal(tr("Failed to open %1. It either does not exist or is not accessible.").arg(filePath),
+ MessageWidget::Error);
return;
}
for (int i = 0, c = count(); i < c; ++i) {
auto* dbWidget = databaseWidgetFromIndex(i);
Q_ASSERT(dbWidget);
- if (dbWidget && dbWidget->database()->filePath() == canonicalFilePath) {
- if (!password.isEmpty()) {
- dbWidget->performUnlockDatabase(password, keyfile);
- }
+ if (dbWidget && dbWidget->database()->canonicalFilePath() == canonicalFilePath) {
+ dbWidget->performUnlockDatabase(password, keyfile);
if (!inBackground) {
// switch to existing tab if file is already open
setCurrentIndex(indexOf(dbWidget));
@@ -171,9 +170,7 @@ void DatabaseTabWidget::addDatabaseTab(const QString& filePath,
auto* dbWidget = new DatabaseWidget(QSharedPointer::create(filePath), this);
addDatabaseTab(dbWidget, inBackground);
- if (!password.isEmpty()) {
- dbWidget->performUnlockDatabase(password, keyfile);
- }
+ dbWidget->performUnlockDatabase(password, keyfile);
updateLastDatabases(filePath);
}
diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp
index e4196734b..97fdeb0f9 100644
--- a/src/gui/DatabaseWidget.cpp
+++ b/src/gui/DatabaseWidget.cpp
@@ -89,6 +89,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent)
, m_databaseOpenWidget(new DatabaseOpenWidget(this))
, m_keepass1OpenWidget(new KeePass1OpenWidget(this))
, m_groupView(new GroupView(m_db.data(), m_mainSplitter))
+ , m_saveAttempts(0)
, m_fileWatcher(new DelayingFileWatcher(this))
{
m_messageWidget->setHidden(true);
@@ -260,12 +261,11 @@ bool DatabaseWidget::isSearchActive() const
bool DatabaseWidget::isEditWidgetModified() const
{
if (currentWidget() == m_editEntryWidget) {
- return m_editEntryWidget->hasBeenModified();
- } else {
- // other edit widget don't have a hasBeenModified() method yet
- // assume that they already have been modified
- return true;
+ return m_editEntryWidget->isModified();
+ } else if (currentWidget() == m_editGroupWidget) {
+ return m_editGroupWidget->isModified();
}
+ return false;
}
QList DatabaseWidget::mainSplitterSizes() const
@@ -859,6 +859,7 @@ void DatabaseWidget::loadDatabase(bool accepted)
replaceDatabase(openWidget->database());
switchToMainView();
m_fileWatcher->restart();
+ m_saveAttempts = 0;
emit databaseUnlocked();
} else {
m_fileWatcher->stop();
@@ -1247,7 +1248,7 @@ bool DatabaseWidget::lock()
clipboard()->clearCopiedText();
- if (currentMode() == DatabaseWidget::Mode::EditMode) {
+ if (isEditWidgetModified()) {
auto result = MessageBox::question(this,
tr("Lock Database?"),
tr("You are editing an entry. Discard changes and lock anyway?"),
@@ -1512,7 +1513,7 @@ EntryView* DatabaseWidget::entryView()
* @param attempt current save attempt or -1 to disable attempts
* @return true on success
*/
-bool DatabaseWidget::save(int attempt)
+bool DatabaseWidget::save()
{
// Never allow saving a locked database; it causes corruption
Q_ASSERT(!isLocked());
@@ -1527,6 +1528,8 @@ bool DatabaseWidget::save(int attempt)
}
blockAutoReload(true);
+ ++m_saveAttempts;
+
// TODO: Make this async, but lock out the database widget to prevent re-entrance
bool useAtomicSaves = config()->get("UseAtomicSaves", true).toBool();
QString errorMessage;
@@ -1534,14 +1537,11 @@ bool DatabaseWidget::save(int attempt)
blockAutoReload(false);
if (ok) {
+ m_saveAttempts = 0;
return true;
}
- if (attempt >= 0 && attempt <= 2) {
- return save(attempt + 1);
- }
-
- if (attempt > 2 && useAtomicSaves) {
+ if (m_saveAttempts > 2 && useAtomicSaves) {
// Saving failed 3 times, issue a warning and attempt to resolve
auto result = MessageBox::question(this,
tr("Disable safe saves?"),
@@ -1552,11 +1552,15 @@ bool DatabaseWidget::save(int attempt)
MessageBox::Disable);
if (result == MessageBox::Disable) {
config()->set("UseAtomicSaves", false);
- return save(attempt + 1);
+ return save();
}
}
- showMessage(tr("Writing the database failed.\n%1").arg(errorMessage), MessageWidget::Error);
+ showMessage(tr("Writing the database failed: %1").arg(errorMessage),
+ MessageWidget::Error,
+ true,
+ MessageWidget::LongAutoHideTimeout);
+
return false;
}
@@ -1585,8 +1589,9 @@ bool DatabaseWidget::saveAs()
// Ensure we don't recurse back into this function
m_db->setReadOnly(false);
m_db->setFilePath(newFilePath);
+ m_saveAttempts = 0;
- if (!save(-1)) {
+ if (!save()) {
// Failed to save, try again
continue;
}
diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h
index 9c2788995..11b2f710c 100644
--- a/src/gui/DatabaseWidget.h
+++ b/src/gui/DatabaseWidget.h
@@ -144,7 +144,7 @@ signals:
public slots:
bool lock();
- bool save(int attempt = 0);
+ bool save();
bool saveAs();
void replaceDatabase(QSharedPointer db);
@@ -255,6 +255,8 @@ private:
QUuid m_groupBeforeLock;
QUuid m_entryBeforeLock;
+ int m_saveAttempts;
+
// Search state
EntrySearcher* m_EntrySearcher;
QString m_lastSearchText;
diff --git a/src/gui/EditWidget.cpp b/src/gui/EditWidget.cpp
index be7ea01df..f7030c9d7 100644
--- a/src/gui/EditWidget.cpp
+++ b/src/gui/EditWidget.cpp
@@ -30,6 +30,7 @@ EditWidget::EditWidget(QWidget* parent)
{
m_ui->setupUi(this);
setReadOnly(false);
+ setModified(false);
m_ui->messageWidget->setHidden(true);
@@ -43,6 +44,7 @@ EditWidget::EditWidget(QWidget* parent)
connect(m_ui->buttonBox, SIGNAL(accepted()), SIGNAL(accepted()));
connect(m_ui->buttonBox, SIGNAL(rejected()), SIGNAL(rejected()));
+ connect(m_ui->buttonBox, SIGNAL(clicked(QAbstractButton*)), SLOT(buttonClicked(QAbstractButton*)));
}
EditWidget::~EditWidget()
@@ -106,9 +108,6 @@ void EditWidget::setReadOnly(bool readOnly)
m_ui->buttonBox->setStandardButtons(QDialogButtonBox::Close);
} else {
m_ui->buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Apply);
- // Find and connect the apply button
- QPushButton* applyButton = m_ui->buttonBox->button(QDialogButtonBox::Apply);
- connect(applyButton, SIGNAL(clicked()), SIGNAL(apply()));
}
}
@@ -117,6 +116,17 @@ bool EditWidget::readOnly() const
return m_readOnly;
}
+void EditWidget::setModified(bool state)
+{
+ m_modified = state;
+ enableApplyButton(state);
+}
+
+bool EditWidget::isModified() const
+{
+ return m_modified;
+}
+
void EditWidget::enableApplyButton(bool enabled)
{
QPushButton* applyButton = m_ui->buttonBox->button(QDialogButtonBox::Apply);
@@ -125,6 +135,27 @@ void EditWidget::enableApplyButton(bool enabled)
}
}
+void EditWidget::showApplyButton(bool state)
+{
+ if (!m_readOnly) {
+ auto buttons = m_ui->buttonBox->standardButtons();
+ if (state) {
+ buttons |= QDialogButtonBox::Apply;
+ } else {
+ buttons &= ~QDialogButtonBox::Apply;
+ }
+ m_ui->buttonBox->setStandardButtons(buttons);
+ }
+}
+
+void EditWidget::buttonClicked(QAbstractButton* button)
+{
+ auto stdButton = m_ui->buttonBox->standardButton(button);
+ if (stdButton == QDialogButtonBox::Apply) {
+ emit apply();
+ }
+}
+
void EditWidget::showMessage(const QString& text, MessageWidget::MessageType type)
{
// Show error messages for a longer time to make sure the user can read them
diff --git a/src/gui/EditWidget.h b/src/gui/EditWidget.h
index f0d157c49..361961f76 100644
--- a/src/gui/EditWidget.h
+++ b/src/gui/EditWidget.h
@@ -49,6 +49,8 @@ public:
void setReadOnly(bool readOnly);
bool readOnly() const;
void enableApplyButton(bool enabled);
+ void showApplyButton(bool state);
+ virtual bool isModified() const;
signals:
void apply();
@@ -58,10 +60,13 @@ signals:
protected slots:
void showMessage(const QString& text, MessageWidget::MessageType type);
void hideMessage();
+ void setModified(bool state = true);
+ void buttonClicked(QAbstractButton* button);
private:
const QScopedPointer m_ui;
bool m_readOnly;
+ bool m_modified;
Q_DISABLE_COPY(EditWidget)
};
diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp
index 0eae0a373..8428a8613 100644
--- a/src/gui/MainWindow.cpp
+++ b/src/gui/MainWindow.cpp
@@ -41,7 +41,7 @@
#include "keys/FileKey.h"
#include "keys/PasswordKey.h"
-#ifdef WITH_XC_NETWORKING
+#ifdef WITH_XC_UPDATECHECK
#include "gui/MessageBox.h"
#include "gui/UpdateCheckDialog.h"
#include "updatecheck/UpdateChecker.h"
@@ -372,12 +372,12 @@ MainWindow::MainWindow()
setUnifiedTitleAndToolBarOnMac(true);
#endif
-#ifdef WITH_XC_NETWORKING
+#ifdef WITH_XC_UPDATECHECK
connect(m_ui->actionCheckForUpdates, SIGNAL(triggered()), SLOT(showUpdateCheckDialog()));
connect(UpdateChecker::instance(),
SIGNAL(updateCheckFinished(bool, QString, bool)),
SLOT(hasUpdateAvailable(bool, QString, bool)));
- QTimer::singleShot(3000, this, SLOT(showUpdateCheckStartup()));
+ QTimer::singleShot(500, this, SLOT(showUpdateCheckStartup()));
#else
m_ui->actionCheckForUpdates->setVisible(false);
#endif
@@ -670,7 +670,7 @@ void MainWindow::showAboutDialog()
void MainWindow::showUpdateCheckStartup()
{
-#ifdef WITH_XC_NETWORKING
+#ifdef WITH_XC_UPDATECHECK
if (!config()->get("UpdateCheckMessageShown", false).toBool()) {
auto result =
MessageBox::question(this,
@@ -693,7 +693,7 @@ void MainWindow::showUpdateCheckStartup()
void MainWindow::hasUpdateAvailable(bool hasUpdate, const QString& version, bool isManuallyRequested)
{
-#ifdef WITH_XC_NETWORKING
+#ifdef WITH_XC_UPDATECHECK
if (hasUpdate && !isManuallyRequested) {
auto* updateCheckDialog = new UpdateCheckDialog(this);
updateCheckDialog->showUpdateCheckResponse(hasUpdate, version);
@@ -708,7 +708,7 @@ void MainWindow::hasUpdateAvailable(bool hasUpdate, const QString& version, bool
void MainWindow::showUpdateCheckDialog()
{
-#ifdef WITH_XC_NETWORKING
+#ifdef WITH_XC_UPDATECHECK
updateCheck()->checkForUpdates(true);
auto* updateCheckDialog = new UpdateCheckDialog(this);
updateCheckDialog->show();
diff --git a/src/gui/MessageWidget.cpp b/src/gui/MessageWidget.cpp
index 5b18a583d..4b7e67a22 100644
--- a/src/gui/MessageWidget.cpp
+++ b/src/gui/MessageWidget.cpp
@@ -23,6 +23,7 @@
#include
const int MessageWidget::DefaultAutoHideTimeout = 6000;
+const int MessageWidget::LongAutoHideTimeout = 15000;
const int MessageWidget::DisableAutoHide = -1;
MessageWidget::MessageWidget(QWidget* parent)
diff --git a/src/gui/MessageWidget.h b/src/gui/MessageWidget.h
index eac506014..fe4baec4a 100644
--- a/src/gui/MessageWidget.h
+++ b/src/gui/MessageWidget.h
@@ -33,6 +33,7 @@ public:
int autoHideTimeout() const;
static const int DefaultAutoHideTimeout;
+ static const int LongAutoHideTimeout;
static const int DisableAutoHide;
signals:
diff --git a/src/gui/SearchWidget.cpp b/src/gui/SearchWidget.cpp
index 6e9b66929..5667852b0 100644
--- a/src/gui/SearchWidget.cpp
+++ b/src/gui/SearchWidget.cpp
@@ -1,5 +1,4 @@
/*
- * Copyright (C) 2016 Jonathan White
* Copyright (C) 2017 KeePassXC Team
*
* This program is free software: you can redistribute it and/or modify
@@ -97,6 +96,13 @@ bool SearchWidget::eventFilter(QObject* obj, QEvent* event)
if (keyEvent->key() == Qt::Key_Escape) {
emit escapePressed();
return true;
+ } else if (keyEvent->matches(QKeySequence::Copy)) {
+ // If Control+C is pressed in the search edit when no text
+ // is selected, copy the password of the current entry.
+ if (!m_ui->searchEdit->hasSelectedText()) {
+ emit copyPressed();
+ return true;
+ }
} else if (keyEvent->matches(QKeySequence::MoveToNextLine)) {
if (m_ui->searchEdit->cursorPosition() == m_ui->searchEdit->text().length()) {
// If down is pressed at EOL, move the focus to the entry view
diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetMasterKey.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetMasterKey.cpp
index d1a64b529..a8425aae1 100644
--- a/src/gui/dbsettings/DatabaseSettingsWidgetMasterKey.cpp
+++ b/src/gui/dbsettings/DatabaseSettingsWidgetMasterKey.cpp
@@ -77,11 +77,8 @@ void DatabaseSettingsWidgetMasterKey::load(QSharedPointer db)
// database has no key, we are about to add a new one
m_passwordEditWidget->changeVisiblePage(KeyComponentWidget::Page::Edit);
m_passwordEditWidget->setPasswordVisible(true);
- m_isDirty = true;
- return;
}
- bool isDirty = false;
bool hasAdditionalKeys = false;
for (const auto& key : m_db->key()->keys()) {
if (key->uuid() == PasswordKey::UUID) {
@@ -103,7 +100,9 @@ void DatabaseSettingsWidgetMasterKey::load(QSharedPointer db)
setAdditionalKeyOptionsVisible(hasAdditionalKeys);
- m_isDirty = isDirty;
+ connect(m_passwordEditWidget->findChild("removeButton"), SIGNAL(clicked()), SLOT(markDirty()));
+ connect(m_keyFileEditWidget->findChild("removeButton"), SIGNAL(clicked()), SLOT(markDirty()));
+ connect(m_yubiKeyEditWidget->findChild("removeButton"), SIGNAL(clicked()), SLOT(markDirty()));
}
void DatabaseSettingsWidgetMasterKey::initialize()
diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp
index 9a4c1600f..508f3c68c 100644
--- a/src/gui/entry/EditEntryWidget.cpp
+++ b/src/gui/entry/EditEntryWidget.cpp
@@ -276,55 +276,54 @@ void EditEntryWidget::setupHistory()
void EditEntryWidget::setupEntryUpdate()
{
// Entry tab
- connect(m_mainUi->titleEdit, SIGNAL(textChanged(QString)), this, SLOT(setUnsavedChanges()));
- connect(m_mainUi->usernameEdit, SIGNAL(textChanged(QString)), this, SLOT(setUnsavedChanges()));
- connect(m_mainUi->passwordEdit, SIGNAL(textChanged(QString)), this, SLOT(setUnsavedChanges()));
- connect(m_mainUi->passwordRepeatEdit, SIGNAL(textChanged(QString)), this, SLOT(setUnsavedChanges()));
- connect(m_mainUi->urlEdit, SIGNAL(textChanged(QString)), this, SLOT(setUnsavedChanges()));
+ connect(m_mainUi->titleEdit, SIGNAL(textChanged(QString)), this, SLOT(setModified()));
+ connect(m_mainUi->usernameEdit, SIGNAL(textChanged(QString)), this, SLOT(setModified()));
+ connect(m_mainUi->passwordEdit, SIGNAL(textChanged(QString)), this, SLOT(setModified()));
+ connect(m_mainUi->passwordRepeatEdit, SIGNAL(textChanged(QString)), this, SLOT(setModified()));
+ connect(m_mainUi->urlEdit, SIGNAL(textChanged(QString)), this, SLOT(setModified()));
#ifdef WITH_XC_NETWORKING
connect(m_mainUi->urlEdit, SIGNAL(textChanged(QString)), this, SLOT(updateFaviconButtonEnable(QString)));
#endif
- connect(m_mainUi->expireCheck, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges()));
- connect(m_mainUi->notesEnabled, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges()));
- connect(m_mainUi->expireDatePicker, SIGNAL(dateTimeChanged(QDateTime)), this, SLOT(setUnsavedChanges()));
- connect(m_mainUi->notesEdit, SIGNAL(textChanged()), this, SLOT(setUnsavedChanges()));
+ connect(m_mainUi->expireCheck, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
+ connect(m_mainUi->notesEnabled, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
+ connect(m_mainUi->expireDatePicker, SIGNAL(dateTimeChanged(QDateTime)), this, SLOT(setModified()));
+ connect(m_mainUi->notesEdit, SIGNAL(textChanged()), this, SLOT(setModified()));
// Advanced tab
- connect(m_advancedUi->attributesEdit, SIGNAL(textChanged()), this, SLOT(setUnsavedChanges()));
- connect(m_advancedUi->protectAttributeButton, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges()));
- connect(m_advancedUi->fgColorCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges()));
- connect(m_advancedUi->bgColorCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges()));
- connect(m_advancedUi->attachmentsWidget, SIGNAL(widgetUpdated()), this, SLOT(setUnsavedChanges()));
+ connect(m_advancedUi->attributesEdit, SIGNAL(textChanged()), this, SLOT(setModified()));
+ connect(m_advancedUi->protectAttributeButton, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
+ connect(m_advancedUi->fgColorCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
+ connect(m_advancedUi->bgColorCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
+ connect(m_advancedUi->attachmentsWidget, SIGNAL(widgetUpdated()), this, SLOT(setModified()));
// Icon tab
- connect(m_iconsWidget, SIGNAL(widgetUpdated()), this, SLOT(setUnsavedChanges()));
+ connect(m_iconsWidget, SIGNAL(widgetUpdated()), this, SLOT(setModified()));
// Auto-Type tab
- connect(m_autoTypeUi->enableButton, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges()));
- connect(m_autoTypeUi->customWindowSequenceButton, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges()));
- connect(m_autoTypeUi->inheritSequenceButton, SIGNAL(toggled(bool)), this, SLOT(setUnsavedChanges()));
- connect(m_autoTypeUi->customSequenceButton, SIGNAL(toggled(bool)), this, SLOT(setUnsavedChanges()));
- connect(m_autoTypeUi->windowSequenceEdit, SIGNAL(textChanged(QString)), this, SLOT(setUnsavedChanges()));
- connect(m_autoTypeUi->sequenceEdit, SIGNAL(textChanged(QString)), this, SLOT(setUnsavedChanges()));
- connect(m_autoTypeUi->windowTitleCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(setUnsavedChanges()));
- connect(m_autoTypeUi->windowTitleCombo, SIGNAL(editTextChanged(QString)), this, SLOT(setUnsavedChanges()));
+ connect(m_autoTypeUi->enableButton, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
+ connect(m_autoTypeUi->customWindowSequenceButton, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
+ connect(m_autoTypeUi->inheritSequenceButton, SIGNAL(toggled(bool)), this, SLOT(setModified()));
+ connect(m_autoTypeUi->customSequenceButton, SIGNAL(toggled(bool)), this, SLOT(setModified()));
+ connect(m_autoTypeUi->windowSequenceEdit, SIGNAL(textChanged(QString)), this, SLOT(setModified()));
+ connect(m_autoTypeUi->sequenceEdit, SIGNAL(textChanged(QString)), this, SLOT(setModified()));
+ connect(m_autoTypeUi->windowTitleCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(setModified()));
+ connect(m_autoTypeUi->windowTitleCombo, SIGNAL(editTextChanged(QString)), this, SLOT(setModified()));
// Properties and History tabs don't need extra connections
#ifdef WITH_XC_SSHAGENT
// SSH Agent tab
if (config()->get("SSHAgent", false).toBool()) {
- connect(m_sshAgentUi->attachmentRadioButton, SIGNAL(toggled(bool)), this, SLOT(setUnsavedChanges()));
- connect(m_sshAgentUi->externalFileRadioButton, SIGNAL(toggled(bool)), this, SLOT(setUnsavedChanges()));
- connect(m_sshAgentUi->attachmentComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(setUnsavedChanges()));
- connect(m_sshAgentUi->attachmentComboBox, SIGNAL(editTextChanged(QString)), this, SLOT(setUnsavedChanges()));
- connect(m_sshAgentUi->externalFileEdit, SIGNAL(textChanged(QString)), this, SLOT(setUnsavedChanges()));
- connect(m_sshAgentUi->addKeyToAgentCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges()));
- connect(m_sshAgentUi->removeKeyFromAgentCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges()));
- connect(
- m_sshAgentUi->requireUserConfirmationCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges()));
- connect(m_sshAgentUi->lifetimeCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges()));
- connect(m_sshAgentUi->lifetimeSpinBox, SIGNAL(valueChanged(int)), this, SLOT(setUnsavedChanges()));
+ connect(m_sshAgentUi->attachmentRadioButton, SIGNAL(toggled(bool)), this, SLOT(setModified()));
+ connect(m_sshAgentUi->externalFileRadioButton, SIGNAL(toggled(bool)), this, SLOT(setModified()));
+ connect(m_sshAgentUi->attachmentComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(setModified()));
+ connect(m_sshAgentUi->attachmentComboBox, SIGNAL(editTextChanged(QString)), this, SLOT(setModified()));
+ connect(m_sshAgentUi->externalFileEdit, SIGNAL(textChanged(QString)), this, SLOT(setModified()));
+ connect(m_sshAgentUi->addKeyToAgentCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
+ connect(m_sshAgentUi->removeKeyFromAgentCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
+ connect(m_sshAgentUi->requireUserConfirmationCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
+ connect(m_sshAgentUi->lifetimeCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
+ connect(m_sshAgentUi->lifetimeSpinBox, SIGNAL(valueChanged(int)), this, SLOT(setModified()));
}
#endif
}
@@ -703,8 +702,10 @@ void EditEntryWidget::loadEntry(Entry* entry,
setCurrentPage(0);
setPageHidden(m_historyWidget, m_history || m_entry->historyItems().count() < 1);
- // Force the user to Save/Apply/Discard new entries
- setUnsavedChanges(m_create);
+ // Force the user to Save/Discard new entries
+ showApplyButton(!m_create);
+
+ setModified(false);
}
void EditEntryWidget::setForms(Entry* entry, bool restore)
@@ -881,7 +882,6 @@ bool EditEntryWidget::commitEntry()
}
updateEntryData(m_entry);
- setUnsavedChanges(false);
if (!m_create) {
m_entry->endUpdate();
@@ -896,6 +896,7 @@ bool EditEntryWidget::commitEntry()
m_historyModel->setEntries(m_entry->historyItems());
showMessage(tr("Entry updated successfully."), MessageWidget::Positive);
+ setModified(false);
return true;
}
@@ -968,7 +969,7 @@ void EditEntryWidget::cancel()
m_entry->setIcon(Entry::DefaultIconNumber);
}
- if (!m_saved) {
+ if (isModified()) {
auto result = MessageBox::question(this,
QString(),
tr("Entry has unsaved changes"),
@@ -980,19 +981,26 @@ void EditEntryWidget::cancel()
}
if (result == MessageBox::Save) {
commitEntry();
- m_saved = true;
+ setModified(false);
}
}
clear();
- emit editFinished(m_saved);
+ emit editFinished(!isModified());
}
void EditEntryWidget::clear()
{
m_entry = nullptr;
m_db.reset();
+
+ m_mainUi->titleEdit->setText("");
+ m_mainUi->passwordEdit->setText("");
+ m_mainUi->passwordRepeatEdit->setText("");
+ m_mainUi->urlEdit->setText("");
+ m_mainUi->notesEdit->clear();
+
m_entryAttributes->clear();
m_advancedUi->attachmentsWidget->clearAttachments();
m_autoTypeAssoc->clear();
@@ -1001,22 +1009,6 @@ void EditEntryWidget::clear()
hideMessage();
}
-bool EditEntryWidget::hasBeenModified() const
-{
- // entry has been modified if a history item is to be deleted
- if (!m_historyModel->deletedEntries().isEmpty()) {
- return true;
- }
-
- // check if updating the entry would modify it
- auto* entry = new Entry();
- entry->copyDataFrom(m_entry.data());
-
- entry->beginUpdate();
- updateEntryData(entry);
- return entry->endUpdate();
-}
-
void EditEntryWidget::togglePasswordGeneratorButton(bool checked)
{
if (checked) {
@@ -1063,7 +1055,7 @@ void EditEntryWidget::insertAttribute()
m_advancedUi->attributesView->setCurrentIndex(index);
m_advancedUi->attributesView->edit(index);
- setUnsavedChanges(true);
+ setModified(true);
}
void EditEntryWidget::editCurrentAttribute()
@@ -1074,7 +1066,7 @@ void EditEntryWidget::editCurrentAttribute()
if (index.isValid()) {
m_advancedUi->attributesView->edit(index);
- setUnsavedChanges(true);
+ setModified(true);
}
}
@@ -1094,7 +1086,7 @@ void EditEntryWidget::removeCurrentAttribute()
if (result == MessageBox::Remove) {
m_entryAttributes->remove(m_attributesModel->keyByIndex(index));
- setUnsavedChanges(true);
+ setModified(true);
}
}
}
@@ -1216,7 +1208,7 @@ void EditEntryWidget::insertAutoTypeAssoc()
m_autoTypeUi->assocView->setCurrentIndex(newIndex);
loadCurrentAssoc(newIndex);
m_autoTypeUi->windowTitleCombo->setFocus();
- setUnsavedChanges(true);
+ setModified(true);
}
void EditEntryWidget::removeAutoTypeAssoc()
@@ -1225,7 +1217,7 @@ void EditEntryWidget::removeAutoTypeAssoc()
if (currentIndex.isValid()) {
m_autoTypeAssoc->remove(currentIndex.row());
- setUnsavedChanges(true);
+ setModified(true);
}
}
@@ -1288,7 +1280,7 @@ void EditEntryWidget::restoreHistoryEntry()
QModelIndex index = m_sortModel->mapToSource(m_historyUi->historyView->currentIndex());
if (index.isValid()) {
setForms(m_historyModel->entryFromIndex(index), true);
- setUnsavedChanges(true);
+ setModified(true);
}
}
@@ -1302,7 +1294,7 @@ void EditEntryWidget::deleteHistoryEntry()
} else {
m_historyUi->deleteAllButton->setEnabled(false);
}
- setUnsavedChanges(true);
+ setModified(true);
}
}
@@ -1310,7 +1302,7 @@ void EditEntryWidget::deleteAllHistoryEntries()
{
m_historyModel->deleteAll();
m_historyUi->deleteAllButton->setEnabled(m_historyModel->rowCount() > 0);
- setUnsavedChanges(true);
+ setModified(true);
}
QMenu* EditEntryWidget::createPresetsMenu()
@@ -1363,12 +1355,6 @@ void EditEntryWidget::pickColor()
QColor newColor = QColorDialog::getColor(oldColor);
if (newColor.isValid()) {
setupColorButton(isForeground, newColor);
- setUnsavedChanges(true);
+ setModified(true);
}
}
-
-void EditEntryWidget::setUnsavedChanges(bool hasUnsaved)
-{
- m_saved = !hasUnsaved;
- enableApplyButton(hasUnsaved);
-}
diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h
index 4c6870594..aea3c894b 100644
--- a/src/gui/entry/EditEntryWidget.h
+++ b/src/gui/entry/EditEntryWidget.h
@@ -68,7 +68,6 @@ public:
QString entryTitle() const;
void clear();
- bool hasBeenModified() const;
signals:
void editFinished(bool accepted);
@@ -106,7 +105,6 @@ private slots:
void useExpiryPreset(QAction* action);
void toggleHideNotes(bool visible);
void pickColor();
- void setUnsavedChanges(bool hasUnsaved = true);
#ifdef WITH_XC_SSHAGENT
void updateSSHAgent();
void updateSSHAgentAttachment();
@@ -148,7 +146,6 @@ private:
bool m_create;
bool m_history;
- bool m_saved;
#ifdef WITH_XC_SSHAGENT
bool m_sshAgentEnabled;
KeeAgentSettings m_sshAgentSettings;
diff --git a/src/gui/group/EditGroupWidget.cpp b/src/gui/group/EditGroupWidget.cpp
index 6c869cf28..fe83a943e 100644
--- a/src/gui/group/EditGroupWidget.cpp
+++ b/src/gui/group/EditGroupWidget.cpp
@@ -22,6 +22,7 @@
#include "core/Metadata.h"
#include "gui/EditWidgetIcons.h"
#include "gui/EditWidgetProperties.h"
+#include "gui/MessageBox.h"
#if defined(WITH_XC_KEESHARE)
#include "keeshare/group/EditGroupPageKeeShare.h"
@@ -46,6 +47,11 @@ public:
editPage->assign(widget);
}
+ QWidget* getWidget()
+ {
+ return widget;
+ }
+
private:
QSharedPointer editPage;
QWidget* widget;
@@ -85,18 +91,38 @@ EditGroupWidget::EditGroupWidget(QWidget* parent)
// clang-format on
connect(m_editGroupWidgetIcons, SIGNAL(messageEditEntryDismiss()), SLOT(hideMessage()));
+
+ setupModifiedTracking();
}
EditGroupWidget::~EditGroupWidget()
{
}
+void EditGroupWidget::setupModifiedTracking()
+{
+ // Group tab
+ connect(m_mainUi->editName, SIGNAL(textChanged(QString)), SLOT(setModified()));
+ connect(m_mainUi->editNotes, SIGNAL(textChanged()), SLOT(setModified()));
+ connect(m_mainUi->expireCheck, SIGNAL(stateChanged(int)), SLOT(setModified()));
+ connect(m_mainUi->expireDatePicker, SIGNAL(dateTimeChanged(QDateTime)), SLOT(setModified()));
+ connect(m_mainUi->searchComboBox, SIGNAL(currentIndexChanged(int)), SLOT(setModified()));
+ connect(m_mainUi->autotypeComboBox, SIGNAL(currentIndexChanged(int)), SLOT(setModified()));
+ connect(m_mainUi->autoTypeSequenceInherit, SIGNAL(toggled(bool)), SLOT(setModified()));
+ connect(m_mainUi->autoTypeSequenceCustomRadio, SIGNAL(toggled(bool)), SLOT(setModified()));
+ connect(m_mainUi->autoTypeSequenceCustomEdit, SIGNAL(textChanged(QString)), SLOT(setModified()));
+
+ // Icon tab
+ connect(m_editGroupWidgetIcons, SIGNAL(widgetUpdated()), SLOT(setModified()));
+}
+
void EditGroupWidget::loadGroup(Group* group, bool create, const QSharedPointer& database)
{
m_group = group;
m_db = database;
m_temporaryGroup.reset(group->clone(Entry::CloneNoFlags, Group::CloneNoFlags));
+ connect(m_temporaryGroup->customData(), SIGNAL(customDataModified()), SLOT(setModified()));
if (create) {
setHeadline(tr("Add group"));
@@ -139,6 +165,11 @@ void EditGroupWidget::loadGroup(Group* group, bool create, const QSharedPointer<
setCurrentPage(0);
m_mainUi->editName->setFocus();
+
+ // Force the user to Save/Discard new groups
+ showApplyButton(!create);
+
+ setModified(false);
}
void EditGroupWidget::save()
@@ -180,6 +211,8 @@ void EditGroupWidget::apply()
// Icons add/remove are applied globally outside the transaction!
m_group->copyDataFrom(m_temporaryGroup.data());
+
+ setModified(false);
}
void EditGroupWidget::cancel()
@@ -188,6 +221,18 @@ void EditGroupWidget::cancel()
m_group->setIcon(Entry::DefaultIconNumber);
}
+ if (isModified()) {
+ auto result = MessageBox::question(this,
+ QString(),
+ tr("Entry has unsaved changes"),
+ MessageBox::Cancel | MessageBox::Save | MessageBox::Discard,
+ MessageBox::Cancel);
+ if (result == MessageBox::Save) {
+ apply();
+ setModified(false);
+ }
+ }
+
clear();
emit editFinished(false);
}
diff --git a/src/gui/group/EditGroupWidget.h b/src/gui/group/EditGroupWidget.h
index fd744503c..cc8738d8c 100644
--- a/src/gui/group/EditGroupWidget.h
+++ b/src/gui/group/EditGroupWidget.h
@@ -74,6 +74,7 @@ private:
void addTriStateItems(QComboBox* comboBox, bool inheritValue);
int indexFromTriState(Group::TriState triState);
Group::TriState triStateFromIndex(int index);
+ void setupModifiedTracking();
const QScopedPointer m_mainUi;
diff --git a/src/gui/masterkey/KeyComponentWidget.cpp b/src/gui/masterkey/KeyComponentWidget.cpp
index 7d795aca1..769cbab95 100644
--- a/src/gui/masterkey/KeyComponentWidget.cpp
+++ b/src/gui/masterkey/KeyComponentWidget.cpp
@@ -37,7 +37,7 @@ KeyComponentWidget::KeyComponentWidget(const QString& name, QWidget* parent)
connect(m_ui->removeButton, SIGNAL(clicked(bool)), SIGNAL(componentRemovalRequested()));
connect(m_ui->cancelButton, SIGNAL(clicked(bool)), SLOT(cancelEdit()));
- connect(m_ui->stackedWidget, SIGNAL(currentChanged(int)), SLOT(reset()));
+ connect(m_ui->stackedWidget, SIGNAL(currentChanged(int)), SLOT(resetComponentEditWidget()));
connect(this, SIGNAL(nameChanged(QString)), SLOT(updateComponentName(QString)));
connect(this, SIGNAL(descriptionChanged(QString)), SLOT(updateComponentDescription(QString)));
@@ -46,11 +46,13 @@ KeyComponentWidget::KeyComponentWidget(const QString& name, QWidget* parent)
connect(this, SIGNAL(componentRemovalRequested()), SLOT(doRemove()));
connect(this, SIGNAL(componentAddChanged(bool)), SLOT(updateAddStatus(bool)));
- blockSignals(true);
+ bool prev = blockSignals(true);
setComponentName(name);
+ blockSignals(prev);
+
+ prev = m_ui->stackedWidget->blockSignals(true);
m_ui->stackedWidget->setCurrentIndex(Page::AddNew);
- updateSize();
- blockSignals(false);
+ m_ui->stackedWidget->blockSignals(prev);
}
KeyComponentWidget::~KeyComponentWidget()
@@ -164,21 +166,22 @@ void KeyComponentWidget::cancelEdit()
emit editCanceled();
}
-void KeyComponentWidget::reset()
+void KeyComponentWidget::showEvent(QShowEvent* event)
{
- if (static_cast(m_ui->stackedWidget->currentIndex()) == Page::Edit) {
- if (!m_ui->componentWidgetLayout->isEmpty()) {
- auto* item = m_ui->componentWidgetLayout->takeAt(0);
- if (item->widget()) {
- delete item->widget();
- }
- delete item;
+ resetComponentEditWidget();
+ QWidget::showEvent(event);
+}
+
+void KeyComponentWidget::resetComponentEditWidget()
+{
+ if (!m_componentWidget || static_cast(m_ui->stackedWidget->currentIndex()) == Page::Edit) {
+ if (m_componentWidget) {
+ delete m_componentWidget;
}
- QWidget* widget = componentEditWidget();
- m_ui->componentWidgetLayout->addWidget(widget);
-
- initComponentEditWidget(widget);
+ m_componentWidget = componentEditWidget();
+ m_ui->componentWidgetLayout->addWidget(m_componentWidget);
+ initComponentEditWidget(m_componentWidget);
}
QTimer::singleShot(0, this, SLOT(updateSize()));
diff --git a/src/gui/masterkey/KeyComponentWidget.h b/src/gui/masterkey/KeyComponentWidget.h
index cf2ae4947..63079863e 100644
--- a/src/gui/masterkey/KeyComponentWidget.h
+++ b/src/gui/masterkey/KeyComponentWidget.h
@@ -20,6 +20,7 @@
#include
#include
+#include
namespace Ui
{
@@ -109,6 +110,9 @@ signals:
void editCanceled();
void componentRemovalRequested();
+protected:
+ void showEvent(QShowEvent* event) override ;
+
private slots:
void updateComponentName(const QString& name);
void updateComponentDescription(const QString& decription);
@@ -117,7 +121,7 @@ private slots:
void doEdit();
void doRemove();
void cancelEdit();
- void reset();
+ void resetComponentEditWidget();
void updateSize();
private:
@@ -125,6 +129,7 @@ private:
Page m_previousPage = Page::AddNew;
QString m_componentName;
QString m_componentDescription;
+ QPointer m_componentWidget;
const QScopedPointer m_ui;
};
diff --git a/src/gui/masterkey/PasswordEditWidget.cpp b/src/gui/masterkey/PasswordEditWidget.cpp
index 86d629da0..de00199bb 100644
--- a/src/gui/masterkey/PasswordEditWidget.cpp
+++ b/src/gui/masterkey/PasswordEditWidget.cpp
@@ -92,6 +92,18 @@ void PasswordEditWidget::initComponentEditWidget(QWidget* widget)
m_compUi->enterPasswordEdit->setFocus();
}
+void PasswordEditWidget::hideEvent(QHideEvent* event)
+{
+ Q_ASSERT(m_compUi->enterPasswordEdit);
+
+ if (!isVisible() && m_compUi->enterPasswordEdit) {
+ m_compUi->enterPasswordEdit->setText("");
+ m_compUi->repeatPasswordEdit->setText("");
+ }
+
+ QWidget::hideEvent(event);
+}
+
bool PasswordEditWidget::validate(QString& errorMessage) const
{
if (m_compUi->enterPasswordEdit->text() != m_compUi->repeatPasswordEdit->text()) {
diff --git a/src/gui/masterkey/PasswordEditWidget.h b/src/gui/masterkey/PasswordEditWidget.h
index 9f3eb75ce..57c225c1f 100644
--- a/src/gui/masterkey/PasswordEditWidget.h
+++ b/src/gui/masterkey/PasswordEditWidget.h
@@ -44,6 +44,7 @@ public:
protected:
QWidget* componentEditWidget() override;
void initComponentEditWidget(QWidget* widget) override;
+ void hideEvent(QHideEvent* event) override;
private slots:
void showPasswordGenerator();
diff --git a/src/keeshare/KeeShare.cpp b/src/keeshare/KeeShare.cpp
index 08c7b4f17..d1cbde099 100644
--- a/src/keeshare/KeeShare.cpp
+++ b/src/keeshare/KeeShare.cpp
@@ -162,18 +162,32 @@ QString KeeShare::sharingLabel(const Group* group)
}
const auto reference = referenceOf(share);
+ if (!reference.isValid()) {
+ return tr("Invalid sharing reference");
+ }
+ QStringList messages;
switch (reference.type) {
case KeeShareSettings::Inactive:
- return tr("Disabled share %1").arg(reference.path);
+ messages << tr("Inactive share %1").arg(reference.path);
+ break;
case KeeShareSettings::ImportFrom:
- return tr("Import from share %1").arg(reference.path);
+ messages << tr("Imported from %1").arg(reference.path);
+ break;
case KeeShareSettings::ExportTo:
- return tr("Export to share %1").arg(reference.path);
+ messages << tr("Exported to %1").arg(reference.path);
+ break;
case KeeShareSettings::SynchronizeWith:
- return tr("Synchronize with share %1").arg(reference.path);
+ messages << tr("Synchronized with %1").arg(reference.path);
+ break;
}
-
- return {};
+ const auto active = KeeShare::active();
+ if (reference.isImporting() && !active.in) {
+ messages << tr("Import is disabled in settings");
+ }
+ if (reference.isExporting() && !active.out) {
+ messages << tr("Export is disabled in settings");
+ }
+ return messages.join("\n");
}
QPixmap KeeShare::indicatorBadge(const Group* group, QPixmap pixmap)
@@ -196,13 +210,13 @@ QString KeeShare::referenceTypeLabel(const KeeShareSettings::Reference& referenc
{
switch (reference.type) {
case KeeShareSettings::Inactive:
- return tr("Disabled share");
+ return tr("Inactive share");
case KeeShareSettings::ImportFrom:
- return tr("Import from");
+ return tr("Imported from");
case KeeShareSettings::ExportTo:
- return tr("Export to");
+ return tr("Exported to");
case KeeShareSettings::SynchronizeWith:
- return tr("Synchronize with");
+ return tr("Synchronized with");
}
return "";
}
diff --git a/src/keeshare/ShareObserver.cpp b/src/keeshare/ShareObserver.cpp
index 63d8358c2..295883ab8 100644
--- a/src/keeshare/ShareObserver.cpp
+++ b/src/keeshare/ShareObserver.cpp
@@ -84,7 +84,7 @@ namespace
key.openKey(QString());
const auto signer = Signature();
if (!signer.verify(data, sign.signature, key)) {
- qCritical("Invalid signature for sharing container %s.", qPrintable(reference.path));
+ qCritical("Invalid signature for shared container %s.", qPrintable(reference.path));
return {Invalid, KeeShareSettings::Certificate()};
}
@@ -190,7 +190,6 @@ void ShareObserver::reinitialize()
KeeShareSettings::Reference newReference;
};
- const auto active = KeeShare::active();
QList updated;
const QList groups = m_db->rootGroup()->groupsRecursive(true);
for (Group* group : groups) {
@@ -202,9 +201,7 @@ void ShareObserver::reinitialize()
m_groupToReference.remove(couple.group);
m_referenceToGroup.remove(couple.oldReference);
m_shareToGroup.remove(couple.oldReference.path);
- if (couple.newReference.isValid()
- && ((active.in && couple.newReference.isImporting())
- || (active.out && couple.newReference.isExporting()))) {
+ if (couple.newReference.isValid()) {
m_groupToReference[couple.group] = couple.newReference;
m_referenceToGroup[couple.newReference] = couple.group;
m_shareToGroup[couple.newReference.path] = couple.group;
diff --git a/src/keeshare/group/EditGroupWidgetKeeShare.cpp b/src/keeshare/group/EditGroupWidgetKeeShare.cpp
index 49e640639..0170a82af 100644
--- a/src/keeshare/group/EditGroupWidgetKeeShare.cpp
+++ b/src/keeshare/group/EditGroupWidgetKeeShare.cpp
@@ -68,13 +68,13 @@ EditGroupWidgetKeeShare::EditGroupWidgetKeeShare(QWidget* parent)
name = tr("Inactive");
break;
case KeeShareSettings::ImportFrom:
- name = tr("Import from path");
+ name = tr("Import");
break;
case KeeShareSettings::ExportTo:
- name = tr("Export to path");
+ name = tr("Export");
break;
case KeeShareSettings::SynchronizeWith:
- name = tr("Synchronize with path");
+ name = tr("Synchronize");
break;
}
m_ui->typeComboBox->insertItem(type, name, static_cast(type));
@@ -124,10 +124,10 @@ void EditGroupWidgetKeeShare::showSharingState()
}
}
if (!supported) {
- m_ui->messageWidget->showMessage(
- tr("Your KeePassXC version does not support sharing your container type. Please use %1.")
- .arg(supportedExtensions.join(", ")),
- MessageWidget::Warning);
+ m_ui->messageWidget->showMessage(tr("Your KeePassXC version does not support sharing this container type.\n"
+ "Supported extensions are: %1.")
+ .arg(supportedExtensions.join(", ")),
+ MessageWidget::Warning);
return;
}
@@ -149,18 +149,18 @@ void EditGroupWidgetKeeShare::showSharingState()
(other.isImporting() && reference.isExporting()) || (other.isExporting() && reference.isImporting());
}
if (conflictExport) {
- m_ui->messageWidget->showMessage(tr("The export container %1 is already referenced.").arg(reference.path),
+ m_ui->messageWidget->showMessage(tr("%1 is already being exported by this database.").arg(reference.path),
MessageWidget::Error);
return;
}
if (multipleImport) {
- m_ui->messageWidget->showMessage(tr("The import container %1 is already imported.").arg(reference.path),
+ m_ui->messageWidget->showMessage(tr("%1 is already being imported by this database.").arg(reference.path),
MessageWidget::Warning);
return;
}
if (cycleImportExport) {
m_ui->messageWidget->showMessage(
- tr("The container %1 imported and export by different groups.").arg(reference.path),
+ tr("%1 is being imported and exported by different groups in this database.").arg(reference.path),
MessageWidget::Warning);
return;
}
@@ -169,15 +169,20 @@ void EditGroupWidgetKeeShare::showSharingState()
}
const auto active = KeeShare::active();
if (!active.in && !active.out) {
- m_ui->messageWidget->showMessage(tr("Database sharing is disabled"), MessageWidget::Information);
+ m_ui->messageWidget->showMessage(
+ tr("KeeShare is currently disabled. You can enable import/export in the application settings.",
+ "KeeShare is a proper noun"),
+ MessageWidget::Information);
return;
}
if (active.in && !active.out) {
- m_ui->messageWidget->showMessage(tr("Database export is disabled"), MessageWidget::Information);
+ m_ui->messageWidget->showMessage(tr("Database export is currently disabled by application settings."),
+ MessageWidget::Information);
return;
}
if (!active.in && active.out) {
- m_ui->messageWidget->showMessage(tr("Database import is disabled"), MessageWidget::Information);
+ m_ui->messageWidget->showMessage(tr("Database import is currently disabled by application settings."),
+ MessageWidget::Information);
return;
}
}
diff --git a/src/updatecheck/UpdateChecker.cpp b/src/updatecheck/UpdateChecker.cpp
index 4272410b6..145312907 100644
--- a/src/updatecheck/UpdateChecker.cpp
+++ b/src/updatecheck/UpdateChecker.cpp
@@ -17,6 +17,7 @@
#include "UpdateChecker.h"
#include "config-keepassx.h"
+#include "core/Clock.h"
#include "core/Config.h"
#include
#include
@@ -38,24 +39,28 @@ UpdateChecker::~UpdateChecker()
void UpdateChecker::checkForUpdates(bool manuallyRequested)
{
+ auto nextCheck = config()->get("GUI/CheckForUpdatesNextCheck", 0).toULongLong();
m_isManuallyRequested = manuallyRequested;
- m_bytesReceived.clear();
- QString apiUrlStr = QString("https://api.github.com/repos/keepassxreboot/keepassxc/releases");
+ if (m_isManuallyRequested || Clock::currentSecondsSinceEpoch() >= nextCheck) {
+ m_bytesReceived.clear();
- if (!config()->get("GUI/CheckForUpdatesIncludeBetas", false).toBool()) {
- apiUrlStr += "/latest";
+ QString apiUrlStr = QString("https://api.github.com/repos/keepassxreboot/keepassxc/releases");
+
+ if (!config()->get("GUI/CheckForUpdatesIncludeBetas", false).toBool()) {
+ apiUrlStr += "/latest";
+ }
+
+ QUrl apiUrl = QUrl(apiUrlStr);
+
+ QNetworkRequest request(apiUrl);
+ request.setRawHeader("Accept", "application/json");
+
+ m_reply = m_netMgr->get(request);
+
+ connect(m_reply, &QNetworkReply::finished, this, &UpdateChecker::fetchFinished);
+ connect(m_reply, &QIODevice::readyRead, this, &UpdateChecker::fetchReadyRead);
}
-
- QUrl apiUrl = QUrl(apiUrlStr);
-
- QNetworkRequest request(apiUrl);
- request.setRawHeader("Accept", "application/json");
-
- m_reply = m_netMgr->get(request);
-
- connect(m_reply, &QNetworkReply::finished, this, &UpdateChecker::fetchFinished);
- connect(m_reply, &QIODevice::readyRead, this, &UpdateChecker::fetchReadyRead);
}
void UpdateChecker::fetchReadyRead()
@@ -84,8 +89,12 @@ void UpdateChecker::fetchFinished()
if (!jsonObject.value("tag_name").isUndefined()) {
version = jsonObject.value("tag_name").toString();
- hasNewVersion = compareVersions(version, QString(KEEPASSXC_VERSION));
+ hasNewVersion = compareVersions(QString(KEEPASSXC_VERSION), version);
}
+
+ // Check again in 7 days
+ // TODO: change to toSecsSinceEpoch() when min Qt >= 5.8
+ config()->set("GUI/CheckForUpdatesNextCheck", Clock::currentDateTime().addDays(7).toTime_t());
} else {
version = "error";
}
@@ -93,38 +102,46 @@ void UpdateChecker::fetchFinished()
emit updateCheckFinished(hasNewVersion, version, m_isManuallyRequested);
}
-bool UpdateChecker::compareVersions(const QString& remoteVersion, const QString& localVersion)
+bool UpdateChecker::compareVersions(const QString& localVersion, const QString& remoteVersion)
{
+ // Quick full-string equivalence check
if (localVersion == remoteVersion) {
- return false; // Currently using updated version
+ return false;
}
- QRegularExpression verRegex("^(\\d+(\\.\\d+){0,2})(-\\w+)?$", QRegularExpression::CaseInsensitiveOption);
+ QRegularExpression verRegex(R"(^((?:\d+\.){2}\d+)(?:-(\w+?)(\d+)?)?$)");
- QRegularExpressionMatch lmatch = verRegex.match(localVersion);
- QRegularExpressionMatch rmatch = verRegex.match(remoteVersion);
+ auto lmatch = verRegex.match(localVersion);
+ auto rmatch = verRegex.match(remoteVersion);
- if (!lmatch.captured(1).isNull() && !rmatch.captured(1).isNull()) {
- if (lmatch.captured(1) == rmatch.captured(1) && !lmatch.captured(3).isNull()) {
- // Same version, but installed version has snapshot/beta suffix and should be updated to stable
- return true;
+ auto lVersion = lmatch.captured(1).split(".");
+ auto lSuffix = lmatch.captured(2);
+ auto lBetaNum = lmatch.captured(3);
+
+ auto rVersion = rmatch.captured(1).split(".");
+ auto rSuffix = rmatch.captured(2);
+ auto rBetaNum = rmatch.captured(3);
+
+ if (!lVersion.isEmpty() && !rVersion.isEmpty()) {
+ if (lSuffix.compare("snapshot", Qt::CaseInsensitive) == 0) {
+ // Snapshots are not checked for version updates
+ return false;
}
- QStringList lparts = lmatch.captured(1).split(".");
- QStringList rparts = rmatch.captured(1).split(".");
-
- if (lparts.length() < 3)
- lparts << "0";
-
- if (rparts.length() < 3)
- rparts << "0";
+ // Check "-beta[X]" versions
+ if (lVersion == rVersion && !lSuffix.isEmpty()) {
+ // Check if stable version has been released or new beta is available
+ // otherwise the version numbers are equal
+ return rSuffix.isEmpty() || lBetaNum.toInt() < rBetaNum.toInt();
+ }
for (int i = 0; i < 3; i++) {
- int l = lparts[i].toInt();
- int r = rparts[i].toInt();
+ int l = lVersion[i].toInt();
+ int r = rVersion[i].toInt();
- if (l == r)
+ if (l == r) {
continue;
+ }
if (l > r) {
return false; // Installed version is newer than release
diff --git a/src/updatecheck/UpdateChecker.h b/src/updatecheck/UpdateChecker.h
index ac6471d64..64430bda3 100644
--- a/src/updatecheck/UpdateChecker.h
+++ b/src/updatecheck/UpdateChecker.h
@@ -31,7 +31,7 @@ public:
~UpdateChecker() override;
void checkForUpdates(bool manuallyRequested);
- static bool compareVersions(const QString& remoteVersion, const QString& localVersion);
+ static bool compareVersions(const QString& localVersion, const QString& remoteVersion);
static UpdateChecker* instance();
signals:
diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp
index 3ba40b904..9574f6d32 100644
--- a/tests/TestCli.cpp
+++ b/tests/TestCli.cpp
@@ -702,8 +702,7 @@ void TestCli::testKeyFileOption()
m_stdoutFile->readLine(); // skip password prompt
m_stderrFile->seek(posErr);
QCOMPARE(m_stdoutFile->readAll(), QByteArray(""));
- QCOMPARE(m_stderrFile->readAll(),
- QByteArray("Error while reading the database: Wrong key or database file is corrupt. (HMAC mismatch)\n"));
+ QVERIFY(m_stderrFile->readAll().contains("Invalid credentials were provided"));
// Should raise an error if key file path is invalid.
pos = m_stdoutFile->pos();
@@ -736,8 +735,7 @@ void TestCli::testNoPasswordOption()
m_stdoutFile->readLine(); // skip password prompt
m_stderrFile->seek(posErr);
QCOMPARE(m_stdoutFile->readAll(), QByteArray(""));
- QCOMPARE(m_stderrFile->readAll(),
- QByteArray("Error while reading the database: Wrong key or database file is corrupt. (HMAC mismatch)\n"));
+ QVERIFY(m_stderrFile->readAll().contains("Invalid credentials were provided"));
}
void TestCli::testList()
diff --git a/tests/TestTools.cpp b/tests/TestTools.cpp
index de5a80c0a..100eb6306 100644
--- a/tests/TestTools.cpp
+++ b/tests/TestTools.cpp
@@ -59,6 +59,7 @@ void TestTools::testIsBase64()
QVERIFY(Tools::isBase64(QByteArray("12==")));
QVERIFY(Tools::isBase64(QByteArray("abcd9876MN==")));
QVERIFY(Tools::isBase64(QByteArray("abcd9876DEFGhijkMNO=")));
+ QVERIFY(Tools::isBase64(QByteArray("abcd987/DEFGh+jk/NO=")));
QVERIFY(not Tools::isBase64(QByteArray("abcd123==")));
QVERIFY(not Tools::isBase64(QByteArray("abc_")));
QVERIFY(not Tools::isBase64(QByteArray("123")));
diff --git a/tests/TestUpdateCheck.cpp b/tests/TestUpdateCheck.cpp
index 8cba43b1d..ff709cd56 100644
--- a/tests/TestUpdateCheck.cpp
+++ b/tests/TestUpdateCheck.cpp
@@ -29,13 +29,32 @@ void TestUpdateCheck::initTestCase()
void TestUpdateCheck::testCompareVersion()
{
- // Remote Version , Installed Version
- QCOMPARE(UpdateChecker::compareVersions(QString("2.4.0"), QString("2.3.4")), true);
- QCOMPARE(UpdateChecker::compareVersions(QString("2.3.0"), QString("2.4.0")), false);
+ // No upgrade
QCOMPARE(UpdateChecker::compareVersions(QString("2.3.0"), QString("2.3.0")), false);
- QCOMPARE(UpdateChecker::compareVersions(QString("2.3.0"), QString("2.3.0-beta1")), true);
- QCOMPARE(UpdateChecker::compareVersions(QString("2.3.0-beta2"), QString("2.3.0-beta1")), true);
- QCOMPARE(UpdateChecker::compareVersions(QString("2.3.4"), QString("2.4.0-snapshot")), false);
- QCOMPARE(UpdateChecker::compareVersions(QString("invalid"), QString("2.4.0")), false);
- QCOMPARE(UpdateChecker::compareVersions(QString(""), QString("2.4.0")), false);
+
+ // First digit upgrade
+ QCOMPARE(UpdateChecker::compareVersions(QString("2.4.0"), QString("3.0.0")), true);
+ QCOMPARE(UpdateChecker::compareVersions(QString("3.0.0"), QString("2.4.0")), false);
+
+ // Second digit upgrade
+ QCOMPARE(UpdateChecker::compareVersions(QString("2.3.4"), QString("2.4.0")), true);
+ QCOMPARE(UpdateChecker::compareVersions(QString("2.4.0"), QString("2.3.4")), false);
+
+ // Third digit upgrade
+ QCOMPARE(UpdateChecker::compareVersions(QString("2.3.0"), QString("2.3.1")), true);
+ QCOMPARE(UpdateChecker::compareVersions(QString("2.3.1"), QString("2.3.0")), false);
+
+ // Beta builds
+ QCOMPARE(UpdateChecker::compareVersions(QString("2.3.0"), QString("2.3.0-beta1")), false);
+ QCOMPARE(UpdateChecker::compareVersions(QString("2.3.0"), QString("2.3.1-beta1")), true);
+ QCOMPARE(UpdateChecker::compareVersions(QString("2.3.0-beta1"), QString("2.3.0")), true);
+ QCOMPARE(UpdateChecker::compareVersions(QString("2.3.0-beta"), QString("2.3.0-beta1")), true);
+ QCOMPARE(UpdateChecker::compareVersions(QString("2.3.0-beta1"), QString("2.3.0-beta")), false);
+ QCOMPARE(UpdateChecker::compareVersions(QString("2.3.0-beta1"), QString("2.3.0-beta2")), true);
+ QCOMPARE(UpdateChecker::compareVersions(QString("2.3.0-beta2"), QString("2.3.0-beta1")), false);
+
+ // Snapshot and invalid data
+ QCOMPARE(UpdateChecker::compareVersions(QString("2.3.4-snapshot"), QString("2.4.0")), false);
+ QCOMPARE(UpdateChecker::compareVersions(QString("2.4.0"), QString("invalid")), false);
+ QCOMPARE(UpdateChecker::compareVersions(QString("2.4.0"), QString("")), false);
}
diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp
index 127563798..63e36fbaa 100644
--- a/tests/gui/TestGui.cpp
+++ b/tests/gui/TestGui.cpp
@@ -429,13 +429,20 @@ void TestGui::testEditEntry()
auto* titleEdit = editEntryWidget->findChild("titleEdit");
QTest::keyClicks(titleEdit, "_test");
- // Apply the edit
auto* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox");
QVERIFY(editEntryWidgetButtonBox);
- QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Apply), Qt::LeftButton);
+ auto* okButton = editEntryWidgetButtonBox->button(QDialogButtonBox::Ok);
+ QVERIFY(okButton);
+ auto* applyButton = editEntryWidgetButtonBox->button(QDialogButtonBox::Apply);
+ QVERIFY(applyButton);
+
+ // Apply the edit
+ QTRY_VERIFY(applyButton->isEnabled());
+ QTest::mouseClick(applyButton, Qt::LeftButton);
QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
QCOMPARE(entry->title(), QString("Sample Entry_test"));
QCOMPARE(entry->historyItems().size(), ++editCount);
+ QVERIFY(!applyButton->isEnabled());
// Test entry colors (simulate choosing a color)
editEntryWidget->setCurrentPage(1);
@@ -451,7 +458,7 @@ void TestGui::testEditEntry()
colorCheckBox = editEntryWidget->findChild("bgColorCheckBox");
colorButton->setProperty("color", bgColor);
colorCheckBox->setChecked(true);
- QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Apply), Qt::LeftButton);
+ QTest::mouseClick(applyButton, Qt::LeftButton);
QCOMPARE(entry->historyItems().size(), ++editCount);
// Test protected attributes
@@ -471,7 +478,7 @@ void TestGui::testEditEntry()
auto* passwordEdit = editEntryWidget->findChild("passwordEdit");
QString originalPassword = passwordEdit->text();
passwordEdit->setText("newpass");
- QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
+ QTest::mouseClick(okButton, Qt::LeftButton);
auto* messageWiget = editEntryWidget->findChild("messageWidget");
QTRY_VERIFY(messageWiget->isVisible());
QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
@@ -479,7 +486,7 @@ void TestGui::testEditEntry()
passwordEdit->setText(originalPassword);
// Save the edit (press OK)
- QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
+ QTest::mouseClick(okButton, Qt::LeftButton);
QApplication::processEvents();
// Confirm edit was made
@@ -496,13 +503,15 @@ void TestGui::testEditEntry()
// Test copy & paste newline sanitization
QTest::mouseClick(entryEditWidget, Qt::LeftButton);
+ okButton = editEntryWidgetButtonBox->button(QDialogButtonBox::Ok);
+ QVERIFY(okButton);
QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditMode);
titleEdit->setText("multiline\ntitle");
editEntryWidget->findChild("usernameEdit")->setText("multiline\nusername");
editEntryWidget->findChild("passwordEdit")->setText("multiline\npassword");
editEntryWidget->findChild("passwordRepeatEdit")->setText("multiline\npassword");
editEntryWidget->findChild("urlEdit")->setText("multiline\nurl");
- QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
+ QTest::mouseClick(okButton, Qt::LeftButton);
QCOMPARE(entry->title(), QString("multiline title"));
QCOMPARE(entry->username(), QString("multiline username"));
@@ -849,19 +858,31 @@ void TestGui::testSearch()
QTRY_VERIFY(searchTextEdit->hasFocus());
QTest::keyClick(searchTextEdit, Qt::Key_Down);
QTRY_VERIFY(entryView->hasFocus());
+ auto* searchedEntry = entryView->currentEntry();
// Restore focus and search text selection
QTest::keyClick(m_mainWindow.data(), Qt::Key_F, Qt::ControlModifier);
QTRY_COMPARE(searchTextEdit->selectedText(), QString("someTHING"));
+ QTRY_VERIFY(searchTextEdit->hasFocus());
+
+ searchedEntry->setPassword("password");
+ QClipboard* clipboard = QApplication::clipboard();
+
+ // Attempt password copy with selected test (should fail)
+ QTest::keyClick(searchTextEdit, Qt::Key_C, Qt::ControlModifier);
+ QVERIFY(clipboard->text() != searchedEntry->password());
+ // Deselect text and confirm password copies
+ QTest::mouseClick(searchTextEdit, Qt::LeftButton);
+ QTRY_VERIFY(searchTextEdit->selectedText().isEmpty());
+ QTRY_VERIFY(searchTextEdit->hasFocus());
+ QTest::keyClick(searchTextEdit, Qt::Key_C, Qt::ControlModifier);
+ QCOMPARE(searchedEntry->password(), clipboard->text());
// Ensure Down focuses on entry view when search text is selected
QTest::keyClick(searchTextEdit, Qt::Key_Down);
QTRY_VERIFY(entryView->hasFocus());
- QCOMPARE(entryView->selectionModel()->currentIndex().row(), 0);
- // Test that password copies (entry has focus)
- QClipboard* clipboard = QApplication::clipboard();
+ QCOMPARE(entryView->currentEntry(), searchedEntry);
+ // Test that password copies with entry focused
QTest::keyClick(entryView, Qt::Key_C, Qt::ControlModifier);
- QModelIndex searchedItem = entryView->model()->index(0, 1);
- Entry* searchedEntry = entryView->entryFromIndex(searchedItem);
- QTRY_COMPARE(searchedEntry->password(), clipboard->text());
+ QCOMPARE(searchedEntry->password(), clipboard->text());
// Refocus back to search edit
QTest::mouseClick(searchTextEdit, Qt::LeftButton);
QTRY_VERIFY(searchTextEdit->hasFocus());
diff --git a/utils/keepassxc-snap-helper.sh b/utils/keepassxc-snap-helper.sh
index 4b2ce94d6..206accaf1 100755
--- a/utils/keepassxc-snap-helper.sh
+++ b/utils/keepassxc-snap-helper.sh
@@ -92,6 +92,11 @@ setupVivaldi() {
INSTALL_DIR="${BASE_DIR}/.config/vivaldi/NativeMessagingHosts"
}
+setupBrave() {
+ buildJson
+ INSTALL_DIR="${BASE_DIR}/.config/BraveSoftware/Brave-Browser/NativeMessagingHosts"
+}
+
setupTorBrowser() {
buildJson "firefox"
INSTALL_DIR="${BASE_DIR}/.tor-browser/app/Browser/TorBrowser/Data/Browser/.mozilla/native-messaging-hosts"
@@ -109,9 +114,10 @@ BROWSER=$(whiptail \
"2" "Chrome" \
"3" "Chromium" \
"4" "Vivaldi" \
- "5" "Tor Browser" \
+ "5" "Brave" \
+ "6" "Tor Browser" \
3>&1 1>&2 2>&3)
-
+
clear
exitstatus=$?
@@ -122,16 +128,17 @@ if [ $exitstatus = 0 ]; then
2) setupChrome ;;
3) setupChromium ;;
4) setupVivaldi ;;
- 5) setupTorBrowser ;;
+ 5) setupBrave ;;
+ 6) setupTorBrowser ;;
esac
# Install the JSON file
cd ~
mkdir -p "$INSTALL_DIR"
echo "$JSON_OUT" > ${INSTALL_DIR}/${INSTALL_FILE}
-
+
$DEBUG && echo "Installed to: ${INSTALL_DIR}/${INSTALL_FILE}"
-
+
whiptail \
--title "Installation Complete" \
--msgbox "You will need to restart your browser in order to connect to KeePassXC" \
@@ -139,4 +146,3 @@ if [ $exitstatus = 0 ]; then
else
whiptail --title "Installation Canceled" --msgbox "No changes were made to your system" 8 50
fi
-