keepassxc/src/gui/DatabaseWidget.cpp

2118 lines
63 KiB
C++
Raw Normal View History

2017-06-09 17:40:36 -04:00
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
2010-08-24 16:26:52 -04:00
*
* 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.
2010-08-24 16:26:52 -04:00
*
* 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.
2010-08-24 16:26:52 -04:00
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
2010-08-24 16:26:52 -04:00
*/
#include "DatabaseWidget.h"
#include <QAction>
2018-03-31 16:01:30 -04:00
#include <QApplication>
#include <QCheckBox>
2018-03-31 16:01:30 -04:00
#include <QDesktopServices>
#include <QFile>
2018-03-31 16:01:30 -04:00
#include <QHBoxLayout>
#include <QHeaderView>
#include <QHostInfo>
#include <QKeyEvent>
#include <QLabel>
2018-03-31 16:01:30 -04:00
#include <QLineEdit>
2015-05-12 12:25:43 -04:00
#include <QProcess>
2018-03-31 16:01:30 -04:00
#include <QSplitter>
#include <QTextEdit>
2010-08-24 16:26:52 -04:00
2012-07-12 16:35:51 -04:00
#include "autotype/AutoType.h"
#include "core/Config.h"
#include "core/Database.h"
2014-05-15 17:48:54 -04:00
#include "core/EntrySearcher.h"
#include "core/FileWatcher.h"
2013-10-29 22:00:02 -04:00
#include "core/Group.h"
#include "core/Merger.h"
#include "core/Metadata.h"
#include "core/Resources.h"
#include "core/Tools.h"
#include "format/KeePass2Reader.h"
#include "gui/Clipboard.h"
#include "gui/CloneDialog.h"
#include "gui/DatabaseOpenDialog.h"
#include "gui/DatabaseOpenWidget.h"
#include "gui/EntryPreviewWidget.h"
#include "gui/FileDialog.h"
#include "gui/KeePass1OpenWidget.h"
#include "gui/MainWindow.h"
#include "gui/MessageBox.h"
#include "gui/OpVaultOpenWidget.h"
2018-03-31 16:01:30 -04:00
#include "gui/TotpDialog.h"
#include "gui/TotpExportSettingsDialog.h"
#include "gui/TotpSetupDialog.h"
#include "gui/dbsettings/DatabaseSettingsDialog.h"
#include "gui/entry/EditEntryWidget.h"
#include "gui/entry/EntryView.h"
#include "gui/group/EditGroupWidget.h"
#include "gui/group/GroupView.h"
#include "gui/reports/ReportsDialog.h"
#include "keeshare/KeeShare.h"
2018-04-04 11:39:26 -04:00
#include "touchid/TouchID.h"
2010-08-24 16:26:52 -04:00
#ifdef WITH_XC_NETWORKING
#include "gui/IconDownloaderDialog.h"
#endif
#ifdef Q_OS_LINUX
#include <sys/vfs.h>
#endif
2017-10-29 11:17:24 -04:00
#ifdef WITH_XC_SSHAGENT
#include "sshagent/SSHAgent.h"
#endif
DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
2010-10-06 13:40:50 -04:00
: QStackedWidget(parent)
, m_db(std::move(db))
, m_mainWidget(new QWidget(this))
, m_mainSplitter(new QSplitter(m_mainWidget))
, m_messageWidget(new MessageWidget(this))
, m_previewView(new EntryPreviewWidget(this))
, m_previewSplitter(new QSplitter(m_mainWidget))
, m_searchingLabel(new QLabel(this))
, m_shareLabel(new QLabel(this))
, m_csvImportWizard(new CsvImportWizard(this))
, m_editEntryWidget(new EditEntryWidget(this))
, m_editGroupWidget(new EditGroupWidget(this))
, m_historyEditEntryWidget(new EditEntryWidget(this))
, m_reportsDialog(new ReportsDialog(this))
, m_databaseSettingDialog(new DatabaseSettingsDialog(this))
, m_databaseOpenWidget(new DatabaseOpenWidget(this))
, m_keepass1OpenWidget(new KeePass1OpenWidget(this))
, m_opVaultOpenWidget(new OpVaultOpenWidget(this))
, m_groupView(new GroupView(m_db.data(), m_mainSplitter))
, m_saveAttempts(0)
2010-08-24 16:26:52 -04:00
{
Q_ASSERT(m_db);
m_messageWidget->setHidden(true);
auto* mainLayout = new QVBoxLayout();
mainLayout->addWidget(m_messageWidget);
auto* hbox = new QHBoxLayout();
mainLayout->addLayout(hbox);
hbox->addWidget(m_mainSplitter);
m_mainWidget->setLayout(mainLayout);
auto* rightHandSideWidget = new QWidget(m_mainSplitter);
auto* vbox = new QVBoxLayout();
vbox->setMargin(0);
vbox->addWidget(m_searchingLabel);
#ifdef WITH_XC_KEESHARE
vbox->addWidget(m_shareLabel);
#endif
vbox->addWidget(m_previewSplitter);
rightHandSideWidget->setLayout(vbox);
m_entryView = new EntryView(rightHandSideWidget);
m_mainSplitter->setChildrenCollapsible(false);
m_mainSplitter->addWidget(m_groupView);
m_mainSplitter->addWidget(rightHandSideWidget);
m_mainSplitter->setStretchFactor(0, 30);
m_mainSplitter->setStretchFactor(1, 70);
m_previewSplitter->setOrientation(Qt::Vertical);
m_previewSplitter->setChildrenCollapsible(true);
m_groupView->setObjectName("groupView");
m_groupView->setContextMenuPolicy(Qt::CustomContextMenu);
2018-03-31 16:01:30 -04:00
connect(m_groupView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(emitGroupContextMenuRequested(QPoint)));
m_entryView->setObjectName("entryView");
m_entryView->setContextMenuPolicy(Qt::CustomContextMenu);
m_entryView->displayGroup(m_db->rootGroup());
2018-03-31 16:01:30 -04:00
connect(m_entryView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(emitEntryContextMenuRequested(QPoint)));
2010-08-24 16:26:52 -04:00
// Add a notification for when we are searching
2020-08-01 11:13:28 -04:00
m_searchingLabel->setObjectName("SearchBanner");
m_searchingLabel->setText(tr("Searching..."));
m_searchingLabel->setAlignment(Qt::AlignCenter);
m_searchingLabel->setVisible(false);
#ifdef WITH_XC_KEESHARE
2020-08-01 11:13:28 -04:00
m_shareLabel->setObjectName("KeeShareBanner");
m_shareLabel->setText(tr("Shared group..."));
m_shareLabel->setAlignment(Qt::AlignCenter);
m_shareLabel->setVisible(false);
#endif
m_previewView->setObjectName("previewWidget");
m_previewView->hide();
m_previewSplitter->addWidget(m_entryView);
m_previewSplitter->addWidget(m_previewView);
m_previewSplitter->setStretchFactor(0, 100);
m_previewSplitter->setStretchFactor(1, 0);
m_previewSplitter->setSizes({1, 1});
m_editEntryWidget->setObjectName("editEntryWidget");
m_editGroupWidget->setObjectName("editGroupWidget");
2017-10-29 10:04:46 -04:00
m_csvImportWizard->setObjectName("csvImportWizard");
m_reportsDialog->setObjectName("reportsDialog");
m_databaseSettingDialog->setObjectName("databaseSettingsDialog");
m_databaseOpenWidget->setObjectName("databaseOpenWidget");
m_keepass1OpenWidget->setObjectName("keepass1OpenWidget");
m_opVaultOpenWidget->setObjectName("opVaultOpenWidget");
addChildWidget(m_mainWidget);
addChildWidget(m_editEntryWidget);
addChildWidget(m_editGroupWidget);
addChildWidget(m_reportsDialog);
addChildWidget(m_databaseSettingDialog);
addChildWidget(m_historyEditEntryWidget);
addChildWidget(m_databaseOpenWidget);
addChildWidget(m_csvImportWizard);
addChildWidget(m_keepass1OpenWidget);
addChildWidget(m_opVaultOpenWidget);
2010-10-06 13:40:50 -04:00
// clang-format off
connect(m_mainSplitter, SIGNAL(splitterMoved(int,int)), SIGNAL(mainSplitterSizesChanged()));
connect(m_previewSplitter, SIGNAL(splitterMoved(int,int)), SIGNAL(previewSplitterSizesChanged()));
connect(this, SIGNAL(currentModeChanged(DatabaseWidget::Mode)), m_previewView, SLOT(setDatabaseMode(DatabaseWidget::Mode)));
connect(m_previewView, SIGNAL(errorOccurred(QString)), SLOT(showErrorMessage(QString)));
connect(m_previewView, SIGNAL(entryUrlActivated(Entry*)), SLOT(openUrlForEntry(Entry*)));
connect(m_entryView, SIGNAL(viewStateChanged()), SIGNAL(entryViewStateChanged()));
connect(m_groupView, SIGNAL(groupSelectionChanged()), SLOT(onGroupChanged()));
connect(m_groupView, SIGNAL(groupSelectionChanged()), SIGNAL(groupChanged()));
connect(m_groupView, &GroupView::groupFocused, this, [this] { m_previewView->setGroup(currentGroup()); });
connect(m_entryView, SIGNAL(entryActivated(Entry*,EntryModel::ModelColumn)),
SLOT(entryActivationSignalReceived(Entry*,EntryModel::ModelColumn)));
connect(m_entryView, SIGNAL(entrySelectionChanged(Entry*)), SLOT(onEntryChanged(Entry*)));
connect(m_editEntryWidget, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
2012-05-15 14:12:05 -04:00
connect(m_editEntryWidget, SIGNAL(historyEntryActivated(Entry*)), SLOT(switchToHistoryView(Entry*)));
connect(m_historyEditEntryWidget, SIGNAL(editFinished(bool)), SLOT(switchBackToEntryEdit()));
connect(m_editGroupWidget, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
connect(m_reportsDialog, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
connect(m_databaseSettingDialog, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
connect(m_databaseOpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool)));
connect(m_keepass1OpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool)));
connect(m_opVaultOpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool)));
connect(m_csvImportWizard, SIGNAL(importFinished(bool)), SLOT(csvImportFinished(bool)));
connect(this, SIGNAL(currentChanged(int)), SLOT(emitCurrentModeChanged()));
// clang-format on
connectDatabaseSignals();
m_blockAutoSave = false;
m_EntrySearcher = new EntrySearcher(false);
m_searchLimitGroup = config()->get(Config::SearchLimitGroup).toBool();
2010-10-06 13:40:50 -04:00
2017-10-29 11:17:24 -04:00
#ifdef WITH_XC_SSHAGENT
if (sshAgent()->isEnabled()) {
connect(this, SIGNAL(databaseLocked()), sshAgent(), SLOT(databaseLocked()));
connect(this, SIGNAL(databaseUnlocked()), sshAgent(), SLOT(databaseUnlocked()));
2017-10-29 11:17:24 -04:00
}
#endif
#ifdef WITH_XC_KEESHARE
// We need to reregister the database to allow exports
// from a newly created database
KeeShare::instance()->connectDatabase(m_db, {});
#endif
if (m_db->isInitialized()) {
switchToMainView();
} else {
switchToOpenDatabase();
}
2010-10-06 13:40:50 -04:00
}
DatabaseWidget::DatabaseWidget(const QString& filePath, QWidget* parent)
: DatabaseWidget(QSharedPointer<Database>::create(filePath), parent)
{
2010-10-06 13:40:50 -04:00
}
DatabaseWidget::~DatabaseWidget()
{
delete m_EntrySearcher;
}
QSharedPointer<Database> DatabaseWidget::database() const
{
return m_db;
}
DatabaseWidget::Mode DatabaseWidget::currentMode() const
{
2015-07-24 12:28:12 -04:00
if (currentWidget() == nullptr) {
return Mode::None;
2018-03-31 16:01:30 -04:00
} else if (currentWidget() == m_mainWidget) {
return Mode::ViewMode;
} else if (currentWidget() == m_databaseOpenWidget || currentWidget() == m_keepass1OpenWidget) {
return Mode::LockedMode;
} else if (currentWidget() == m_csvImportWizard) {
return Mode::ImportMode;
2018-03-31 16:01:30 -04:00
} else {
return Mode::EditMode;
}
}
bool DatabaseWidget::isLocked() const
{
return currentMode() == Mode::LockedMode;
}
bool DatabaseWidget::isSaving() const
{
return m_db->isSaving();
}
bool DatabaseWidget::isSorted() const
{
return m_entryView->isSorted();
}
bool DatabaseWidget::isSearchActive() const
{
return m_entryView->inSearchMode();
}
bool DatabaseWidget::isEntryViewActive() const
{
return currentWidget() == m_mainWidget;
}
bool DatabaseWidget::isEntryEditActive() const
{
return currentWidget() == m_editEntryWidget;
}
bool DatabaseWidget::isGroupEditActive() const
{
return currentWidget() == m_editGroupWidget;
}
bool DatabaseWidget::isEditWidgetModified() const
{
if (currentWidget() == m_editEntryWidget) {
return m_editEntryWidget->isModified();
} else if (currentWidget() == m_editGroupWidget) {
return m_editGroupWidget->isModified();
}
return false;
}
QList<int> DatabaseWidget::mainSplitterSizes() const
{
return m_mainSplitter->sizes();
}
void DatabaseWidget::setMainSplitterSizes(const QList<int>& sizes)
{
m_mainSplitter->setSizes(sizes);
}
QList<int> DatabaseWidget::previewSplitterSizes() const
{
return m_previewSplitter->sizes();
}
void DatabaseWidget::setPreviewSplitterSizes(const QList<int>& sizes)
{
m_previewSplitter->setSizes(sizes);
}
/**
* Get current view state of entry view
*/
QByteArray DatabaseWidget::entryViewState() const
{
return m_entryView->viewState();
}
/**
* Set view state of entry view
*/
bool DatabaseWidget::setEntryViewState(const QByteArray& state) const
{
return m_entryView->setViewState(state);
}
void DatabaseWidget::clearAllWidgets()
{
m_editEntryWidget->clear();
m_historyEditEntryWidget->clear();
m_editGroupWidget->clear();
}
void DatabaseWidget::emitCurrentModeChanged()
{
emit currentModeChanged(currentMode());
}
2011-12-27 10:04:59 -05:00
void DatabaseWidget::createEntry()
{
Q_ASSERT(m_groupView->currentGroup());
if (!m_groupView->currentGroup()) {
return;
}
m_newEntry.reset(new Entry());
2018-01-17 19:16:46 -05:00
if (isSearchActive()) {
2018-01-17 19:16:46 -05:00
m_newEntry->setTitle(getCurrentSearch());
endSearch();
}
2018-03-22 17:56:05 -04:00
m_newEntry->setUuid(QUuid::createUuid());
2012-04-23 14:10:07 -04:00
m_newEntry->setUsername(m_db->metadata()->defaultUserName());
2011-12-27 10:04:59 -05:00
m_newParent = m_groupView->currentGroup();
m_newParent->applyGroupIconOnCreateTo(m_newEntry.data());
switchToEntryEdit(m_newEntry.data(), true);
}
void DatabaseWidget::replaceDatabase(QSharedPointer<Database> db)
{
Q_ASSERT(!isEntryEditActive() && !isGroupEditActive());
// Save off new parent UUID which will be valid when creating a new entry
QUuid newParentUuid;
if (m_newParent) {
newParentUuid = m_newParent->uuid();
}
// TODO: instead of increasing the ref count temporarily, there should be a clean
// break from the old database. Without this crashes occur due to the change
// signals triggering dangling pointers.
auto oldDb = m_db;
m_db = std::move(db);
connectDatabaseSignals();
m_groupView->changeDatabase(m_db);
// Restore the new parent group pointer, if not found default to the root group
// this prevents data loss when merging a database while creating a new entry
if (!newParentUuid.isNull()) {
m_newParent = m_db->rootGroup()->findGroupByUuid(newParentUuid);
if (!m_newParent) {
m_newParent = m_db->rootGroup();
}
}
emit databaseReplaced(oldDb, m_db);
#if defined(WITH_XC_KEESHARE)
KeeShare::instance()->connectDatabase(m_db, oldDb);
#else
// Keep the instance active till the end of this function
Q_UNUSED(oldDb);
#endif
oldDb->releaseData();
}
2012-05-15 15:10:39 -04:00
void DatabaseWidget::cloneEntry()
{
auto currentEntry = currentSelectedEntry();
Q_ASSERT(currentEntry);
if (!currentEntry) {
return;
}
auto cloneDialog = new CloneDialog(this, m_db.data(), currentEntry);
cloneDialog->show();
2012-05-15 15:10:39 -04:00
}
2017-04-13 06:05:36 -04:00
void DatabaseWidget::showTotp()
{
auto currentEntry = currentSelectedEntry();
Q_ASSERT(currentEntry);
2017-04-13 06:05:36 -04:00
if (!currentEntry) {
return;
}
auto totpDialog = new TotpDialog(this, currentEntry);
connect(this, &DatabaseWidget::databaseLockRequested, totpDialog, &TotpDialog::close);
2017-04-13 06:05:36 -04:00
totpDialog->open();
}
void DatabaseWidget::copyTotp()
{
auto currentEntry = currentSelectedEntry();
Q_ASSERT(currentEntry);
2017-04-13 06:05:36 -04:00
if (!currentEntry) {
return;
}
setClipboardTextAndMinimize(currentEntry->totp());
}
void DatabaseWidget::setupTotp()
{
auto currentEntry = currentSelectedEntry();
Q_ASSERT(currentEntry);
2017-04-13 06:05:36 -04:00
if (!currentEntry) {
return;
}
auto setupTotpDialog = new TotpSetupDialog(this, currentEntry);
connect(setupTotpDialog, SIGNAL(totpUpdated()), SIGNAL(entrySelectionChanged()));
connect(this, &DatabaseWidget::databaseLockRequested, setupTotpDialog, &TotpSetupDialog::close);
2017-04-13 06:05:36 -04:00
setupTotpDialog->open();
}
void DatabaseWidget::deleteSelectedEntries()
2012-04-18 14:08:54 -04:00
{
2013-10-08 15:36:01 -04:00
const QModelIndexList selected = m_entryView->selectionModel()->selectedRows();
if (selected.isEmpty()) {
return;
}
// Resolve entries from the selection model
2013-10-08 15:36:01 -04:00
QList<Entry*> selectedEntries;
for (const QModelIndex& index : selected) {
2013-10-08 15:36:01 -04:00
selectedEntries.append(m_entryView->entryFromIndex(index));
}
deleteEntries(std::move(selectedEntries));
}
void DatabaseWidget::deleteEntries(QList<Entry*> selectedEntries)
{
// Confirm entry removal before moving forward
2018-10-19 15:41:42 -04:00
auto* recycleBin = m_db->metadata()->recycleBin();
bool permanent = (recycleBin && recycleBin->findEntryByUuid(selectedEntries.first()->uuid()))
|| !m_db->metadata()->recycleBinEnabled();
if (!confirmDeleteEntries(selectedEntries, permanent)) {
return;
}
// Find references to selected entries and prompt for direction if necessary
auto it = selectedEntries.begin();
while (it != selectedEntries.end()) {
auto references = m_db->rootGroup()->referencesRecursive(*it);
if (!references.isEmpty()) {
// Ignore references that are selected for deletion
for (auto* entry : selectedEntries) {
references.removeAll(entry);
}
if (!references.isEmpty()) {
// Prompt for reference handling
auto result = MessageBox::question(
this,
tr("Replace references to entry?"),
tr("Entry \"%1\" has %2 reference(s). "
"Do you want to overwrite references with values, skip this entry, or delete anyway?",
"",
references.size())
.arg((*it)->title().toHtmlEscaped())
.arg(references.size()),
MessageBox::Overwrite | MessageBox::Skip | MessageBox::Delete,
MessageBox::Overwrite);
if (result == MessageBox::Overwrite) {
for (auto* entry : references) {
entry->replaceReferencesWithValues(*it);
}
} else if (result == MessageBox::Skip) {
it = selectedEntries.erase(it);
continue;
}
}
}
it++;
}
if (permanent) {
for (auto* entry : asConst(selectedEntries)) {
delete entry;
}
} else {
for (auto* entry : asConst(selectedEntries)) {
m_db->recycleEntry(entry);
}
}
refreshSearch();
m_entryView->setFirstEntryActive();
auto* currentEntry = currentSelectedEntry();
if (currentEntry) {
m_previewView->setEntry(currentEntry);
} else {
m_previewView->setGroup(groupView()->currentGroup());
}
}
bool DatabaseWidget::confirmDeleteEntries(QList<Entry*> entries, bool permanent)
{
if (entries.isEmpty()) {
return false;
}
if (permanent) {
QString prompt;
if (entries.size() == 1) {
prompt = tr("Do you really want to delete the entry \"%1\" for good?")
.arg(entries.first()->title().toHtmlEscaped());
2018-03-31 16:01:30 -04:00
} else {
prompt = tr("Do you really want to delete %n entry(s) for good?", "", entries.size());
2013-10-08 15:36:01 -04:00
}
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-19 23:14:11 -05:00
auto answer = MessageBox::question(this,
tr("Delete entry(s)?", "", entries.size()),
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-19 23:14:11 -05:00
prompt,
MessageBox::Delete | MessageBox::Cancel,
MessageBox::Cancel);
return answer == MessageBox::Delete;
} else if (config()->get(Config::Security_NoConfirmMoveEntryToRecycleBin).toBool()) {
return true;
2018-03-31 16:01:30 -04:00
} else {
QString prompt;
if (entries.size() == 1) {
prompt = tr("Do you really want to move entry \"%1\" to the recycle bin?")
.arg(entries.first()->title().toHtmlEscaped());
} else {
prompt = tr("Do you really want to move %n entry(s) to the recycle bin?", "", entries.size());
}
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-19 23:14:11 -05:00
auto answer = MessageBox::question(this,
tr("Move entry(s) to recycle bin?", "", entries.size()),
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-19 23:14:11 -05:00
prompt,
MessageBox::Move | MessageBox::Cancel,
MessageBox::Cancel);
return answer == MessageBox::Move;
}
2012-04-18 14:08:54 -04:00
}
void DatabaseWidget::setFocus(Qt::FocusReason reason)
{
if (reason == Qt::BacktabFocusReason) {
m_previewView->setFocus();
} else {
m_groupView->setFocus();
}
}
void DatabaseWidget::focusOnEntries(bool editIfFocused)
{
if (isEntryViewActive()) {
if (editIfFocused && m_entryView->hasFocus()) {
switchToEntryEdit();
} else {
m_entryView->setFocus();
}
}
}
void DatabaseWidget::focusOnGroups(bool editIfFocused)
{
if (isEntryViewActive()) {
if (editIfFocused && m_groupView->hasFocus()) {
switchToGroupEdit();
} else {
m_groupView->setFocus();
}
}
}
void DatabaseWidget::moveEntryUp()
{
auto currentEntry = currentSelectedEntry();
if (currentEntry) {
currentEntry->moveUp();
m_entryView->setCurrentEntry(currentEntry);
}
}
void DatabaseWidget::moveEntryDown()
{
auto currentEntry = currentSelectedEntry();
if (currentEntry) {
currentEntry->moveDown();
m_entryView->setCurrentEntry(currentEntry);
}
}
void DatabaseWidget::copyTitle()
{
auto currentEntry = currentSelectedEntry();
if (currentEntry) {
setClipboardTextAndMinimize(currentEntry->resolveMultiplePlaceholders(currentEntry->title()));
}
}
void DatabaseWidget::copyUsername()
{
auto currentEntry = currentSelectedEntry();
if (currentEntry) {
setClipboardTextAndMinimize(currentEntry->resolveMultiplePlaceholders(currentEntry->username()));
}
}
void DatabaseWidget::copyPassword()
{
// QTextEdit does not properly trap Ctrl+C copy shortcut
// if a text edit has focus pass the copy operation to it
auto textEdit = qobject_cast<QTextEdit*>(focusWidget());
if (textEdit) {
textEdit->copy();
return;
}
auto currentEntry = currentSelectedEntry();
if (currentEntry) {
setClipboardTextAndMinimize(currentEntry->resolveMultiplePlaceholders(currentEntry->password()));
}
}
void DatabaseWidget::copyURL()
{
auto currentEntry = currentSelectedEntry();
if (currentEntry) {
setClipboardTextAndMinimize(currentEntry->resolveMultiplePlaceholders(currentEntry->url()));
}
}
void DatabaseWidget::copyNotes()
{
auto currentEntry = currentSelectedEntry();
if (currentEntry) {
setClipboardTextAndMinimize(currentEntry->resolveMultiplePlaceholders(currentEntry->notes()));
}
}
void DatabaseWidget::copyAttribute(QAction* action)
{
auto currentEntry = currentSelectedEntry();
if (currentEntry) {
setClipboardTextAndMinimize(
currentEntry->resolveMultiplePlaceholders(currentEntry->attributes()->value(action->data().toString())));
}
2013-12-01 03:59:43 -05:00
}
void DatabaseWidget::showTotpKeyQrCode()
{
auto currentEntry = currentSelectedEntry();
if (currentEntry) {
auto totpDisplayDialog = new TotpExportSettingsDialog(this, currentEntry);
connect(this, &DatabaseWidget::databaseLockRequested, totpDisplayDialog, &TotpExportSettingsDialog::close);
totpDisplayDialog->open();
}
2013-12-01 03:59:43 -05:00
}
2013-12-01 03:59:43 -05:00
void DatabaseWidget::setClipboardTextAndMinimize(const QString& text)
{
clipboard()->setText(text);
if (config()->get(Config::HideWindowOnCopy).toBool()) {
if (config()->get(Config::MinimizeOnCopy).toBool()) {
getMainWindow()->minimizeOrHide();
} else if (config()->get(Config::DropToBackgroundOnCopy).toBool()) {
2019-06-09 14:15:02 -04:00
window()->lower();
}
}
}
2018-05-10 09:12:36 -04:00
#ifdef WITH_XC_SSHAGENT
void DatabaseWidget::addToAgent()
{
Entry* currentEntry = m_entryView->currentEntry();
Q_ASSERT(currentEntry);
if (!currentEntry) {
return;
}
KeeAgentSettings settings;
if (!settings.fromEntry(currentEntry)) {
return;
}
OpenSSHKey key;
if (settings.toOpenSSHKey(currentEntry, key, true)) {
SSHAgent::instance()->addIdentity(key, settings, database()->uuid());
2018-05-10 09:12:36 -04:00
} else {
m_messageWidget->showMessage(key.errorString(), MessageWidget::Error);
}
}
void DatabaseWidget::removeFromAgent()
{
Entry* currentEntry = m_entryView->currentEntry();
Q_ASSERT(currentEntry);
if (!currentEntry) {
return;
}
KeeAgentSettings settings;
if (!settings.fromEntry(currentEntry)) {
return;
}
OpenSSHKey key;
if (settings.toOpenSSHKey(currentEntry, key, false)) {
SSHAgent::instance()->removeIdentity(key);
} else {
m_messageWidget->showMessage(key.errorString(), MessageWidget::Error);
}
}
#endif
2012-07-12 16:35:51 -04:00
void DatabaseWidget::performAutoType()
{
auto currentEntry = currentSelectedEntry();
if (currentEntry) {
autoType()->performAutoType(currentEntry, window());
2012-07-12 16:35:51 -04:00
}
}
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();
if (currentEntry) {
openUrlForEntry(currentEntry);
}
}
void DatabaseWidget::downloadSelectedFavicons()
{
#ifdef WITH_XC_NETWORKING
QList<Entry*> selectedEntries;
for (const auto& index : m_entryView->selectionModel()->selectedRows()) {
selectedEntries.append(m_entryView->entryFromIndex(index));
}
// Force download even if icon already exists
performIconDownloads(selectedEntries, true);
#endif
}
void DatabaseWidget::downloadAllFavicons()
{
#ifdef WITH_XC_NETWORKING
auto currentGroup = m_groupView->currentGroup();
if (currentGroup) {
performIconDownloads(currentGroup->entries());
}
#endif
}
void DatabaseWidget::performIconDownloads(const QList<Entry*>& entries, bool force)
{
#ifdef WITH_XC_NETWORKING
auto* iconDownloaderDialog = new IconDownloaderDialog(this);
connect(this, SIGNAL(databaseLockRequested()), iconDownloaderDialog, SLOT(close()));
iconDownloaderDialog->downloadFavicons(m_db, entries, force);
#else
Q_UNUSED(entries);
Q_UNUSED(force);
#endif
}
void DatabaseWidget::openUrlForEntry(Entry* entry)
{
Q_ASSERT(entry);
if (!entry) {
return;
}
QString cmdString = entry->resolveMultiplePlaceholders(entry->url());
if (cmdString.startsWith("cmd://")) {
// check if decision to execute command was stored
2019-06-21 20:42:27 -04:00
bool launch = (entry->attributes()->value(EntryAttributes::RememberCmdExecAttr) == "1");
// otherwise ask user
2019-06-21 20:42:27 -04:00
if (!launch && cmdString.length() > 6) {
QString cmdTruncated = entry->resolveMultiplePlaceholders(entry->maskPasswordPlaceholders(entry->url()));
cmdTruncated = cmdTruncated.mid(6);
if (cmdTruncated.length() > 400) {
cmdTruncated = cmdTruncated.left(400) + " […]";
}
QMessageBox msgbox(QMessageBox::Icon::Question,
tr("Execute command?"),
tr("Do you really want to execute the following command?<br><br>%1<br>")
.arg(cmdTruncated.toHtmlEscaped()),
QMessageBox::Yes | QMessageBox::No,
2018-03-31 16:01:30 -04:00
this);
msgbox.setDefaultButton(QMessageBox::No);
QCheckBox* checkbox = new QCheckBox(tr("Remember my choice"), &msgbox);
msgbox.setCheckBox(checkbox);
bool remember = false;
QObject::connect(checkbox, &QCheckBox::stateChanged, [&](int state) {
if (static_cast<Qt::CheckState>(state) == Qt::CheckState::Checked) {
2018-03-31 16:01:30 -04:00
remember = true;
}
});
int result = msgbox.exec();
2019-06-21 20:42:27 -04:00
launch = (result == QMessageBox::Yes);
if (remember) {
2018-03-31 16:01:30 -04:00
entry->attributes()->set(EntryAttributes::RememberCmdExecAttr, result == QMessageBox::Yes ? "1" : "0");
}
}
2019-06-21 20:42:27 -04:00
if (launch) {
QProcess::startDetached(cmdString.mid(6));
if (config()->get(Config::MinimizeOnOpenUrl).toBool()) {
getMainWindow()->minimizeOrHide();
2019-06-21 20:42:27 -04:00
}
}
} else if (cmdString.startsWith("kdbx://")) {
openDatabaseFromEntry(entry, false);
2018-03-31 16:01:30 -04:00
} else {
QUrl url = QUrl::fromUserInput(entry->resolveMultiplePlaceholders(entry->url()));
if (!url.isEmpty()) {
QDesktopServices::openUrl(url);
2019-06-21 20:42:27 -04:00
if (config()->get(Config::MinimizeOnOpenUrl).toBool()) {
getMainWindow()->minimizeOrHide();
2019-06-21 20:42:27 -04:00
}
}
}
}
Entry* DatabaseWidget::currentSelectedEntry()
{
if (currentWidget() == m_editEntryWidget) {
return m_editEntryWidget->currentEntry();
}
return m_entryView->currentEntry();
}
void DatabaseWidget::createGroup()
{
Q_ASSERT(m_groupView->currentGroup());
if (!m_groupView->currentGroup()) {
return;
}
m_newGroup.reset(new Group());
2018-03-22 17:56:05 -04:00
m_newGroup->setUuid(QUuid::createUuid());
2011-12-27 10:04:59 -05:00
m_newParent = m_groupView->currentGroup();
switchToGroupEdit(m_newGroup.data(), true);
}
2012-04-21 13:06:28 -04:00
void DatabaseWidget::deleteGroup()
{
Group* currentGroup = m_groupView->currentGroup();
Q_ASSERT(currentGroup && canDeleteCurrentGroup());
if (!currentGroup || !canDeleteCurrentGroup()) {
return;
}
2012-04-21 13:06:28 -04:00
2018-10-19 15:41:42 -04:00
auto* recycleBin = m_db->metadata()->recycleBin();
bool inRecycleBin = recycleBin && recycleBin->findGroupByUuid(currentGroup->uuid());
bool isRecycleBin = recycleBin && (currentGroup == recycleBin);
bool isRecycleBinSubgroup = recycleBin && currentGroup->findGroupByUuid(recycleBin->uuid());
2016-12-01 23:04:42 -05:00
if (inRecycleBin || isRecycleBin || isRecycleBinSubgroup || !m_db->metadata()->recycleBinEnabled()) {
auto result = MessageBox::question(
this,
tr("Delete group"),
tr("Do you really want to delete the group \"%1\" for good?").arg(currentGroup->name().toHtmlEscaped()),
MessageBox::Delete | MessageBox::Cancel,
MessageBox::Cancel);
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-19 23:14:11 -05:00
if (result == MessageBox::Delete) {
delete currentGroup;
2012-04-21 13:06:28 -04:00
}
2018-03-31 16:01:30 -04:00
} else {
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-19 23:14:11 -05:00
auto result = MessageBox::question(this,
tr("Move group to recycle bin?"),
tr("Do you really want to move the group "
"\"%1\" to the recycle bin?")
.arg(currentGroup->name().toHtmlEscaped()),
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-19 23:14:11 -05:00
MessageBox::Move | MessageBox::Cancel,
MessageBox::Cancel);
if (result == MessageBox::Move) {
m_db->recycleGroup(currentGroup);
}
2012-04-21 13:06:28 -04:00
}
}
int DatabaseWidget::addChildWidget(QWidget* w)
{
w->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
int index = QStackedWidget::addWidget(w);
adjustSize();
return index;
}
void DatabaseWidget::switchToMainView(bool previousDialogAccepted)
{
setCurrentWidget(m_mainWidget);
if (m_newGroup) {
if (previousDialogAccepted) {
2011-12-27 10:04:59 -05:00
m_newGroup->setParent(m_newParent);
m_groupView->setCurrentGroup(m_newGroup.take());
m_groupView->expandGroup(m_newParent);
} else {
m_newGroup.reset();
}
2015-07-24 12:28:12 -04:00
m_newParent = nullptr;
} else if (m_newEntry) {
if (previousDialogAccepted) {
2011-12-27 10:04:59 -05:00
m_newEntry->setGroup(m_newParent);
2012-05-02 13:33:37 -04:00
m_entryView->setFocus();
m_entryView->setCurrentEntry(m_newEntry.take());
} else {
m_newEntry.reset();
2011-12-27 10:04:59 -05:00
}
2015-07-24 12:28:12 -04:00
m_newParent = nullptr;
} else {
// Workaround: ensure entries are focused so search doesn't reset
m_entryView->setFocus();
}
if (sender() == m_entryView || sender() == m_editEntryWidget) {
onEntryChanged(m_entryView->currentEntry());
} else if (sender() == m_groupView || sender() == m_editGroupWidget) {
onGroupChanged();
}
2010-10-06 13:40:50 -04:00
}
2012-05-15 14:12:05 -04:00
void DatabaseWidget::switchToHistoryView(Entry* entry)
{
auto entryTitle = m_editEntryWidget->currentEntry() ? m_editEntryWidget->currentEntry()->title() : "";
m_historyEditEntryWidget->loadEntry(entry, false, true, entryTitle, m_db);
2012-10-12 06:12:00 -04:00
setCurrentWidget(m_historyEditEntryWidget);
2012-05-15 14:12:05 -04:00
}
void DatabaseWidget::switchBackToEntryEdit()
{
2012-10-12 06:12:00 -04:00
setCurrentWidget(m_editEntryWidget);
2012-05-15 14:12:05 -04:00
}
void DatabaseWidget::switchToEntryEdit(Entry* entry)
2010-10-06 13:40:50 -04:00
{
2011-12-27 10:04:59 -05:00
switchToEntryEdit(entry, false);
}
void DatabaseWidget::switchToEntryEdit(Entry* entry, bool create)
{
// If creating an entry, it will be in `currentGroup()` so it's
// okay to use but when editing, the entry may not be in
// `currentGroup()` so we get the entry's group.
Group* group;
if (create) {
group = currentGroup();
} else {
group = entry->group();
// Ensure we have only this entry selected
m_entryView->setCurrentEntry(entry);
}
Q_ASSERT(group);
// Setup the entry edit widget and display
m_editEntryWidget->loadEntry(entry, create, false, group->name(), m_db);
2012-10-12 06:12:00 -04:00
setCurrentWidget(m_editEntryWidget);
2010-08-24 16:26:52 -04:00
}
void DatabaseWidget::switchToGroupEdit(Group* group, bool create)
{
m_editGroupWidget->loadGroup(group, create, m_db);
2012-10-12 06:12:00 -04:00
setCurrentWidget(m_editGroupWidget);
}
void DatabaseWidget::connectDatabaseSignals()
{
// relayed Database events
connect(m_db.data(),
SIGNAL(filePathChanged(QString, QString)),
SIGNAL(databaseFilePathChanged(QString, QString)));
connect(m_db.data(), SIGNAL(databaseModified()), SIGNAL(databaseModified()));
connect(m_db.data(), SIGNAL(databaseModified()), SLOT(onDatabaseModified()));
connect(m_db.data(), SIGNAL(databaseSaved()), SIGNAL(databaseSaved()));
connect(m_db.data(), SIGNAL(databaseFileChanged()), this, SLOT(reloadDatabaseFile()));
}
void DatabaseWidget::loadDatabase(bool accepted)
{
auto* openWidget = qobject_cast<DatabaseOpenWidget*>(sender());
Q_ASSERT(openWidget);
if (!openWidget) {
return;
}
if (accepted) {
replaceDatabase(openWidget->database());
switchToMainView();
processAutoOpen();
2020-10-16 18:42:59 -04:00
restoreGroupEntryFocus(m_groupBeforeLock, m_entryBeforeLock);
m_groupBeforeLock = QUuid();
m_entryBeforeLock = QUuid();
m_saveAttempts = 0;
emit databaseUnlocked();
if (config()->get(Config::MinimizeAfterUnlock).toBool()) {
getMainWindow()->minimizeOrHide();
}
2018-03-31 16:01:30 -04:00
} else {
if (m_databaseOpenWidget->database()) {
m_databaseOpenWidget->database().reset();
}
emit closeRequest();
}
}
void DatabaseWidget::mergeDatabase(bool accepted)
{
if (accepted) {
if (!m_db) {
showMessage(tr("No current database."), MessageWidget::Error);
return;
}
auto* senderDialog = qobject_cast<DatabaseOpenDialog*>(sender());
Q_ASSERT(senderDialog);
if (!senderDialog) {
return;
}
auto srcDb = senderDialog->database();
if (!srcDb) {
showMessage(tr("No source database, nothing to do."), MessageWidget::Error);
return;
}
Merger merger(srcDb.data(), m_db.data());
QStringList changeList = merger.merge();
if (!changeList.isEmpty()) {
showMessage(tr("Successfully merged the database files."), MessageWidget::Information);
} else {
showMessage(tr("Database was not modified by merge operation."), MessageWidget::Information);
}
}
switchToMainView();
emit databaseMerged(m_db);
}
/**
* Unlock the database.
*
* @param accepted true if the unlock dialog or widget was confirmed with OK
*/
2012-10-12 06:12:00 -04:00
void DatabaseWidget::unlockDatabase(bool accepted)
{
auto* senderDialog = qobject_cast<DatabaseOpenDialog*>(sender());
if (!accepted) {
if (!senderDialog && (!m_db || !m_db->isInitialized())) {
emit closeRequest();
}
return;
}
if (senderDialog && senderDialog->intent() == DatabaseOpenDialog::Intent::Merge) {
mergeDatabase(accepted);
return;
}
QSharedPointer<Database> db;
if (senderDialog) {
db = senderDialog->database();
} else {
db = m_databaseOpenWidget->database();
}
replaceDatabase(db);
if (db->isReadOnly()) {
showMessage(
tr("This database is opened in read-only mode. Autosave is disabled."), MessageWidget::Warning, false, -1);
}
2012-10-12 06:12:00 -04:00
restoreGroupEntryFocus(m_groupBeforeLock, m_entryBeforeLock);
2018-03-22 17:56:05 -04:00
m_groupBeforeLock = QUuid();
m_entryBeforeLock = QUuid();
switchToMainView();
processAutoOpen();
emit databaseUnlocked();
if (senderDialog && senderDialog->intent() == DatabaseOpenDialog::Intent::AutoType) {
QList<QSharedPointer<Database>> dbList;
dbList.append(m_db);
autoType()->performGlobalAutoType(dbList);
}
2012-10-12 06:12:00 -04:00
}
void DatabaseWidget::entryActivationSignalReceived(Entry* entry, EntryModel::ModelColumn column)
{
Q_ASSERT(entry);
if (!entry) {
return;
}
// Implement 'copy-on-doubleclick' functionality for certain columns
switch (column) {
case EntryModel::Username:
setClipboardTextAndMinimize(entry->resolveMultiplePlaceholders(entry->username()));
break;
case EntryModel::Password:
setClipboardTextAndMinimize(entry->resolveMultiplePlaceholders(entry->password()));
break;
case EntryModel::Url:
if (!entry->url().isEmpty()) {
openUrlForEntry(entry);
}
break;
case EntryModel::Totp:
if (entry->hasTotp()) {
setClipboardTextAndMinimize(entry->totp());
} else {
setupTotp();
}
break;
case EntryModel::ParentGroup:
// Call this first to clear out of search mode, otherwise
// the desired entry is not properly selected
endSearch();
m_groupView->setCurrentGroup(entry->group());
m_entryView->setCurrentEntry(entry);
break;
// TODO: switch to 'Notes' tab in details view/pane
2018-03-31 16:01:30 -04:00
// case EntryModel::Notes:
// break;
// TODO: switch to 'Attachments' tab in details view/pane
2018-03-31 16:01:30 -04:00
// case EntryModel::Attachments:
// break;
default:
switchToEntryEdit(entry);
}
}
void DatabaseWidget::switchToDatabaseReports()
{
m_reportsDialog->load(m_db);
setCurrentWidget(m_reportsDialog);
}
void DatabaseWidget::switchToDatabaseSettings()
{
m_databaseSettingDialog->load(m_db);
setCurrentWidget(m_databaseSettingDialog);
}
void DatabaseWidget::switchToOpenDatabase()
{
if (currentWidget() != m_databaseOpenWidget || m_databaseOpenWidget->filename() != m_db->filePath()) {
switchToOpenDatabase(m_db->filePath());
}
}
void DatabaseWidget::switchToOpenDatabase(const QString& filePath)
{
m_databaseOpenWidget->load(filePath);
setCurrentWidget(m_databaseOpenWidget);
}
2018-03-31 16:01:30 -04:00
void DatabaseWidget::switchToOpenDatabase(const QString& filePath, const QString& password, const QString& keyFile)
{
switchToOpenDatabase(filePath);
m_databaseOpenWidget->enterKey(password, keyFile);
}
void DatabaseWidget::switchToCsvImport(const QString& filePath)
{
setCurrentWidget(m_csvImportWizard);
m_csvImportWizard->load(filePath, m_db.data());
}
void DatabaseWidget::csvImportFinished(bool accepted)
{
if (!accepted) {
emit closeRequest();
} else {
switchToMainView();
}
}
void DatabaseWidget::switchToImportKeepass1(const QString& filePath)
{
m_keepass1OpenWidget->load(filePath);
2012-10-12 06:12:00 -04:00
setCurrentWidget(m_keepass1OpenWidget);
}
void DatabaseWidget::switchToImportOpVault(const QString& fileName)
{
m_opVaultOpenWidget->load(fileName);
setCurrentWidget(m_opVaultOpenWidget);
}
void DatabaseWidget::switchToEntryEdit()
{
auto entry = m_entryView->currentEntry();
if (!entry) {
return;
}
switchToEntryEdit(entry, false);
}
void DatabaseWidget::switchToGroupEdit()
{
auto group = m_groupView->currentGroup();
if (!group) {
return;
}
switchToGroupEdit(group, false);
}
void DatabaseWidget::sortGroupsAsc()
{
m_groupView->sortGroups();
}
void DatabaseWidget::sortGroupsDesc()
{
m_groupView->sortGroups(true);
}
void DatabaseWidget::switchToDatabaseSecurity()
{
switchToDatabaseSettings();
m_databaseSettingDialog->showDatabaseKeySettings();
}
void DatabaseWidget::performUnlockDatabase(const QString& password, const QString& keyfile)
{
if (password.isEmpty() && keyfile.isEmpty()) {
return;
}
if (!m_db->isInitialized() || isLocked()) {
switchToOpenDatabase();
m_databaseOpenWidget->enterKey(password, keyfile);
}
}
2018-03-31 16:01:30 -04:00
void DatabaseWidget::refreshSearch()
{
if (isSearchActive()) {
search(m_lastSearchText);
}
}
void DatabaseWidget::search(const QString& searchtext)
{
2018-03-31 16:01:30 -04:00
if (searchtext.isEmpty()) {
endSearch();
return;
}
emit searchModeAboutToActivate();
Group* searchGroup = m_searchLimitGroup ? currentGroup() : m_db->rootGroup();
QList<Entry*> searchResult = m_EntrySearcher->search(searchtext, searchGroup);
2012-05-27 08:47:15 -04:00
2018-03-21 21:52:57 -04:00
m_entryView->displaySearch(searchResult);
m_lastSearchText = searchtext;
2012-05-27 08:47:15 -04:00
// Display a label detailing our search results
if (!searchResult.isEmpty()) {
m_searchingLabel->setText(tr("Search Results (%1)").arg(searchResult.size()));
2018-03-31 16:01:30 -04:00
} else {
m_searchingLabel->setText(tr("No Results"));
}
m_searchingLabel->setVisible(true);
#ifdef WITH_XC_KEESHARE
m_shareLabel->setVisible(false);
#endif
emit searchModeActivated();
}
void DatabaseWidget::setSearchCaseSensitive(bool state)
2012-05-12 07:22:41 -04:00
{
m_EntrySearcher->setCaseSensitive(state);
refreshSearch();
}
2012-05-12 07:22:41 -04:00
void DatabaseWidget::setSearchLimitGroup(bool state)
{
m_searchLimitGroup = state;
refreshSearch();
}
void DatabaseWidget::onGroupChanged()
{
auto group = m_groupView->currentGroup();
// Intercept group changes if in search mode
if (isSearchActive() && m_searchLimitGroup) {
search(m_lastSearchText);
} else if (isSearchActive()) {
endSearch();
} else {
2018-03-21 21:52:57 -04:00
m_entryView->displayGroup(group);
}
m_previewView->setGroup(group);
#ifdef WITH_XC_KEESHARE
auto shareLabel = KeeShare::sharingLabel(group);
if (!shareLabel.isEmpty()) {
m_shareLabel->setText(shareLabel);
m_shareLabel->setVisible(true);
} else {
m_shareLabel->setVisible(false);
}
#endif
2012-05-12 07:22:41 -04:00
}
void DatabaseWidget::onDatabaseModified()
{
if (!m_blockAutoSave && config()->get(Config::AutoSaveAfterEveryChange).toBool() && !m_db->isReadOnly()) {
save();
} else {
// Only block once, then reset
m_blockAutoSave = false;
}
}
QString DatabaseWidget::getCurrentSearch()
{
return m_lastSearchText;
}
void DatabaseWidget::endSearch()
2012-05-27 07:19:12 -04:00
{
if (isSearchActive()) {
// Show the normal entry view of the current group
emit listModeAboutToActivate();
2018-03-21 21:52:57 -04:00
m_entryView->displayGroup(currentGroup());
emit listModeActivated();
m_entryView->setFirstEntryActive();
// Enforce preview view update (prevents stale information if focus group is empty)
m_previewView->setEntry(currentSelectedEntry());
2012-05-27 07:19:12 -04:00
}
m_searchingLabel->setVisible(false);
m_searchingLabel->setText(tr("Searching..."));
m_lastSearchText.clear();
// Tell the search widget to clear
emit clearSearch();
2012-05-27 07:19:12 -04:00
}
void DatabaseWidget::emitGroupContextMenuRequested(const QPoint& pos)
{
emit groupContextMenuRequested(m_groupView->viewport()->mapToGlobal(pos));
}
void DatabaseWidget::emitEntryContextMenuRequested(const QPoint& pos)
{
emit entryContextMenuRequested(m_entryView->viewport()->mapToGlobal(pos));
}
void DatabaseWidget::onEntryChanged(Entry* entry)
{
if (entry) {
m_previewView->setEntry(entry);
}
emit entrySelectionChanged();
}
bool DatabaseWidget::canDeleteCurrentGroup() const
{
2012-04-21 13:06:28 -04:00
bool isRootGroup = m_db->rootGroup() == m_groupView->currentGroup();
return !isRootGroup;
2012-04-21 13:06:28 -04:00
}
Group* DatabaseWidget::currentGroup() const
{
return m_groupView->currentGroup();
}
void DatabaseWidget::closeEvent(QCloseEvent* event)
{
if (!isLocked() && !lock()) {
event->ignore();
return;
}
event->accept();
}
void DatabaseWidget::showEvent(QShowEvent* event)
{
if (!m_db->isInitialized() || isLocked()) {
switchToOpenDatabase();
}
2012-04-21 13:06:28 -04:00
event->accept();
2012-04-21 13:06:28 -04:00
}
bool DatabaseWidget::focusNextPrevChild(bool next)
{
// [parent] <-> GroupView <-> EntryView <-> EntryPreview <-> [parent]
if (next) {
if (m_groupView->hasFocus()) {
m_entryView->setFocus();
return true;
} else if (m_entryView->hasFocus()) {
m_previewView->setFocus();
return true;
}
} else {
if (m_previewView->hasFocus()) {
m_entryView->setFocus();
return true;
} else if (m_entryView->hasFocus()) {
m_groupView->setFocus();
return true;
}
}
// Defer to the parent widget to make a decision
return QStackedWidget::focusNextPrevChild(next);
}
bool DatabaseWidget::lock()
{
if (isLocked()) {
return true;
}
// Don't try to lock the database while saving, this will cause a deadlock
if (m_db->isSaving()) {
QTimer::singleShot(200, this, SLOT(lock()));
return false;
}
emit databaseLockRequested();
// ignore event if we are active and a modal dialog is still open (such as a message box or file dialog)
if (isVisible() && QApplication::activeModalWidget()) {
return false;
}
clipboard()->clearCopiedText();
2012-10-12 06:12:00 -04:00
if (isEditWidgetModified()) {
auto result = MessageBox::question(this,
tr("Lock Database?"),
tr("You are editing an entry. Discard changes and lock anyway?"),
MessageBox::Discard | MessageBox::Cancel,
MessageBox::Cancel);
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-19 23:14:11 -05:00
if (result == MessageBox::Cancel) {
return false;
}
}
if (m_db->isModified()) {
bool saved = false;
// Attempt to save on exit, but don't block locking if it fails
if (config()->get(Config::AutoSaveOnExit).toBool()
|| config()->get(Config::AutoSaveAfterEveryChange).toBool()) {
saved = save();
}
if (!saved) {
QString msg;
if (!m_db->metadata()->name().toHtmlEscaped().isEmpty()) {
msg = tr("\"%1\" was modified.\nSave changes?").arg(m_db->metadata()->name().toHtmlEscaped());
} else {
msg = tr("Database was modified.\nSave changes?");
}
auto result = MessageBox::question(this,
tr("Save changes?"),
msg,
MessageBox::Save | MessageBox::Discard | MessageBox::Cancel,
MessageBox::Save);
if (result == MessageBox::Save) {
if (!save()) {
return false;
}
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-19 23:14:11 -05:00
} else if (result == MessageBox::Cancel) {
return false;
}
}
} else if (m_db->hasNonDataChanges() && config()->get(Config::AutoSaveNonDataChanges).toBool()) {
// Silently auto-save non-data changes, ignore errors
QString errorMessage;
performSave(errorMessage);
}
if (m_groupView->currentGroup()) {
m_groupBeforeLock = m_groupView->currentGroup()->uuid();
2018-03-31 16:01:30 -04:00
} else {
m_groupBeforeLock = m_db->rootGroup()->uuid();
}
2012-10-12 06:12:00 -04:00
auto currentEntry = currentSelectedEntry();
if (currentEntry) {
m_entryBeforeLock = currentEntry->uuid();
}
2017-05-28 03:11:11 -04:00
endSearch();
clearAllWidgets();
switchToOpenDatabase(m_db->filePath());
auto newDb = QSharedPointer<Database>::create(m_db->filePath());
replaceDatabase(newDb);
emit databaseLocked();
return true;
2012-10-12 06:12:00 -04:00
}
void DatabaseWidget::reloadDatabaseFile()
{
// Ignore reload if we are locked or currently editing an entry or group
if (!m_db || isLocked() || isEntryEditActive() || isGroupEditActive()) {
return;
}
m_blockAutoSave = true;
if (!config()->get(Config::AutoReloadOnChange).toBool()) {
// Ask if we want to reload the db
auto result = MessageBox::question(this,
tr("File has changed"),
tr("The database file has changed. Do you want to load the changes?"),
MessageBox::Yes | MessageBox::No);
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-19 23:14:11 -05:00
if (result == MessageBox::No) {
// Notify everyone the database does not match the file
m_db->markAsModified();
return;
}
}
// Lock out interactions
m_entryView->setDisabled(true);
m_groupView->setDisabled(true);
QApplication::processEvents();
QString error;
auto db = QSharedPointer<Database>::create(m_db->filePath());
if (db->open(database()->key(), &error)) {
if (m_db->isModified() || db->hasNonDataChanges()) {
// Ask if we want to merge changes into new database
auto result = MessageBox::question(
this,
tr("Merge Request"),
tr("The database file has changed and you have unsaved changes.\nDo you want to merge your changes?"),
MessageBox::Merge | MessageBox::Discard,
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-19 23:14:11 -05:00
MessageBox::Merge);
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-19 23:14:11 -05:00
if (result == MessageBox::Merge) {
// Merge the old database into the new one
Merger merger(m_db.data(), db.data());
merger.merge();
}
}
QUuid groupBeforeReload = m_db->rootGroup()->uuid();
if (m_groupView && m_groupView->currentGroup()) {
groupBeforeReload = m_groupView->currentGroup()->uuid();
}
QUuid entryBeforeReload;
if (m_entryView && m_entryView->currentEntry()) {
entryBeforeReload = m_entryView->currentEntry()->uuid();
}
replaceDatabase(db);
processAutoOpen();
restoreGroupEntryFocus(groupBeforeReload, entryBeforeReload);
m_blockAutoSave = false;
} else {
showMessage(tr("Could not open the new database file while attempting to autoreload.\nError: %1").arg(error),
MessageWidget::Error);
// Mark db as modified since existing data may differ from file or file was deleted
m_db->markAsModified();
}
// Return control
m_entryView->setDisabled(false);
m_groupView->setDisabled(false);
}
2014-05-15 16:53:59 -04:00
int DatabaseWidget::numberOfSelectedEntries() const
{
return m_entryView->numberOfSelectedEntries();
}
int DatabaseWidget::currentEntryIndex() const
{
return m_entryView->currentEntryIndex();
}
2014-05-15 16:53:59 -04:00
QStringList DatabaseWidget::customEntryAttributes() const
{
Entry* entry = m_entryView->currentEntry();
if (!entry) {
return QStringList();
}
return entry->attributes()->customKeys();
}
/*
* Restores the focus on the group and entry provided
*/
2018-03-22 17:56:05 -04:00
void DatabaseWidget::restoreGroupEntryFocus(const QUuid& groupUuid, const QUuid& entryUuid)
{
auto group = m_db->rootGroup()->findGroupByUuid(groupUuid);
if (group) {
m_groupView->setCurrentGroup(group);
auto entry = group->findEntryByUuid(entryUuid, false);
if (entry) {
m_entryView->setCurrentEntry(entry);
2018-03-31 16:01:30 -04:00
}
}
}
2014-05-15 16:53:59 -04:00
bool DatabaseWidget::isGroupSelected() const
{
return m_groupView->currentGroup();
}
bool DatabaseWidget::currentEntryHasTitle()
{
auto currentEntry = currentSelectedEntry();
Q_ASSERT(currentEntry);
if (!currentEntry) {
return false;
}
return !currentEntry->title().isEmpty();
}
bool DatabaseWidget::currentEntryHasUsername()
{
auto currentEntry = currentSelectedEntry();
Q_ASSERT(currentEntry);
if (!currentEntry) {
return false;
}
return !currentEntry->resolveMultiplePlaceholders(currentEntry->username()).isEmpty();
}
bool DatabaseWidget::currentEntryHasPassword()
{
auto currentEntry = currentSelectedEntry();
Q_ASSERT(currentEntry);
if (!currentEntry) {
return false;
}
return !currentEntry->resolveMultiplePlaceholders(currentEntry->password()).isEmpty();
}
bool DatabaseWidget::currentEntryHasUrl()
{
auto currentEntry = currentSelectedEntry();
Q_ASSERT(currentEntry);
if (!currentEntry) {
return false;
}
return !currentEntry->resolveMultiplePlaceholders(currentEntry->url()).isEmpty();
}
2017-04-13 06:05:36 -04:00
bool DatabaseWidget::currentEntryHasTotp()
{
auto currentEntry = currentSelectedEntry();
Q_ASSERT(currentEntry);
2017-04-13 06:05:36 -04:00
if (!currentEntry) {
return false;
}
return currentEntry->hasTotp();
}
2018-05-10 09:12:36 -04:00
#ifdef WITH_XC_SSHAGENT
bool DatabaseWidget::currentEntryHasSshKey()
{
Entry* currentEntry = m_entryView->currentEntry();
Q_ASSERT(currentEntry);
if (!currentEntry) {
return false;
}
return KeeAgentSettings::inEntryAttachments(currentEntry->attachments());
2018-05-10 09:12:36 -04:00
}
#endif
bool DatabaseWidget::currentEntryHasNotes()
{
auto currentEntry = currentSelectedEntry();
Q_ASSERT(currentEntry);
if (!currentEntry) {
return false;
}
return !currentEntry->resolveMultiplePlaceholders(currentEntry->notes()).isEmpty();
}
2018-03-31 16:01:30 -04:00
GroupView* DatabaseWidget::groupView()
{
return m_groupView;
}
2018-03-31 16:01:30 -04:00
EntryView* DatabaseWidget::entryView()
{
return m_entryView;
}
/**
* Save the database to disk.
*
* This method will try to save several times in case of failure and
* ask to disable safe saves if it is unable to save after the third attempt.
* Set `attempt` to -1 to disable this behavior.
*
* @return true on success
*/
bool DatabaseWidget::save()
{
// Never allow saving a locked database; it causes corruption
Q_ASSERT(!isLocked());
// Release build interlock
if (isLocked()) {
// We return true since a save is not required
return true;
}
// Read-only and new databases ask for filename
if (m_db->isReadOnly() || m_db->filePath().isEmpty()) {
return saveAs();
}
// Prevent recursions and infinite save loops
m_blockAutoSave = true;
++m_saveAttempts;
QString errorMessage;
if (performSave(errorMessage)) {
m_saveAttempts = 0;
m_blockAutoSave = false;
return true;
}
if (m_saveAttempts > 2 && config()->get(Config::UseAtomicSaves).toBool()) {
// Saving failed 3 times, issue a warning and attempt to resolve
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-19 23:14:11 -05:00
auto result = MessageBox::question(this,
tr("Disable safe saves?"),
tr("KeePassXC has failed to save the database multiple times. "
"This is likely caused by file sync services holding a lock on "
"the save file.\nDisable safe saves and try again?"),
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-19 23:14:11 -05:00
MessageBox::Disable | MessageBox::Cancel,
MessageBox::Disable);
if (result == MessageBox::Disable) {
config()->set(Config::UseAtomicSaves, false);
return save();
}
}
showMessage(tr("Writing the database failed: %1").arg(errorMessage),
MessageWidget::Error,
true,
MessageWidget::LongAutoHideTimeout);
return false;
}
/**
* Save database under a new user-selected filename.
*
* @return true on success
*/
bool DatabaseWidget::saveAs()
{
// Never allow saving a locked database; it causes corruption
Q_ASSERT(!isLocked());
// Release build interlock
if (isLocked()) {
// We return true since a save is not required
return true;
}
QString oldFilePath = m_db->filePath();
if (!QFileInfo::exists(oldFilePath)) {
oldFilePath =
QDir::toNativeSeparators(config()->get(Config::LastDir).toString() + "/" + tr("Passwords").append(".kdbx"));
}
const QString newFilePath = fileDialog()->getSaveFileName(
this, tr("Save database as"), oldFilePath, tr("KeePass 2 Database").append(" (*.kdbx)"), nullptr, nullptr);
bool ok = false;
if (!newFilePath.isEmpty()) {
QString errorMessage;
if (!performSave(errorMessage, newFilePath)) {
showMessage(tr("Writing the database failed: %1").arg(errorMessage),
MessageWidget::Error,
true,
MessageWidget::LongAutoHideTimeout);
}
}
return ok;
}
bool DatabaseWidget::performSave(QString& errorMessage, const QString& fileName)
{
QPointer<QWidget> focusWidget(qApp->focusWidget());
// Lock out interactions
m_entryView->setDisabled(true);
m_groupView->setDisabled(true);
QApplication::processEvents();
bool ok;
if (fileName.isEmpty()) {
ok = m_db->save(&errorMessage,
config()->get(Config::UseAtomicSaves).toBool(),
config()->get(Config::BackupBeforeSave).toBool());
} else {
ok = m_db->saveAs(fileName,
&errorMessage,
config()->get(Config::UseAtomicSaves).toBool(),
config()->get(Config::BackupBeforeSave).toBool());
}
// Return control
m_entryView->setDisabled(false);
m_groupView->setDisabled(false);
if (focusWidget) {
focusWidget->setFocus();
}
return ok;
}
/**
* Save copy of database under a new user-selected filename.
*
* @return true on success
*/
bool DatabaseWidget::saveBackup()
{
while (true) {
QString oldFilePath = m_db->filePath();
if (!QFileInfo::exists(oldFilePath)) {
oldFilePath = QDir::toNativeSeparators(config()->get(Config::LastDir).toString() + "/"
+ tr("Passwords").append(".kdbx"));
}
const QString newFilePath = fileDialog()->getSaveFileName(this,
tr("Save database backup"),
oldFilePath,
tr("KeePass 2 Database").append(" (*.kdbx)"),
nullptr,
nullptr);
if (!newFilePath.isEmpty()) {
// Ensure we don't recurse back into this function
m_db->setReadOnly(false);
m_db->setFilePath(newFilePath);
m_saveAttempts = 0;
bool modified = m_db->isModified();
if (!save()) {
// Failed to save, try again
m_db->setFilePath(oldFilePath);
continue;
}
m_db->setFilePath(oldFilePath);
if (modified) {
// Source database is marked as clean when copy is saved, even if source has unsaved changes
m_db->markAsModified();
}
return true;
}
// Canceled file selection
return false;
}
}
void DatabaseWidget::showMessage(const QString& text,
MessageWidget::MessageType type,
bool showClosebutton,
int autoHideTimeout)
{
m_messageWidget->setCloseButtonVisible(showClosebutton);
m_messageWidget->showMessage(text, type, autoHideTimeout);
}
2017-12-25 08:53:22 -05:00
void DatabaseWidget::showErrorMessage(const QString& errorMessage)
{
showMessage(errorMessage, MessageWidget::MessageType::Error);
}
void DatabaseWidget::hideMessage()
{
if (m_messageWidget->isVisible()) {
m_messageWidget->animatedHide();
}
}
bool DatabaseWidget::isRecycleBinSelected() const
{
return m_groupView->currentGroup() && m_groupView->currentGroup() == m_db->metadata()->recycleBin();
}
2017-04-21 10:33:06 -04:00
void DatabaseWidget::emptyRecycleBin()
{
2018-03-31 16:01:30 -04:00
if (!isRecycleBinSelected()) {
return;
}
auto result =
MessageBox::question(this,
tr("Empty recycle bin?"),
tr("Are you sure you want to permanently delete everything from your recycle bin?"),
MessageBox::Empty | MessageBox::Cancel,
MessageBox::Cancel);
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-19 23:14:11 -05:00
if (result == MessageBox::Empty) {
2017-04-21 10:33:06 -04:00
m_db->emptyRecycleBin();
refreshSearch();
}
}
void DatabaseWidget::processAutoOpen()
{
Q_ASSERT(m_db);
auto* autoopenGroup = m_db->rootGroup()->findGroupByPath("/AutoOpen");
if (!autoopenGroup) {
return;
}
for (const auto* entry : autoopenGroup->entries()) {
if (entry->url().isEmpty() || (entry->password().isEmpty() && entry->username().isEmpty())) {
continue;
}
// Support ifDevice advanced entry, a comma separated list of computer names
// that control whether to perform AutoOpen on this entry or not. Can be
// negated using '!'
auto ifDevice = entry->attribute("IfDevice");
if (!ifDevice.isEmpty()) {
bool loadDb = false;
auto hostName = QHostInfo::localHostName();
for (auto& device : ifDevice.split(",")) {
device = device.trimmed();
if (device.startsWith("!")) {
if (device.mid(1).compare(hostName, Qt::CaseInsensitive) == 0) {
// Machine name matched an exclusion, don't load this database
loadDb = false;
break;
} else {
// Not matching an exclusion allows loading on all machines
loadDb = true;
}
} else if (device.compare(hostName, Qt::CaseInsensitive) == 0) {
// Explicitly named for loading
loadDb = true;
}
}
if (!loadDb) {
continue;
}
}
openDatabaseFromEntry(entry);
}
}
void DatabaseWidget::openDatabaseFromEntry(const Entry* entry, bool inBackground)
{
auto keyFile = entry->resolveMultiplePlaceholders(entry->username());
auto password = entry->resolveMultiplePlaceholders(entry->password());
auto databaseUrl = entry->resolveMultiplePlaceholders(entry->url());
if (databaseUrl.startsWith("kdbx://")) {
databaseUrl = databaseUrl.mid(7);
}
QFileInfo dbFileInfo;
if (databaseUrl.startsWith("file://")) {
QUrl url(databaseUrl);
dbFileInfo.setFile(url.toLocalFile());
} else {
dbFileInfo.setFile(databaseUrl);
if (dbFileInfo.isRelative()) {
QFileInfo currentpath(m_db->filePath());
dbFileInfo.setFile(currentpath.absoluteDir(), databaseUrl);
}
}
if (!dbFileInfo.isFile()) {
showErrorMessage(tr("Could not find database file: %1").arg(databaseUrl));
return;
}
QFileInfo keyFileInfo;
if (!keyFile.isEmpty()) {
if (keyFile.startsWith("file://")) {
QUrl keyfileUrl(keyFile);
keyFileInfo.setFile(keyfileUrl.toLocalFile());
} else {
keyFileInfo.setFile(keyFile);
if (keyFileInfo.isRelative()) {
QFileInfo currentpath(m_db->filePath());
keyFileInfo.setFile(currentpath.absoluteDir(), keyFile);
}
}
}
// Request to open the database file in the background with a password and keyfile
emit requestOpenDatabase(dbFileInfo.canonicalFilePath(), inBackground, password, keyFileInfo.canonicalFilePath());
}