Merge branch 'release/2.6.1' into develop

This commit is contained in:
Jonathan White 2020-07-22 12:10:05 -04:00
commit 71b05dbcf4
No known key found for this signature in database
GPG Key ID: 440FC65F2E0C6E01
28 changed files with 466 additions and 82 deletions

View File

@ -24,18 +24,20 @@ set(DOC_DIR ${CMAKE_CURRENT_SOURCE_DIR})
set(OUT_DIR ${CMAKE_CURRENT_BINARY_DIR})
# Build html documentation on all platforms
file(GLOB html_depends ${DOC_DIR}/topics/* ${DOC_DIR}/styles/* ${DOC_DIR}/images/*)
add_custom_command(OUTPUT KeePassXC_GettingStarted.html
COMMAND ${ASCIIDOCTOR_EXE} -D ${OUT_DIR} -o KeePassXC_GettingStarted.html ${DOC_DIR}/GettingStarted.adoc
DEPENDS ${DOC_DIR}/topics/* ${DOC_DIR}/styles/* ${DOC_DIR}/images/* ${DOC_DIR}/GettingStarted.adoc
DEPENDS ${html_depends} ${DOC_DIR}/GettingStarted.adoc
VERBATIM)
add_custom_command(OUTPUT KeePassXC_UserGuide.html
COMMAND ${ASCIIDOCTOR_EXE} -D ${OUT_DIR} -o KeePassXC_UserGuide.html ${DOC_DIR}/UserGuide.adoc
DEPENDS ${DOC_DIR}/topics/* ${DOC_DIR}/styles/* ${DOC_DIR}/images/* ${DOC_DIR}/UserGuide.adoc
DEPENDS ${html_depends} ${DOC_DIR}/UserGuide.adoc
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
VERBATIM)
file(GLOB styles_depends ${DOC_DIR}/styles/*)
add_custom_command(OUTPUT KeePassXC_KeyboardShortcuts.html
COMMAND ${ASCIIDOCTOR_EXE} -D ${OUT_DIR} -o KeePassXC_KeyboardShortcuts.html ${DOC_DIR}/topics/KeyboardShortcuts.adoc
DEPENDS ${DOC_DIR}/topics/KeyboardShortcuts.adoc ${DOC_DIR}/styles/*
DEPENDS ${DOC_DIR}/topics/KeyboardShortcuts.adoc ${styles_depends}
VERBATIM)
add_custom_target(docs ALL DEPENDS KeePassXC_GettingStarted.html KeePassXC_UserGuide.html KeePassXC_KeyboardShortcuts.html)
@ -50,11 +52,11 @@ install(FILES
if(APPLE OR UNIX)
add_custom_command(OUTPUT keepassxc.1
COMMAND ${ASCIIDOCTOR_EXE} -D ${OUT_DIR} -b manpage ${DOC_DIR}/man/keepassxc.1.adoc
DEPENDS ${DOC_DIR}/man/*
DEPENDS ${DOC_DIR}/man/keepassxc.1.adoc
VERBATIM)
add_custom_command(OUTPUT keepassxc-cli.1
COMMAND ${ASCIIDOCTOR_EXE} -D ${OUT_DIR} -b manpage ${DOC_DIR}/man/keepassxc-cli.1.adoc
DEPENDS ${DOC_DIR}/man/*
DEPENDS ${DOC_DIR}/man/keepassxc-cli.1.adoc
VERBATIM)
add_custom_target(manpages ALL DEPENDS keepassxc.1 keepassxc-cli.1)

View File

@ -1,10 +1,28 @@
// Copyright (C) 2017 Manolis Agkopian <m.agkopian@gmail.com>
// Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 or (at your option)
// version 3 of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
= keepassxc-cli(1)
:docdate: 2020-07-05
:docdate: 2020-07-10
:doctype: manpage
:revnumber: 2.6.0
:mansource: KeePassXC {revnumber}
:manmanual: General Commands Manual
== NAME
keepassxc-cli - command line interface for the KeePassXC password manager.
keepassxc-cli - command line interface for the KeePassXC password manager
== SYNOPSIS
*keepassxc-cli* _command_ [_options_]
@ -16,21 +34,21 @@ It provides the ability to query and modify the entries of a KeePass database, d
== COMMANDS
*add* [_options_] <__database__> <__entry__>::
Adds a new entry to a database.
A password can be generated (_-g_ option), or a prompt can be displayed to input the password (_-p_ option).
The same password generation options as documented for the generate command can be used when the _-g_ option is set.
A password can be generated (*-g* option), or a prompt can be displayed to input the password (*-p* option).
The same password generation options as documented for the generate command can be used when the *-g* option is set.
*analyze* [_options_] <__database__>::
Analyzes passwords in a database for weaknesses.
*clip* [_options_] <__database__> <__entry__> [_timeout_]::
Copies an attribute or the current TOTP (if the _-t_ option is specified) of a database entry to the clipboard.
If no attribute name is specified using the _-a_ option, the password is copied.
Copies an attribute or the current TOTP (if the *-t* option is specified) of a database entry to the clipboard.
If no attribute name is specified using the *-a* option, the password is copied.
If multiple entries with the same name exist in different groups, only the attribute for the first one is copied.
For copying the attribute of an entry in a specific group, the group path to the entry should be specified as well, instead of just the name.
Optionally, a timeout in seconds can be specified to automatically clear the clipboard.
*close*::
In interactive mode, closes the currently opened database (see _open_).
In interactive mode, closes the currently opened database (see *open*).
*db-create* [_options_] <__database__>::
Creates a new database with a password and/or a key file.
@ -45,8 +63,8 @@ It provides the ability to query and modify the entries of a KeePass database, d
*edit* [_options_] <__database__> <__entry__>::
Edits a database entry.
A password can be generated (_-g_ option), or a prompt can be displayed to input the password (_-p_ option).
The same password generation options as documented for the generate command can be used when the _-g_ option is set.
A password can be generated (*-g* option), or a prompt can be displayed to input the password (*-p* option).
The same password generation options as documented for the generate command can be used when the *-g* option is set.
*estimate* [_options_] [_password_]::
Estimates the entropy of a password.
@ -54,7 +72,7 @@ It provides the ability to query and modify the entries of a KeePass database, d
*exit*::
Exits interactive mode.
Synonymous with _quit_.
Synonymous with *quit*.
*export* [_options_] <__database__>::
Exports the content of a database to standard output in the specified format (defaults to XML).
@ -78,7 +96,7 @@ It provides the ability to query and modify the entries of a KeePass database, d
*merge* [_options_] <__database1__> <__database2__>::
Merges two databases together.
The first database file is going to be replaced by the result of the merge, for that reason it is advisable to keep a backup of the two database files before attempting a merge.
In the case that both databases make use of the same credentials, the _--same-credentials_ or _-s_ option can be used.
In the case that both databases make use of the same credentials, the *--same-credentials* or *-s* option can be used.
*mkdir* [_options_] <__database__> <__group__>::
Adds a new group to a database.
@ -88,11 +106,11 @@ It provides the ability to query and modify the entries of a KeePass database, d
*open* [_options_] <__database__>::
Opens the given database in a shell-style interactive mode.
This is useful for performing multiple operations on a single database (e.g. _ls_ followed by _show_).
This is useful for performing multiple operations on a single database (e.g. *ls* followed by *show*).
*quit*::
Exits interactive mode.
Synonymous with _exit_.
Synonymous with *exit*.
*rm* [_options_] <__database__> <__entry__>::
Removes an entry from a database.
@ -107,7 +125,7 @@ It provides the ability to query and modify the entries of a KeePass database, d
*show* [_options_] <__database__> <__entry__>::
Shows the title, username, password, URL and notes of a database entry.
Can also show the current TOTP.
Regarding the occurrence of multiple entries with the same name in different groups, everything stated in the _clip_ command section also applies here.
Regarding the occurrence of multiple entries with the same name in different groups, everything stated in the *clip* command section also applies here.
== OPTIONS
=== General options
@ -151,7 +169,7 @@ It provides the ability to query and modify the entries of a KeePass database, d
Uses the same credentials for unlocking both databases.
=== Add and edit options
The same password generation options as documented for the generate command can be used with those 2 commands when the -g option is set.
The same password generation options as documented for the generate command can be used with those 2 commands when the *-g* option is set.
*-u*, *--username* <__username__>::
Specifies the username of the entry.
@ -183,7 +201,7 @@ The same password generation options as documented for the generate command can
*-a*, *--attribute*::
Copies the specified attribute to the clipboard.
If no attribute is specified, the password attribute is the default.
For example, "_-a_ username" would copy the username to the clipboard.
For example, "*-a* *username*" would copy the username to the clipboard.
[Default: password]
*-t*, *--totp*::
@ -204,7 +222,7 @@ The same password generation options as documented for the generate command can
*-a*, *--attributes* <__attribute__>...::
Shows the named attributes.
This option can be specified more than once, with each attribute shown one-per-line in the given order.
If no attributes are specified and _-t_ is not specified, a summary of the default attributes is given.
If no attributes are specified and *-t* is not specified, a summary of the default attributes is given.
Protected attributes will be displayed in clear text if specified explicitly by this option.
*-s*, *--show-protected*::
@ -274,9 +292,11 @@ The same password generation options as documented for the generate command can
Include characters from every selected group.
[Default: Disabled]
== REPORTING BUGS
Bugs and feature requests can be reported on GitHub at https://github.com/keepassxreboot/keepassxc/issues.
include::section-notes.adoc[]
== AUTHOR
This manual page was originally written by Manolis Agkopian <m.agkopian@gmail.com>,
and is maintained by the KeePassXC Team <team@keepassxc.org>.
This manual page was originally written by Manolis Agkopian <m.agkopian@gmail.com>.
include::section-reporting-bugs.adoc[]
include::section-copyright.adoc[]

View File

@ -1,10 +1,28 @@
// Copyright (C) 2019 Janek Bevendorff <janek@jbev.net>
// Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 or (at your option)
// version 3 of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
= keepassxc(1)
:docdate: 2020-07-05
:docdate: 2020-07-10
:doctype: manpage
:revnumber: 2.6.0
:mansource: KeePassXC {revnumber}
:manmanual: General Commands Manual
== NAME
keepassxc - password manager
keepassxc - a modern open-source password manager
== SYNOPSIS
*keepassxc* [_options_] [_filename(s)_]
@ -23,19 +41,25 @@ Your wallet works offline and requires no Internet connection.
Displays version information.
*--config* <__config__>::
Path to a custom config file
Path to a custom config file.
*--keyfile* <__keyfile__>::
Key file of the database
Key file of the database.
*--pw-stdin*::
Read password of the database from stdin
Read password of the database from stdin.
*--pw*, *--parent-window* <__handle__>::
Parent window handle
Parent window handle.
*--debug-info*::
Displays debugging information.
include::section-notes.adoc[]
== AUTHOR
This manual page is maintained by the KeePassXC Team <team@keepassxc.org>.
This manual page was originally written by Janek Bevendorff <janek@jbev.net>.
include::section-reporting-bugs.adoc[]
include::section-copyright.adoc[]

View File

@ -0,0 +1,19 @@
// Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 or (at your option)
// version 3 of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
== COPYRIGHT
Copyright \(C) 2016-2020 KeePassXC Team <team@keepassxc.org>
*KeePassXC* code is licensed under GPL-2 or GPL-3.

View File

@ -0,0 +1,27 @@
// Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 or (at your option)
// version 3 of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
== NOTES
*Project homepage*::
https://keepassxc.org
*QuickStart Guide*::
https://keepassxc.org/docs/KeePassXC_GettingStarted.html
*User Guide*::
https://keepassxc.org/docs/KeePassXC_UserGuide.html
*Git repository*::
https://github.com/keepassxreboot/keepassxc.git

View File

@ -0,0 +1,17 @@
// Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 or (at your option)
// version 3 of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
== REPORTING BUGS
Bugs and feature requests can be reported on GitHub at https://github.com/keepassxreboot/keepassxc/issues.

View File

@ -28,23 +28,23 @@
<screenshots>
<screenshot type="default">
<image>https://keepassxc.org/images/screenshots/linux/screen_001.png</image>
<image>https://keepassxc.org/images/screenshots/thumbs/welcome_screen.png</image>
<caption>Create, Import or Open Databases</caption>
</screenshot>
<screenshot>
<image>https://keepassxc.org/images/screenshots/linux/screen_002.png</image>
<image>https://keepassxc.org/images/screenshots/thumbs/database_view.png</image>
<caption>Organize with Groups and Entries</caption>
</screenshot>
<screenshot>
<image>https://keepassxc.org/images/screenshots/linux/screen_003.png</image>
<image>https://keepassxc.org/images/screenshots/thumbs/edit_entry.png</image>
<caption>Database Entry</caption>
</screenshot>
<screenshot>
<image>https://keepassxc.org/images/screenshots/linux/screen_004.png</image>
<image>https://keepassxc.org/images/screenshots/thumbs/edit_entry_icons.png</image>
<caption>Icon Selection for Entry</caption>
</screenshot>
<screenshot>
<image>https://keepassxc.org/images/screenshots/linux/screen_006.png</image>
<image>https://keepassxc.org/images/screenshots/thumbs/password_generator_advanced.png</image>
<caption>Password Generator</caption>
</screenshot>
</screenshots>
@ -614,4 +614,5 @@
</description>
</release>
</releases>
<content_rating type="oars-1.0" />
</component>

View File

@ -1,13 +1,39 @@
[Desktop Entry]
Name=KeePassXC
GenericName=Password Manager
GenericName[ar]=مدير كلمات المرور
GenericName[bg]=Мениджър на пароли
GenericName[ca]=Gestor de contrasenyes
GenericName[cs]=Aplikace pro správu hesel
GenericName[da]=Adgangskodehåndtering
GenericName[de]=Passwortverwaltung
GenericName[es]=Gestor de contraseñas
GenericName[et]=Paroolihaldur
GenericName[fi]=Salasanamanageri
GenericName[fr]=Gestionnaire de mot de passe
GenericName[hu]=Jelszókezelő
GenericName[id]=Pengelola Sandi
GenericName[it]=Gestione password
GenericName[ja]=
GenericName[ko]=
GenericName[lt]=Slaptažodžių tvarkytuvė
GenericName[nb]=Passordhåndterer
GenericName[nl]=Wachtwoordbeheer
GenericName[pl]=Menedżer haseł
GenericName[pt_BR]=Gerenciador de Senhas
GenericName[pt]=Gestor de palavras-passe
GenericName[ro]=Manager de parole
GenericName[ru]=менеджер паролей
GenericName[sk]=Správca hesiel
GenericName[sv]=Lösenordshanterare
GenericName[th]=
GenericName[tr]=Parola yöneticisi
GenericName[uk]=Розпорядник паролів
GenericName[zh_CN]=
GenericName[zh_TW]=
Comment=Community-driven port of the Windows application KeePass Password Safe
Comment[da]=Fællesskabsdrevet port af Windows-programmet KeePass Password Safe
Comment[et]=Kogukonna arendatav port Windowsi programmist KeePass Password Safe
Exec=keepassxc %f
TryExec=keepassxc
Icon=keepassxc

View File

@ -269,7 +269,7 @@ void AutoType::executeAutoTypeActions(const Entry* entry, QWidget* hideWindow, c
/**
* Single Autotype entry-point function
* Perfom autotype sequence in the active window
* Look up the Auto-Type sequence for the given entry then perfom Auto-Type in the active window
*/
void AutoType::performAutoType(const Entry* entry, QWidget* hideWindow)
{
@ -285,6 +285,19 @@ void AutoType::performAutoType(const Entry* entry, QWidget* hideWindow)
executeAutoTypeActions(entry, hideWindow, sequences.first());
}
/**
* Extra Autotype entry-point function
* Perfom Auto-Type of the directly specified sequence in the active window
*/
void AutoType::performAutoTypeWithSequence(const Entry* entry, const QString& sequence, QWidget* hideWindow)
{
if (!m_plugin) {
return;
}
executeAutoTypeActions(entry, hideWindow, sequence);
}
void AutoType::startGlobalAutoType()
{
m_windowForGlobal = m_plugin->activeWindow();

View File

@ -48,6 +48,7 @@ public:
static bool checkHighDelay(const QString& string);
static bool verifyAutoTypeSyntax(const QString& sequence);
void performAutoType(const Entry* entry, QWidget* hideWindow = nullptr);
void performAutoTypeWithSequence(const Entry* entry, const QString& sequence, QWidget* hideWindow = nullptr);
inline bool isAvailable()
{

View File

@ -413,7 +413,7 @@ QJsonArray BrowserService::findMatchingEntries(const QString& dbid,
}
// Sort results
pwEntries = sortEntries(pwEntries, host, submitUrl);
pwEntries = sortEntries(pwEntries, host, submitUrl, url);
// Fill the list
QJsonArray result;
@ -698,7 +698,10 @@ void BrowserService::convertAttributesToCustomData(QSharedPointer<Database> db)
}
}
QList<Entry*> BrowserService::sortEntries(QList<Entry*>& pwEntries, const QString& host, const QString& entryUrl)
QList<Entry*> BrowserService::sortEntries(QList<Entry*>& pwEntries,
const QString& host,
const QString& entryUrl,
const QString& fullUrl)
{
QUrl url(entryUrl);
if (url.scheme().isEmpty()) {
@ -712,7 +715,7 @@ QList<Entry*> BrowserService::sortEntries(QList<Entry*>& pwEntries, const QStrin
// Build map of prioritized entries
QMultiMap<int, Entry*> priorities;
for (auto* entry : pwEntries) {
priorities.insert(sortPriority(entry, host, submitUrl, baseSubmitUrl), entry);
priorities.insert(sortPriority(entry, host, submitUrl, baseSubmitUrl, fullUrl), entry);
}
QList<Entry*> results;
@ -895,7 +898,8 @@ Group* BrowserService::getDefaultEntryGroup(const QSharedPointer<Database>& sele
int BrowserService::sortPriority(const Entry* entry,
const QString& host,
const QString& submitUrl,
const QString& baseSubmitUrl) const
const QString& baseSubmitUrl,
const QString& fullUrl) const
{
QUrl url(entry->url());
if (url.scheme().isEmpty()) {
@ -914,9 +918,12 @@ int BrowserService::sortPriority(const Entry* entry,
if (!url.host().contains(".") && url.host() != "localhost") {
return 0;
}
if (submitUrl == entryURL) {
if (fullUrl == entryURL) {
return 100;
}
if (submitUrl == entryURL) {
return 95;
}
if (submitUrl.startsWith(entryURL) && entryURL != host && baseSubmitUrl != entryURL) {
return 90;
}
@ -1025,7 +1032,17 @@ bool BrowserService::handleURL(const QString& entryUrl, const QString& url, cons
// Match the subdomains with the limited wildcard
if (siteQUrl.host().endsWith(entryQUrl.host())) {
return true;
if (!browserSettings()->bestMatchOnly()) {
return true;
}
// Match the exact subdomain and path, or start of the path when entry's path is longer than plain "/"
if (siteQUrl.host() == entryQUrl.host()) {
if (siteQUrl.path() == entryQUrl.path()
|| (entryQUrl.path().size() > 1 && siteQUrl.path().startsWith(entryQUrl.path()))) {
return true;
}
}
}
return false;

View File

@ -119,7 +119,8 @@ private:
QList<Entry*> searchEntries(const QSharedPointer<Database>& db, const QString& url, const QString& submitUrl);
QList<Entry*> searchEntries(const QString& url, const QString& submitUrl, const StringPairList& keyList);
QList<Entry*> sortEntries(QList<Entry*>& pwEntries, const QString& host, const QString& submitUrl);
QList<Entry*>
sortEntries(QList<Entry*>& pwEntries, const QString& host, const QString& submitUrl, const QString& fullUrl);
QList<Entry*> confirmEntries(QList<Entry*>& pwEntriesToConfirm,
const QString& url,
const QString& host,
@ -130,8 +131,11 @@ private:
QJsonArray getChildrenFromGroup(Group* group);
Access checkAccess(const Entry* entry, const QString& host, const QString& submitHost, const QString& realm);
Group* getDefaultEntryGroup(const QSharedPointer<Database>& selectedDb = {});
int
sortPriority(const Entry* entry, const QString& host, const QString& submitUrl, const QString& baseSubmitUrl) const;
int sortPriority(const Entry* entry,
const QString& host,
const QString& submitUrl,
const QString& baseSubmitUrl,
const QString& fullUrl) const;
bool schemeFound(const QString& url);
bool removeFirstDomain(QString& hostname);
bool handleURL(const QString& entryUrl, const QString& url, const QString& submitUrl);

View File

@ -27,7 +27,7 @@
Info::Info()
{
name = QString("db-show");
name = QString("db-info");
description = QObject::tr("Show a database's information.");
}

View File

@ -481,6 +481,8 @@ void Entry::updateTotp()
m_attributes->value(Totp::ATTRIBUTE_SEED));
} else if (m_attributes->contains(Totp::ATTRIBUTE_OTP)) {
m_data.totpSettings = Totp::parseSettings(m_attributes->value(Totp::ATTRIBUTE_OTP));
} else {
m_data.totpSettings.reset();
}
}

View File

@ -51,6 +51,13 @@ ScreenLockListenerDBus::ScreenLockListenerDBus(QWidget* parent)
this, // receiver
SLOT(gnomeSessionStatusChanged(uint)));
sessionBus.connect("org.xfce.ScreenSaver", // service
"/org/xfce/ScreenSaver", // path
"org.xfce.ScreenSaver", // interface
"ActiveChanged", // signal name
this, // receiver
SLOT(freedesktopScreenSaver(bool)));
systemBus.connect("org.freedesktop.login1", // service
"/org/freedesktop/login1", // path
"org.freedesktop.login1.Manager", // interface

View File

@ -331,11 +331,15 @@ namespace Tools
#if defined(Q_OS_WIN)
QRegularExpression varRe("\\%([A-Za-z][A-Za-z0-9_]*)\\%");
QString homeEnv = "USERPROFILE";
#else
QRegularExpression varRe("\\$([A-Za-z][A-Za-z0-9_]*)");
subbed.replace("~", environment.value("HOME"));
QString homeEnv = "HOME";
#endif
if (subbed.startsWith("~/") || subbed.startsWith("~\\"))
subbed.replace(0, 1, environment.value(homeEnv));
QRegularExpressionMatch match;
do {

View File

@ -71,6 +71,8 @@ bool Translator::installTranslator(const QStringList& languages, const QString&
QScopedPointer<QTranslator> translator(new QTranslator(qApp));
if (translator->load(locale, "keepassx_", "", path)) {
return QCoreApplication::installTranslator(translator.take());
} else if (translator->load(locale, "keepassx_", "", QLibraryInfo::location(QLibraryInfo::TranslationsPath))) {
return QCoreApplication::installTranslator(translator.take());
}
}

View File

@ -27,6 +27,7 @@
#include "core/Global.h"
#include "core/Resources.h"
#include "core/Translator.h"
#include "gui/MainWindow.h"
#include "gui/osutils/OSUtils.h"
#include "MessageBox.h"
@ -324,7 +325,15 @@ void ApplicationSettingsWidget::saveSettings()
config()->set(Config::AutoTypeEntryURLMatch, m_generalUi->autoTypeEntryURLMatchCheckBox->isChecked());
config()->set(Config::FaviconDownloadTimeout, m_generalUi->faviconTimeoutSpinBox->value());
config()->set(Config::GUI_Language, m_generalUi->languageComboBox->currentData().toString());
auto language = m_generalUi->languageComboBox->currentData().toString();
if (config()->get(Config::GUI_Language) != language) {
QTimer::singleShot(200, [] {
getMainWindow()->restartApp(
tr("You must restart the application to set the new language. Would you like to restart now?"));
});
}
config()->set(Config::GUI_Language, language);
config()->set(Config::GUI_MovableToolbar, m_generalUi->toolbarMovableCheckBox->isChecked());
config()->set(Config::GUI_MonospaceNotes, m_generalUi->monospaceNotesCheckBox->isChecked());

View File

@ -799,6 +799,38 @@ void DatabaseWidget::performAutoType()
}
}
void DatabaseWidget::performAutoTypeUsername()
{
auto currentEntry = currentSelectedEntry();
if (currentEntry) {
autoType()->performAutoTypeWithSequence(currentEntry, QStringLiteral("{USERNAME}"), window());
}
}
void DatabaseWidget::performAutoTypeUsernameEnter()
{
auto currentEntry = currentSelectedEntry();
if (currentEntry) {
autoType()->performAutoTypeWithSequence(currentEntry, QStringLiteral("{USERNAME}{ENTER}"), window());
}
}
void DatabaseWidget::performAutoTypePassword()
{
auto currentEntry = currentSelectedEntry();
if (currentEntry) {
autoType()->performAutoTypeWithSequence(currentEntry, QStringLiteral("{PASSWORD}"), window());
}
}
void DatabaseWidget::performAutoTypePasswordEnter()
{
auto currentEntry = currentSelectedEntry();
if (currentEntry) {
autoType()->performAutoTypeWithSequence(currentEntry, QStringLiteral("{PASSWORD}{ENTER}"), window());
}
}
void DatabaseWidget::openUrl()
{
auto currentEntry = currentSelectedEntry();
@ -1813,7 +1845,7 @@ bool DatabaseWidget::save()
m_blockAutoSave = true;
++m_saveAttempts;
auto focusWidget = qApp->focusWidget();
QPointer<QWidget> focusWidget(qApp->focusWidget());
// TODO: Make this async
// Lock out interactions
@ -1887,7 +1919,7 @@ bool DatabaseWidget::saveAs()
bool ok = false;
if (!newFilePath.isEmpty()) {
auto focusWidget = qApp->focusWidget();
QPointer<QWidget> focusWidget(qApp->focusWidget());
// Lock out interactions
m_entryView->setDisabled(true);

View File

@ -186,6 +186,10 @@ public slots:
void removeFromAgent();
#endif
void performAutoType();
void performAutoTypeUsername();
void performAutoTypeUsernameEnter();
void performAutoTypePassword();
void performAutoTypePasswordEnter();
void openUrl();
void downloadSelectedFavicons();
void downloadAllFavicons();

View File

@ -126,6 +126,7 @@ MainWindow::MainWindow()
m_entryContextMenu->addAction(m_ui->menuEntryTotp->menuAction());
m_entryContextMenu->addSeparator();
m_entryContextMenu->addAction(m_ui->actionEntryAutoType);
m_entryContextMenu->addAction(m_ui->menuEntryAutoTypeWithSequence->menuAction());
m_entryContextMenu->addSeparator();
m_entryContextMenu->addAction(m_ui->actionEntryEdit);
m_entryContextMenu->addAction(m_ui->actionEntryClone);
@ -220,7 +221,12 @@ MainWindow::MainWindow()
m_ui->toolbarSeparator->setVisible(false);
m_showToolbarSeparator = config()->get(Config::GUI_ApplicationTheme).toString() != "classic";
m_ui->actionEntryAutoType->setVisible(autoType()->isAvailable());
bool isAutoTypeAvailable = autoType()->isAvailable();
m_ui->actionEntryAutoType->setVisible(isAutoTypeAvailable);
m_ui->actionEntryAutoTypeUsername->setVisible(isAutoTypeAvailable);
m_ui->actionEntryAutoTypeUsernameEnter->setVisible(isAutoTypeAvailable);
m_ui->actionEntryAutoTypePassword->setVisible(isAutoTypeAvailable);
m_ui->actionEntryAutoTypePasswordEnter->setVisible(isAutoTypeAvailable);
m_inactivityTimer = new InactivityTimer(this);
connect(m_inactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(lockDatabasesAfterInactivity()));
@ -352,6 +358,11 @@ MainWindow::MainWindow()
m_ui->actionEntryEdit->setIcon(resources()->icon("entry-edit"));
m_ui->actionEntryDelete->setIcon(resources()->icon("entry-delete"));
m_ui->actionEntryAutoType->setIcon(resources()->icon("auto-type"));
m_ui->menuEntryAutoTypeWithSequence->setIcon(resources()->icon("auto-type"));
m_ui->actionEntryAutoTypeUsername->setIcon(resources()->icon("auto-type"));
m_ui->actionEntryAutoTypeUsernameEnter->setIcon(resources()->icon("auto-type"));
m_ui->actionEntryAutoTypePassword->setIcon(resources()->icon("auto-type"));
m_ui->actionEntryAutoTypePasswordEnter->setIcon(resources()->icon("auto-type"));
m_ui->actionEntryMoveUp->setIcon(resources()->icon("move-up"));
m_ui->actionEntryMoveDown->setIcon(resources()->icon("move-down"));
m_ui->actionEntryCopyUsername->setIcon(resources()->icon("username-copy"));
@ -446,6 +457,14 @@ MainWindow::MainWindow()
m_actionMultiplexer.connect(m_ui->actionEntryCopyURL, SIGNAL(triggered()), SLOT(copyURL()));
m_actionMultiplexer.connect(m_ui->actionEntryCopyNotes, SIGNAL(triggered()), SLOT(copyNotes()));
m_actionMultiplexer.connect(m_ui->actionEntryAutoType, SIGNAL(triggered()), SLOT(performAutoType()));
m_actionMultiplexer.connect(
m_ui->actionEntryAutoTypeUsername, SIGNAL(triggered()), SLOT(performAutoTypeUsername()));
m_actionMultiplexer.connect(
m_ui->actionEntryAutoTypeUsernameEnter, SIGNAL(triggered()), SLOT(performAutoTypeUsernameEnter()));
m_actionMultiplexer.connect(
m_ui->actionEntryAutoTypePassword, SIGNAL(triggered()), SLOT(performAutoTypePassword()));
m_actionMultiplexer.connect(
m_ui->actionEntryAutoTypePasswordEnter, SIGNAL(triggered()), SLOT(performAutoTypePasswordEnter()));
m_actionMultiplexer.connect(m_ui->actionEntryOpenUrl, SIGNAL(triggered()), SLOT(openUrl()));
m_actionMultiplexer.connect(m_ui->actionEntryDownloadIcon, SIGNAL(triggered()), SLOT(downloadSelectedFavicons()));
#ifdef WITH_XC_SSHAGENT
@ -711,6 +730,13 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
m_ui->menuEntryCopyAttribute->setEnabled(singleEntrySelected);
m_ui->menuEntryTotp->setEnabled(singleEntrySelected);
m_ui->actionEntryAutoType->setEnabled(singleEntrySelected);
m_ui->menuEntryAutoTypeWithSequence->setEnabled(singleEntrySelected);
m_ui->actionEntryAutoTypeUsername->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUsername());
m_ui->actionEntryAutoTypeUsernameEnter->setEnabled(singleEntrySelected
&& dbWidget->currentEntryHasUsername());
m_ui->actionEntryAutoTypePassword->setEnabled(singleEntrySelected && dbWidget->currentEntryHasPassword());
m_ui->actionEntryAutoTypePasswordEnter->setEnabled(singleEntrySelected
&& dbWidget->currentEntryHasPassword());
m_ui->actionEntryOpenUrl->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUrl());
m_ui->actionEntryTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp());
m_ui->actionEntryCopyTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp());
@ -761,6 +787,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
m_ui->actionEntryCopyURL,
m_ui->actionEntryOpenUrl,
m_ui->actionEntryAutoType,
m_ui->menuEntryAutoTypeWithSequence->menuAction(),
m_ui->actionEntryDownloadIcon,
m_ui->actionEntryCopyNotes,
m_ui->actionEntryCopyTitle,
@ -1679,17 +1706,20 @@ void MainWindow::initViewMenu()
}
}
connect(themeActions, &QActionGroup::triggered, this, [this](QAction* action) {
if (action->data() != config()->get(Config::GUI_ApplicationTheme)) {
config()->set(Config::GUI_ApplicationTheme, action->data());
connect(themeActions, &QActionGroup::triggered, this, [this, theme](QAction* action) {
config()->set(Config::GUI_ApplicationTheme, action->data());
if (action->data() != theme) {
restartApp(tr("You must restart the application to apply this setting. Would you like to restart now?"));
}
});
m_ui->actionCompactMode->setChecked(config()->get(Config::GUI_CompactMode).toBool());
connect(m_ui->actionCompactMode, &QAction::toggled, this, [this](bool checked) {
bool compact = config()->get(Config::GUI_CompactMode).toBool();
m_ui->actionCompactMode->setChecked(compact);
connect(m_ui->actionCompactMode, &QAction::toggled, this, [this, compact](bool checked) {
config()->set(Config::GUI_CompactMode, checked);
restartApp(tr("You must restart the application to apply this setting. Would you like to restart now?"));
if (checked != compact) {
restartApp(tr("You must restart the application to apply this setting. Would you like to restart now?"));
}
});
m_ui->actionShowToolbar->setChecked(!config()->get(Config::GUI_HideToolbar).toBool());

View File

@ -310,6 +310,18 @@
<addaction name="actionEntryTotpQRCode"/>
<addaction name="actionEntrySetupTotp"/>
</widget>
<widget class="QMenu" name="menuEntryAutoTypeWithSequence">
<property name="enabled">
<bool>false</bool>
</property>
<property name="title">
<string>Perform Auto-Type Sequence</string>
</property>
<addaction name="actionEntryAutoTypeUsername"/>
<addaction name="actionEntryAutoTypeUsernameEnter"/>
<addaction name="actionEntryAutoTypePassword"/>
<addaction name="actionEntryAutoTypePasswordEnter"/>
</widget>
<addaction name="actionEntryNew"/>
<addaction name="actionEntryEdit"/>
<addaction name="actionEntryClone"/>
@ -324,6 +336,7 @@
<addaction name="menuEntryTotp"/>
<addaction name="separator"/>
<addaction name="actionEntryAutoType"/>
<addaction name="menuEntryAutoTypeWithSequence"/>
<addaction name="separator"/>
<addaction name="actionEntryOpenUrl"/>
<addaction name="actionEntryDownloadIcon"/>
@ -680,6 +693,38 @@
<string>Perform &amp;Auto-Type</string>
</property>
</action>
<action name="actionEntryAutoTypeUsername">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>{USERNAME}</string>
</property>
</action>
<action name="actionEntryAutoTypeUsernameEnter">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>{USERNAME}{ENTER}</string>
</property>
</action>
<action name="actionEntryAutoTypePassword">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>{PASSWORD}</string>
</property>
</action>
<action name="actionEntryAutoTypePasswordEnter">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>{PASSWORD}{ENTER}</string>
</property>
</action>
<action name="actionEntryDownloadIcon">
<property name="text">
<string>Download &amp;Favicon</string>

View File

@ -50,21 +50,28 @@ PasswordEdit::PasswordEdit(QWidget* parent)
passwordFont.setLetterSpacing(QFont::PercentageSpacing, 110);
setFont(passwordFont);
// Prevent conflicts with global Mac shortcuts (force Control on all platforms)
#ifdef Q_OS_MAC
auto modifier = Qt::META;
#else
auto modifier = Qt::CTRL;
#endif
m_toggleVisibleAction = new QAction(
resources()->icon("password-show-off"),
tr("Toggle Password (%1)").arg(QKeySequence(Qt::CTRL + Qt::Key_H).toString(QKeySequence::NativeText)),
tr("Toggle Password (%1)").arg(QKeySequence(modifier + Qt::Key_H).toString(QKeySequence::NativeText)),
nullptr);
m_toggleVisibleAction->setCheckable(true);
m_toggleVisibleAction->setShortcut(Qt::CTRL + Qt::Key_H);
m_toggleVisibleAction->setShortcut(modifier + Qt::Key_H);
m_toggleVisibleAction->setShortcutContext(Qt::WidgetShortcut);
addAction(m_toggleVisibleAction, QLineEdit::TrailingPosition);
connect(m_toggleVisibleAction, &QAction::triggered, this, &PasswordEdit::setShowPassword);
m_passwordGeneratorAction = new QAction(
resources()->icon("password-generator"),
tr("Generate Password (%1)").arg(QKeySequence(Qt::CTRL + Qt::Key_G).toString(QKeySequence::NativeText)),
tr("Generate Password (%1)").arg(QKeySequence(modifier + Qt::Key_G).toString(QKeySequence::NativeText)),
nullptr);
m_passwordGeneratorAction->setShortcut(Qt::CTRL + Qt::Key_G);
m_passwordGeneratorAction->setShortcut(modifier + Qt::Key_G);
m_passwordGeneratorAction->setShortcutContext(Qt::WidgetShortcut);
addAction(m_passwordGeneratorAction, QLineEdit::TrailingPosition);
m_passwordGeneratorAction->setVisible(false);

View File

@ -447,12 +447,8 @@ void SSHAgent::databaseLocked()
if (!removeIdentity(key)) {
emit error(m_error);
}
it = m_addedKeys.erase(it);
} else {
// don't remove it yet
m_addedKeys[key].second = false;
++it;
}
it = m_addedKeys.erase(it);
}
}

View File

@ -113,7 +113,7 @@ QSharedPointer<Totp::Settings> Totp::parseSettings(const QString& rawSettings, c
}
// Bound digits and step
settings->digits = qMax(1u, settings->digits);
settings->digits = qBound(1u, settings->digits, 10u);
settings->step = qBound(1u, settings->step, 60u);
// Detect custom settings, used by setup GUI

View File

@ -38,6 +38,7 @@ void TestBrowser::initTestCase()
{
QVERIFY(Crypto::init());
m_browserService = browserService();
browserSettings()->setBestMatchOnly(false);
}
void TestBrowser::init()
@ -130,6 +131,7 @@ void TestBrowser::testSortPriority()
QString host = "github.com";
QString submitUrl = "https://github.com/session";
QString baseSubmitUrl = "https://github.com";
QString fullUrl = "https://github.com/login";
QScopedPointer<Entry> entry1(new Entry());
QScopedPointer<Entry> entry2(new Entry());
@ -141,6 +143,7 @@ void TestBrowser::testSortPriority()
QScopedPointer<Entry> entry8(new Entry());
QScopedPointer<Entry> entry9(new Entry());
QScopedPointer<Entry> entry10(new Entry());
QScopedPointer<Entry> entry11(new Entry());
entry1->setUrl("https://github.com/login");
entry2->setUrl("https://github.com/login");
@ -152,18 +155,20 @@ void TestBrowser::testSortPriority()
entry8->setUrl("github.com/login");
entry9->setUrl("https://github"); // Invalid URL
entry10->setUrl("github.com");
entry11->setUrl("https://github.com/login"); // Exact match
// The extension uses the submitUrl as default for comparison
auto res1 = m_browserService->sortPriority(entry1.data(), host, "https://github.com/login", baseSubmitUrl);
auto res2 = m_browserService->sortPriority(entry2.data(), host, submitUrl, baseSubmitUrl);
auto res3 = m_browserService->sortPriority(entry3.data(), host, submitUrl, baseSubmitUrl);
auto res4 = m_browserService->sortPriority(entry4.data(), host, submitUrl, baseSubmitUrl);
auto res5 = m_browserService->sortPriority(entry5.data(), host, submitUrl, baseSubmitUrl);
auto res6 = m_browserService->sortPriority(entry6.data(), host, submitUrl, baseSubmitUrl);
auto res7 = m_browserService->sortPriority(entry7.data(), host, submitUrl, baseSubmitUrl);
auto res8 = m_browserService->sortPriority(entry8.data(), host, submitUrl, baseSubmitUrl);
auto res9 = m_browserService->sortPriority(entry9.data(), host, submitUrl, baseSubmitUrl);
auto res10 = m_browserService->sortPriority(entry10.data(), host, submitUrl, baseSubmitUrl);
auto res1 = m_browserService->sortPriority(entry1.data(), host, "https://github.com/login", baseSubmitUrl, fullUrl);
auto res2 = m_browserService->sortPriority(entry2.data(), host, submitUrl, baseSubmitUrl, baseSubmitUrl);
auto res3 = m_browserService->sortPriority(entry3.data(), host, submitUrl, baseSubmitUrl, fullUrl);
auto res4 = m_browserService->sortPriority(entry4.data(), host, submitUrl, baseSubmitUrl, fullUrl);
auto res5 = m_browserService->sortPriority(entry5.data(), host, submitUrl, baseSubmitUrl, fullUrl);
auto res6 = m_browserService->sortPriority(entry6.data(), host, submitUrl, baseSubmitUrl, fullUrl);
auto res7 = m_browserService->sortPriority(entry7.data(), host, submitUrl, baseSubmitUrl, fullUrl);
auto res8 = m_browserService->sortPriority(entry8.data(), host, submitUrl, baseSubmitUrl, fullUrl);
auto res9 = m_browserService->sortPriority(entry9.data(), host, submitUrl, baseSubmitUrl, fullUrl);
auto res10 = m_browserService->sortPriority(entry10.data(), host, submitUrl, baseSubmitUrl, fullUrl);
auto res11 = m_browserService->sortPriority(entry11.data(), host, submitUrl, baseSubmitUrl, fullUrl);
QCOMPARE(res1, 100);
QCOMPARE(res2, 40);
@ -175,6 +180,7 @@ void TestBrowser::testSortPriority()
QCOMPARE(res8, 0);
QCOMPARE(res9, 0);
QCOMPARE(res10, 0);
QCOMPARE(res11, 100);
}
void TestBrowser::testSearchEntries()
@ -382,8 +388,8 @@ void TestBrowser::testSortEntries()
auto entries = createEntries(urls, root);
browserSettings()->setBestMatchOnly(false);
auto result =
m_browserService->sortEntries(entries, "github.com", "https://github.com/session"); // entries, host, submitUrl
auto result = m_browserService->sortEntries(
entries, "github.com", "https://github.com/session", "https://github.com"); // entries, host, submitUrl
QCOMPARE(result.size(), 10);
QCOMPARE(result[0]->username(), QString("User 2"));
QCOMPARE(result[0]->url(), QString("https://github.com/"));
@ -393,6 +399,15 @@ void TestBrowser::testSortEntries()
QCOMPARE(result[2]->url(), QString("https://github.com/login"));
QCOMPARE(result[3]->username(), QString("User 3"));
QCOMPARE(result[3]->url(), QString("github.com/login"));
// Test with a perfect match. That should be first in the list.
result = m_browserService->sortEntries(
entries, "github.com", "https://github.com/session", "https://github.com/login_page");
QCOMPARE(result.size(), 10);
QCOMPARE(result[0]->username(), QString("User 0"));
QCOMPARE(result[0]->url(), QString("https://github.com/login_page"));
QCOMPARE(result[1]->username(), QString("User 2"));
QCOMPARE(result[1]->url(), QString("https://github.com/"));
}
QList<Entry*> TestBrowser::createEntries(QStringList& urls, Group* root) const
@ -429,3 +444,58 @@ void TestBrowser::testValidURLs()
QCOMPARE(Tools::checkUrlValid(i.key()), i.value());
}
}
void TestBrowser::testBestMatchingCredentials()
{
auto db = QSharedPointer<Database>::create();
auto* root = db->rootGroup();
// Test with simple URL entries
QStringList urls = {"https://github.com/loginpage", "https://github.com/justsomepage", "https://github.com/"};
auto entries = createEntries(urls, root);
browserSettings()->setBestMatchOnly(true);
auto result = m_browserService->searchEntries(db, "https://github.com/loginpage", "https://github.com/loginpage");
QCOMPARE(result.size(), 1);
QCOMPARE(result[0]->url(), QString("https://github.com/loginpage"));
result = m_browserService->searchEntries(db, "https://github.com/justsomepage", "https://github.com/justsomepage");
QCOMPARE(result.size(), 1);
QCOMPARE(result[0]->url(), QString("https://github.com/justsomepage"));
result = m_browserService->searchEntries(db, "https://github.com/", "https://github.com/");
m_browserService->sortEntries(entries, "github.com", "https://github.com/", "https://github.com/");
QCOMPARE(result.size(), 1);
QCOMPARE(result[0]->url(), QString("https://github.com/"));
browserSettings()->setBestMatchOnly(false);
result = m_browserService->searchEntries(db, "https://github.com/loginpage", "https://github.com/loginpage");
QCOMPARE(result.size(), 3);
QCOMPARE(result[0]->url(), QString("https://github.com/loginpage"));
// Test with subdomains
QStringList subdomainsUrls = {"https://sub.github.com/loginpage",
"https://sub.github.com/justsomepage",
"https://bus.github.com/justsomepage"};
entries = createEntries(subdomainsUrls, root);
browserSettings()->setBestMatchOnly(true);
result = m_browserService->searchEntries(
db, "https://sub.github.com/justsomepage", "https://sub.github.com/justsomepage");
QCOMPARE(result.size(), 1);
QCOMPARE(result[0]->url(), QString("https://sub.github.com/justsomepage"));
result = m_browserService->searchEntries(db, "https://github.com/justsomepage", "https://github.com/justsomepage");
QCOMPARE(result.size(), 1);
QCOMPARE(result[0]->url(), QString("https://github.com/justsomepage"));
result = m_browserService->searchEntries(db,
"https://sub.github.com/justsomepage?wehavesomeextra=here",
"https://sub.github.com/justsomepage?wehavesomeextra=here");
QCOMPARE(result.size(), 1);
QCOMPARE(result[0]->url(), QString("https://sub.github.com/justsomepage"));
}

View File

@ -47,6 +47,7 @@ private slots:
void testSubdomainsAndPaths();
void testSortEntries();
void testValidURLs();
void testBestMatchingCredentials();
private:
QList<Entry*> createEntries(QStringList& urls, Group* root) const;

View File

@ -72,10 +72,14 @@ void TestTools::testEnvSubstitute()
#if defined(Q_OS_WIN)
environment.insert("HOMEDRIVE", "C:");
environment.insert("HOMEPATH", "\\Users\\User");
environment.insert("USERPROFILE", "C:\\Users\\User");
QCOMPARE(Tools::envSubstitute("%HOMEDRIVE%%HOMEPATH%\\.ssh\\id_rsa", environment),
QString("C:\\Users\\User\\.ssh\\id_rsa"));
QCOMPARE(Tools::envSubstitute("start%EMPTY%%EMPTY%%%HOMEDRIVE%%end", environment), QString("start%C:%end"));
QCOMPARE(Tools::envSubstitute("%USERPROFILE%\\.ssh\\id_rsa", environment),
QString("C:\\Users\\User\\.ssh\\id_rsa"));
QCOMPARE(Tools::envSubstitute("~\\.ssh\\id_rsa", environment), QString("C:\\Users\\User\\.ssh\\id_rsa"));
#else
environment.insert("HOME", QString("/home/user"));
environment.insert("USER", QString("user"));