2
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
3
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
5
* This program is free software: you can redistribute it and/or modify
6
* it under the terms of the GNU General Public License as published by
7
* the Free Software Foundation, either version 2 or (at your option)
8
* version 3 of the License.
10
* This program is distributed in the hope that it will be useful,
11
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
* GNU General Public License for more details.
15
* You should have received a copy of the GNU General Public License
16
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19
#include "DatabaseWidget.h"
21
#include <QApplication>
24
#include <QDesktopServices>
26
#include <QInputDialog>
28
#include <QPlainTextEdit>
31
#include <QTextDocumentFragment>
34
#include "autotype/AutoType.h"
35
#include "core/EntrySearcher.h"
36
#include "core/Merger.h"
37
#include "core/Tools.h"
38
#include "gui/Clipboard.h"
39
#include "gui/CloneDialog.h"
40
#include "gui/DatabaseOpenDialog.h"
41
#include "gui/DatabaseOpenWidget.h"
42
#include "gui/EntryPreviewWidget.h"
43
#include "gui/FileDialog.h"
44
#include "gui/GuiTools.h"
45
#include "gui/MainWindow.h"
46
#include "gui/MessageBox.h"
47
#include "gui/TotpDialog.h"
48
#include "gui/TotpExportSettingsDialog.h"
49
#include "gui/TotpSetupDialog.h"
50
#include "gui/dbsettings/DatabaseSettingsDialog.h"
51
#include "gui/entry/EntryView.h"
52
#include "gui/group/EditGroupWidget.h"
53
#include "gui/group/GroupView.h"
54
#include "gui/reports/ReportsDialog.h"
55
#include "gui/tag/TagView.h"
56
#include "gui/widgets/ElidedLabel.h"
57
#include "keeshare/KeeShare.h"
59
#ifdef WITH_XC_NETWORKING
60
#include "gui/IconDownloaderDialog.h"
63
#ifdef WITH_XC_SSHAGENT
64
#include "sshagent/SSHAgent.h"
67
#ifdef WITH_XC_BROWSER_PASSKEYS
68
#include "gui/passkeys/PasskeyImporter.h"
71
DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
72
: QStackedWidget(parent)
74
, m_mainWidget(new QWidget(this))
75
, m_mainSplitter(new QSplitter(m_mainWidget))
76
, m_groupSplitter(new QSplitter(this))
77
, m_messageWidget(new MessageWidget(this))
78
, m_previewView(new EntryPreviewWidget(this))
79
, m_previewSplitter(new QSplitter(m_mainWidget))
80
, m_searchingLabel(new QLabel(this))
81
, m_shareLabel(new ElidedLabel(this))
82
, m_editEntryWidget(new EditEntryWidget(this))
83
, m_editGroupWidget(new EditGroupWidget(this))
84
, m_historyEditEntryWidget(new EditEntryWidget(this))
85
, m_reportsDialog(new ReportsDialog(this))
86
, m_databaseSettingDialog(new DatabaseSettingsDialog(this))
87
, m_databaseOpenWidget(new DatabaseOpenWidget(this))
88
, m_groupView(new GroupView(m_db.data(), this))
89
, m_tagView(new TagView(this))
91
, m_entrySearcher(new EntrySearcher(false))
95
m_messageWidget->setHidden(true);
97
auto mainLayout = new QVBoxLayout();
98
mainLayout->addWidget(m_messageWidget);
99
auto hbox = new QHBoxLayout();
100
mainLayout->addLayout(hbox);
101
hbox->addWidget(m_mainSplitter);
102
m_mainWidget->setLayout(mainLayout);
104
// Setup searches and tags view and place under groups
105
m_tagView->setObjectName("tagView");
106
m_tagView->setDatabase(m_db);
107
connect(m_tagView, SIGNAL(activated(QModelIndex)), this, SLOT(filterByTag()));
108
connect(m_tagView, SIGNAL(clicked(QModelIndex)), this, SLOT(filterByTag()));
110
auto tagsWidget = new QWidget();
111
auto tagsLayout = new QVBoxLayout();
112
auto tagsTitle = new QLabel(tr("Searches and Tags"));
113
tagsTitle->setProperty("title", true);
114
tagsWidget->setObjectName("tagWidget");
115
tagsWidget->setLayout(tagsLayout);
116
tagsLayout->addWidget(tagsTitle);
117
tagsLayout->addWidget(m_tagView);
118
tagsLayout->setMargin(0);
120
m_groupSplitter->setOrientation(Qt::Vertical);
121
m_groupSplitter->setChildrenCollapsible(true);
122
m_groupSplitter->addWidget(m_groupView);
123
m_groupSplitter->addWidget(tagsWidget);
124
m_groupSplitter->setStretchFactor(0, 70);
125
m_groupSplitter->setStretchFactor(1, 30);
127
auto rightHandSideWidget = new QWidget(m_mainSplitter);
128
auto rightHandSideVBox = new QVBoxLayout();
129
rightHandSideVBox->setMargin(0);
130
rightHandSideVBox->addWidget(m_searchingLabel);
131
#ifdef WITH_XC_KEESHARE
132
rightHandSideVBox->addWidget(m_shareLabel);
134
rightHandSideVBox->addWidget(m_previewSplitter);
135
rightHandSideWidget->setLayout(rightHandSideVBox);
136
m_entryView = new EntryView(rightHandSideWidget);
138
m_mainSplitter->setChildrenCollapsible(true);
139
m_mainSplitter->addWidget(m_groupSplitter);
140
m_mainSplitter->addWidget(rightHandSideWidget);
141
m_mainSplitter->setStretchFactor(0, 30);
142
m_mainSplitter->setStretchFactor(1, 70);
144
m_previewSplitter->setOrientation(Qt::Vertical);
145
m_previewSplitter->setChildrenCollapsible(true);
147
m_groupView->setObjectName("groupView");
148
m_groupView->setContextMenuPolicy(Qt::CustomContextMenu);
149
connect(m_groupView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(emitGroupContextMenuRequested(QPoint)));
151
m_entryView->setObjectName("entryView");
152
m_entryView->setContextMenuPolicy(Qt::CustomContextMenu);
153
m_entryView->displayGroup(m_db->rootGroup());
154
connect(m_entryView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(emitEntryContextMenuRequested(QPoint)));
156
// Add a notification for when we are searching
157
m_searchingLabel->setObjectName("SearchBanner");
158
m_searchingLabel->setText(tr("Searching…"));
159
m_searchingLabel->setAlignment(Qt::AlignCenter);
160
m_searchingLabel->setVisible(false);
162
#ifdef WITH_XC_KEESHARE
163
m_shareLabel->setObjectName("KeeShareBanner");
164
m_shareLabel->setRawText(tr("Shared group…"));
165
m_shareLabel->setAlignment(Qt::AlignCenter);
166
m_shareLabel->setVisible(false);
169
m_previewView->setObjectName("previewWidget");
170
m_previewView->hide();
171
m_previewSplitter->addWidget(m_entryView);
172
m_previewSplitter->addWidget(m_previewView);
173
m_previewSplitter->setStretchFactor(0, 100);
174
m_previewSplitter->setStretchFactor(1, 0);
175
m_previewSplitter->setSizes({1, 1});
177
m_editEntryWidget->setObjectName("editEntryWidget");
178
m_editGroupWidget->setObjectName("editGroupWidget");
179
m_reportsDialog->setObjectName("reportsDialog");
180
m_databaseSettingDialog->setObjectName("databaseSettingsDialog");
181
m_databaseOpenWidget->setObjectName("databaseOpenWidget");
183
addChildWidget(m_mainWidget);
184
addChildWidget(m_editEntryWidget);
185
addChildWidget(m_editGroupWidget);
186
addChildWidget(m_reportsDialog);
187
addChildWidget(m_databaseSettingDialog);
188
addChildWidget(m_historyEditEntryWidget);
189
addChildWidget(m_databaseOpenWidget);
192
connect(m_mainSplitter, SIGNAL(splitterMoved(int,int)), SIGNAL(splitterSizesChanged()));
193
connect(m_groupSplitter, SIGNAL(splitterMoved(int,int)), SIGNAL(splitterSizesChanged()));
194
connect(m_previewSplitter, SIGNAL(splitterMoved(int,int)), SIGNAL(splitterSizesChanged()));
195
connect(this, SIGNAL(currentModeChanged(DatabaseWidget::Mode)), m_previewView, SLOT(setDatabaseMode(DatabaseWidget::Mode)));
196
connect(m_previewView, SIGNAL(entryUrlActivated(Entry*)), SLOT(openUrlForEntry(Entry*)));
197
connect(m_entryView, SIGNAL(viewStateChanged()), SIGNAL(entryViewStateChanged()));
198
connect(m_groupView, SIGNAL(groupSelectionChanged()), SLOT(onGroupChanged()));
199
connect(m_groupView, &GroupView::groupFocused, this, [this] { m_previewView->setGroup(currentGroup()); });
200
connect(m_entryView, SIGNAL(entryActivated(Entry*,EntryModel::ModelColumn)),
201
SLOT(entryActivationSignalReceived(Entry*,EntryModel::ModelColumn)));
202
connect(m_entryView, SIGNAL(entrySelectionChanged(Entry*)), SLOT(onEntryChanged(Entry*)));
203
connect(m_editEntryWidget, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
204
connect(m_editEntryWidget, SIGNAL(historyEntryActivated(Entry*)), SLOT(switchToHistoryView(Entry*)));
205
connect(m_historyEditEntryWidget, SIGNAL(editFinished(bool)), SLOT(switchBackToEntryEdit()));
206
connect(m_editGroupWidget, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
207
connect(m_reportsDialog, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
208
connect(m_databaseSettingDialog, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
209
connect(m_databaseOpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool)));
210
connect(this, SIGNAL(currentChanged(int)), SLOT(emitCurrentModeChanged()));
211
connect(this, SIGNAL(requestGlobalAutoType(const QString&)), parent, SLOT(performGlobalAutoType(const QString&)));
214
connectDatabaseSignals();
216
m_blockAutoSave = false;
218
m_autosaveTimer = new QTimer(this);
219
m_autosaveTimer->setSingleShot(true);
220
connect(m_autosaveTimer, SIGNAL(timeout()), this, SLOT(onAutosaveDelayTimeout()));
222
m_searchLimitGroup = config()->get(Config::SearchLimitGroup).toBool();
224
#ifdef WITH_XC_KEESHARE
225
// We need to reregister the database to allow exports
226
// from a newly created database
227
KeeShare::instance()->connectDatabase(m_db, {});
230
if (m_db->isInitialized()) {
233
switchToOpenDatabase();
237
DatabaseWidget::DatabaseWidget(const QString& filePath, QWidget* parent)
238
: DatabaseWidget(QSharedPointer<Database>::create(filePath), parent)
242
DatabaseWidget::~DatabaseWidget()
244
// Trigger any Database deletion related signals manually by
245
// explicitly clearing the Database pointer, instead of leaving it to ~QSharedPointer.
246
// QSharedPointer may behave differently depending on whether it is cleared by the `clear` method
247
// or by its destructor. In the latter case, the ref counter may not be correctly maintained
248
// if a copy of the QSharedPointer is created in any slots activated by the Database destructor.
249
// More details: https://github.com/keepassxreboot/keepassxc/issues/6393.
253
QSharedPointer<Database> DatabaseWidget::database() const
258
DatabaseWidget::Mode DatabaseWidget::currentMode() const
260
if (currentWidget() == nullptr) {
262
} else if (currentWidget() == m_mainWidget) {
263
return Mode::ViewMode;
264
} else if (currentWidget() == m_databaseOpenWidget) {
265
return Mode::LockedMode;
267
return Mode::EditMode;
271
bool DatabaseWidget::isLocked() const
273
return currentMode() == Mode::LockedMode;
276
bool DatabaseWidget::isSaving() const
278
return m_db->isSaving();
281
bool DatabaseWidget::isSorted() const
283
return m_entryView->isSorted();
286
bool DatabaseWidget::isSearchActive() const
288
return m_entryView->inSearchMode();
291
bool DatabaseWidget::isEntryViewActive() const
293
return currentWidget() == m_mainWidget;
296
bool DatabaseWidget::isEntryEditActive() const
298
return currentWidget() == m_editEntryWidget;
301
bool DatabaseWidget::isGroupEditActive() const
303
return currentWidget() == m_editGroupWidget;
306
bool DatabaseWidget::isEditWidgetModified() const
308
if (currentWidget() == m_editEntryWidget) {
309
return m_editEntryWidget->isModified();
310
} else if (currentWidget() == m_editGroupWidget) {
311
return m_editGroupWidget->isModified();
316
QString DatabaseWidget::displayName() const
322
auto displayName = m_db->metadata()->name();
323
if (!m_db->filePath().isEmpty()) {
324
if (displayName.isEmpty()) {
325
displayName = displayFileName();
328
if (displayName.isEmpty()) {
329
displayName = tr("New Database");
331
displayName = tr("%1 [New Database]", "Database tab name modifier").arg(displayName);
338
QString DatabaseWidget::displayFileName() const
341
QFileInfo fileinfo(m_db->filePath());
342
return fileinfo.fileName();
347
QString DatabaseWidget::displayFilePath() const
350
return m_db->canonicalFilePath();
355
QHash<Config::ConfigKey, QList<int>> DatabaseWidget::splitterSizes() const
357
return {{Config::GUI_SplitterState, m_mainSplitter->sizes()},
358
{Config::GUI_PreviewSplitterState, m_previewSplitter->sizes()},
359
{Config::GUI_GroupSplitterState, m_groupSplitter->sizes()}};
362
void DatabaseWidget::setSplitterSizes(const QHash<Config::ConfigKey, QList<int>>& sizes)
364
for (auto itr = sizes.constBegin(); itr != sizes.constEnd(); ++itr) {
365
// Less than two sizes indicates an invalid value
366
if (itr.value().size() < 2) {
370
case Config::GUI_SplitterState:
371
m_mainSplitter->setSizes(itr.value());
373
case Config::GUI_PreviewSplitterState:
374
m_previewSplitter->setSizes(itr.value());
376
case Config::GUI_GroupSplitterState:
377
m_groupSplitter->setSizes(itr.value());
385
void DatabaseWidget::setSearchStringForAutoType(const QString& search)
387
m_searchStringForAutoType = search;
391
* Get current view state of entry view
393
QByteArray DatabaseWidget::entryViewState() const
395
return m_entryView->viewState();
399
* Set view state of entry view
401
bool DatabaseWidget::setEntryViewState(const QByteArray& state) const
403
return m_entryView->setViewState(state);
406
void DatabaseWidget::clearAllWidgets()
408
m_editEntryWidget->clear();
409
m_historyEditEntryWidget->clear();
410
m_editGroupWidget->clear();
411
m_previewView->clear();
414
void DatabaseWidget::emitCurrentModeChanged()
416
emit currentModeChanged(currentMode());
419
void DatabaseWidget::createEntry()
421
Q_ASSERT(m_groupView->currentGroup());
422
if (!m_groupView->currentGroup()) {
426
m_newEntry.reset(new Entry());
428
m_newEntry->setUuid(QUuid::createUuid());
429
m_newEntry->setUsername(m_db->metadata()->defaultUserName());
430
m_newParent = m_groupView->currentGroup();
431
m_newParent->applyGroupIconOnCreateTo(m_newEntry.data());
432
switchToEntryEdit(m_newEntry.data(), true);
435
void DatabaseWidget::replaceDatabase(QSharedPointer<Database> db)
437
Q_ASSERT(!isEntryEditActive() && !isGroupEditActive());
439
// Save off new parent UUID which will be valid when creating a new entry
442
newParentUuid = m_newParent->uuid();
445
// TODO: instead of increasing the ref count temporarily, there should be a clean
446
// break from the old database. Without this crashes occur due to the change
447
// signals triggering dangling pointers.
449
m_db = std::move(db);
450
connectDatabaseSignals();
451
m_groupView->changeDatabase(m_db);
452
m_tagView->setDatabase(m_db);
454
// Restore the new parent group pointer, if not found default to the root group
455
// this prevents data loss when merging a database while creating a new entry
456
if (!newParentUuid.isNull()) {
457
m_newParent = m_db->rootGroup()->findGroupByUuid(newParentUuid);
459
m_newParent = m_db->rootGroup();
463
emit databaseReplaced(oldDb, m_db);
465
#if defined(WITH_XC_KEESHARE)
466
KeeShare::instance()->connectDatabase(m_db, oldDb);
468
// Keep the instance active till the end of this function
472
oldDb->releaseData();
475
void DatabaseWidget::cloneEntry()
477
auto currentEntry = currentSelectedEntry();
478
Q_ASSERT(currentEntry);
483
auto cloneDialog = new CloneDialog(this, m_db.data(), currentEntry);
484
connect(cloneDialog, &CloneDialog::entryCloned, this, [this](auto entry) {
486
m_entryView->setCurrentEntry(entry);
492
void DatabaseWidget::showTotp()
494
auto currentEntry = currentSelectedEntry();
495
Q_ASSERT(currentEntry);
500
auto totpDialog = new TotpDialog(this, currentEntry);
501
connect(this, &DatabaseWidget::databaseLockRequested, totpDialog, &TotpDialog::close);
505
void DatabaseWidget::copyTotp()
507
auto currentEntry = currentSelectedEntry();
508
Q_ASSERT(currentEntry);
512
setClipboardTextAndMinimize(currentEntry->totp());
515
void DatabaseWidget::setupTotp()
517
auto currentEntry = currentSelectedEntry();
518
Q_ASSERT(currentEntry);
523
auto setupTotpDialog = new TotpSetupDialog(this, currentEntry);
524
connect(setupTotpDialog, SIGNAL(totpUpdated()), SIGNAL(entrySelectionChanged()));
525
if (currentWidget() == m_editEntryWidget) {
526
// Entry is being edited, tell it when we are finished updating TOTP
527
connect(setupTotpDialog, SIGNAL(totpUpdated()), m_editEntryWidget, SLOT(updateTotp()));
529
connect(this, &DatabaseWidget::databaseLockRequested, setupTotpDialog, &TotpSetupDialog::close);
530
setupTotpDialog->open();
533
void DatabaseWidget::deleteSelectedEntries()
535
const QModelIndexList selected = m_entryView->selectionModel()->selectedRows();
536
if (selected.isEmpty()) {
540
// Resolve entries from the selection model
541
QList<Entry*> selectedEntries;
542
for (const QModelIndex& index : selected) {
543
selectedEntries.append(m_entryView->entryFromIndex(index));
546
deleteEntries(std::move(selectedEntries));
549
void DatabaseWidget::restoreSelectedEntries()
551
const QModelIndexList selected = m_entryView->selectionModel()->selectedRows();
552
if (selected.isEmpty()) {
556
// Resolve entries from the selection model
557
QList<Entry*> selectedEntries;
558
for (auto& index : selected) {
559
selectedEntries.append(m_entryView->entryFromIndex(index));
562
for (auto* entry : selectedEntries) {
563
if (entry->previousParentGroup()) {
564
entry->setGroup(entry->previousParentGroup());
569
void DatabaseWidget::deleteEntries(QList<Entry*> selectedEntries, bool confirm)
571
if (selectedEntries.isEmpty()) {
575
// Find the index above the first entry for selection after deletion
576
auto index = m_entryView->indexFromEntry(selectedEntries.first());
577
index = m_entryView->indexAbove(index);
579
// Confirm entry removal before moving forward
580
auto recycleBin = m_db->metadata()->recycleBin();
581
bool permanent = (recycleBin && recycleBin->findEntryByUuid(selectedEntries.first()->uuid()))
582
|| !m_db->metadata()->recycleBinEnabled();
584
if (confirm && !GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) {
588
GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent);
590
// Select the row above the deleted entries
591
if (index.isValid()) {
592
m_entryView->setCurrentIndex(index);
594
m_entryView->setFirstEntryActive();
598
void DatabaseWidget::setFocus(Qt::FocusReason reason)
600
focusNextPrevChild(reason == Qt::TabFocusReason);
603
void DatabaseWidget::focusOnEntries(bool editIfFocused)
605
if (isEntryViewActive()) {
606
if (editIfFocused && m_entryView->hasFocus()) {
609
m_entryView->setFocus();
614
void DatabaseWidget::focusOnGroups(bool editIfFocused)
616
if (isEntryViewActive()) {
617
if (editIfFocused && m_groupView->hasFocus()) {
620
m_groupView->setFocus();
625
void DatabaseWidget::moveEntryUp()
627
auto currentEntry = currentSelectedEntry();
629
currentEntry->moveUp();
630
m_entryView->setCurrentEntry(currentEntry);
634
void DatabaseWidget::moveEntryDown()
636
auto currentEntry = currentSelectedEntry();
638
currentEntry->moveDown();
639
m_entryView->setCurrentEntry(currentEntry);
643
void DatabaseWidget::copyTitle()
645
auto currentEntry = currentSelectedEntry();
647
setClipboardTextAndMinimize(currentEntry->resolveMultiplePlaceholders(currentEntry->title()));
651
void DatabaseWidget::copyUsername()
653
auto currentEntry = currentSelectedEntry();
655
setClipboardTextAndMinimize(currentEntry->resolveMultiplePlaceholders(currentEntry->username()));
659
void DatabaseWidget::copyPassword()
661
// Some platforms do not properly trap Ctrl+C copy shortcut
662
// if a text edit or label has focus pass the copy operation to it
664
bool clearClipboard = config()->get(Config::Security_ClearClipboard).toBool();
666
auto plainTextEdit = qobject_cast<QPlainTextEdit*>(focusWidget());
667
if (plainTextEdit && plainTextEdit->textCursor().hasSelection()) {
668
clipboard()->setText(plainTextEdit->textCursor().selectedText(), clearClipboard);
672
auto label = qobject_cast<QLabel*>(focusWidget());
673
if (label && label->hasSelectedText()) {
674
clipboard()->setText(label->selectedText(), clearClipboard);
678
auto textEdit = qobject_cast<QTextEdit*>(focusWidget());
679
if (textEdit && textEdit->textCursor().hasSelection()) {
680
clipboard()->setText(textEdit->textCursor().selection().toPlainText(), clearClipboard);
684
auto currentEntry = currentSelectedEntry();
686
setClipboardTextAndMinimize(currentEntry->resolveMultiplePlaceholders(currentEntry->password()));
690
void DatabaseWidget::copyPasswordTotp()
692
auto currentEntry = currentSelectedEntry();
694
setClipboardTextAndMinimize(
695
currentEntry->resolveMultiplePlaceholders(currentEntry->password()).append(currentEntry->totp()));
699
void DatabaseWidget::copyURL()
701
auto currentEntry = currentSelectedEntry();
703
setClipboardTextAndMinimize(currentEntry->resolveMultiplePlaceholders(currentEntry->url()));
707
void DatabaseWidget::copyNotes()
709
auto currentEntry = currentSelectedEntry();
711
setClipboardTextAndMinimize(currentEntry->resolveMultiplePlaceholders(currentEntry->notes()));
715
void DatabaseWidget::copyAttribute(QAction* action)
717
auto currentEntry = currentSelectedEntry();
719
setClipboardTextAndMinimize(
720
currentEntry->resolveMultiplePlaceholders(currentEntry->attributes()->value(action->data().toString())));
724
void DatabaseWidget::filterByTag()
726
QStringList searchTerms;
727
const auto selections = m_tagView->selectionModel()->selectedIndexes();
728
for (const auto& index : selections) {
729
searchTerms << index.data(Qt::UserRole).toString();
731
emit requestSearch(searchTerms.join(" "));
734
void DatabaseWidget::setTag(QAction* action)
736
auto tag = action->text();
737
auto state = action->isChecked();
738
for (auto entry : m_entryView->selectedEntries()) {
739
state ? entry->addTag(tag) : entry->removeTag(tag);
743
void DatabaseWidget::showTotpKeyQrCode()
745
auto currentEntry = currentSelectedEntry();
747
auto totpDisplayDialog = new TotpExportSettingsDialog(this, currentEntry);
748
connect(this, &DatabaseWidget::databaseLockRequested, totpDisplayDialog, &TotpExportSettingsDialog::close);
749
totpDisplayDialog->open();
753
void DatabaseWidget::setClipboardTextAndMinimize(const QString& text)
755
clipboard()->setText(text);
756
if (config()->get(Config::HideWindowOnCopy).toBool()) {
757
if (config()->get(Config::MinimizeOnCopy).toBool()) {
758
getMainWindow()->minimizeOrHide();
759
} else if (config()->get(Config::DropToBackgroundOnCopy).toBool()) {
765
#ifdef WITH_XC_SSHAGENT
766
void DatabaseWidget::addToAgent()
768
Entry* currentEntry = m_entryView->currentEntry();
769
Q_ASSERT(currentEntry);
774
KeeAgentSettings settings;
775
if (!settings.fromEntry(currentEntry)) {
779
SSHAgent* agent = SSHAgent::instance();
781
if (settings.toOpenSSHKey(currentEntry, key, true)) {
782
if (!agent->addIdentity(key, settings, database()->uuid())) {
783
m_messageWidget->showMessage(agent->errorString(), MessageWidget::Error);
786
m_messageWidget->showMessage(settings.errorString(), MessageWidget::Error);
790
void DatabaseWidget::removeFromAgent()
792
Entry* currentEntry = m_entryView->currentEntry();
793
Q_ASSERT(currentEntry);
798
KeeAgentSettings settings;
799
if (!settings.fromEntry(currentEntry)) {
803
SSHAgent* agent = SSHAgent::instance();
805
if (settings.toOpenSSHKey(currentEntry, key, false)) {
806
if (!agent->removeIdentity(key)) {
807
m_messageWidget->showMessage(agent->errorString(), MessageWidget::Error);
810
m_messageWidget->showMessage(settings.errorString(), MessageWidget::Error);
815
void DatabaseWidget::performAutoType(const QString& sequence)
817
auto currentEntry = currentSelectedEntry();
819
// TODO: Include name of previously active window in confirmation question
820
if (config()->get(Config::Security_AutoTypeAsk).toBool()
821
&& MessageBox::question(
822
this, tr("Confirm Auto-Type"), tr("Perform Auto-Type into the previously active window?"))
823
!= MessageBox::Yes) {
827
if (sequence.isEmpty()) {
828
autoType()->performAutoType(currentEntry);
830
autoType()->performAutoTypeWithSequence(currentEntry, sequence);
835
void DatabaseWidget::performAutoTypeUsername()
837
performAutoType(QStringLiteral("{USERNAME}"));
840
void DatabaseWidget::performAutoTypeUsernameEnter()
842
performAutoType(QStringLiteral("{USERNAME}{ENTER}"));
845
void DatabaseWidget::performAutoTypePassword()
847
performAutoType(QStringLiteral("{PASSWORD}"));
850
void DatabaseWidget::performAutoTypePasswordEnter()
852
performAutoType(QStringLiteral("{PASSWORD}{ENTER}"));
855
void DatabaseWidget::performAutoTypeTOTP()
857
performAutoType(QStringLiteral("{TOTP}"));
860
void DatabaseWidget::openUrl()
862
auto currentEntry = currentSelectedEntry();
864
openUrlForEntry(currentEntry);
868
void DatabaseWidget::downloadSelectedFavicons()
870
#ifdef WITH_XC_NETWORKING
871
QList<Entry*> selectedEntries;
872
for (const auto& index : m_entryView->selectionModel()->selectedRows()) {
873
selectedEntries.append(m_entryView->entryFromIndex(index));
876
// Force download even if icon already exists
877
performIconDownloads(selectedEntries, true);
881
void DatabaseWidget::downloadAllFavicons()
883
#ifdef WITH_XC_NETWORKING
884
auto currentGroup = m_groupView->currentGroup();
886
performIconDownloads(currentGroup->entries());
891
void DatabaseWidget::downloadFaviconInBackground(Entry* entry)
893
#ifdef WITH_XC_NETWORKING
894
performIconDownloads({entry}, true, true);
900
void DatabaseWidget::performIconDownloads(const QList<Entry*>& entries, bool force, bool downloadInBackground)
902
#ifdef WITH_XC_NETWORKING
903
auto* iconDownloaderDialog = new IconDownloaderDialog(this);
904
connect(this, SIGNAL(databaseLockRequested()), iconDownloaderDialog, SLOT(close()));
906
if (downloadInBackground && entries.count() > 0) {
907
iconDownloaderDialog->downloadFaviconInBackground(m_db, entries.first());
909
iconDownloaderDialog->downloadFavicons(m_db, entries, force);
914
Q_UNUSED(downloadInBackground);
918
void DatabaseWidget::openUrlForEntry(Entry* entry)
925
QString cmdString = entry->resolveMultiplePlaceholders(entry->url());
926
if (cmdString.startsWith("cmd://")) {
927
// check if decision to execute command was stored
928
bool launch = (entry->attributes()->value(EntryAttributes::RememberCmdExecAttr) == "1");
930
// otherwise ask user
931
if (!launch && cmdString.length() > 6) {
932
QString cmdTruncated = entry->resolveMultiplePlaceholders(entry->maskPasswordPlaceholders(entry->url()));
933
cmdTruncated = cmdTruncated.mid(6);
934
if (cmdTruncated.length() > 400) {
935
cmdTruncated = cmdTruncated.left(400) + " […]";
937
QMessageBox msgbox(QMessageBox::Icon::Question,
938
tr("Execute command?"),
939
tr("Do you really want to execute the following command?<br><br>%1<br>")
940
.arg(cmdTruncated.toHtmlEscaped()),
941
QMessageBox::Yes | QMessageBox::No,
943
msgbox.setDefaultButton(QMessageBox::No);
945
auto checkbox = new QCheckBox(tr("Remember my choice"), &msgbox);
946
msgbox.setCheckBox(checkbox);
947
bool remember = false;
948
QObject::connect(checkbox, &QCheckBox::stateChanged, [&](int state) {
949
if (static_cast<Qt::CheckState>(state) == Qt::CheckState::Checked) {
954
int result = msgbox.exec();
955
launch = (result == QMessageBox::Yes);
958
entry->attributes()->set(EntryAttributes::RememberCmdExecAttr, result == QMessageBox::Yes ? "1" : "0");
963
QProcess::startDetached(cmdString.mid(6));
965
if (config()->get(Config::MinimizeOnOpenUrl).toBool()) {
966
getMainWindow()->minimizeOrHide();
969
} else if (cmdString.startsWith("kdbx://")) {
970
openDatabaseFromEntry(entry, false);
972
QUrl url = QUrl::fromUserInput(entry->resolveMultiplePlaceholders(entry->url()));
973
if (!url.isEmpty()) {
974
#ifdef KEEPASSXC_DIST_APPIMAGE
975
QProcess::execute("xdg-open", {url.toString(QUrl::FullyEncoded)});
977
QDesktopServices::openUrl(url);
980
if (config()->get(Config::MinimizeOnOpenUrl).toBool()) {
981
getMainWindow()->minimizeOrHide();
987
Entry* DatabaseWidget::currentSelectedEntry()
989
if (currentWidget() == m_editEntryWidget) {
990
return m_editEntryWidget->currentEntry();
993
return m_entryView->currentEntry();
996
void DatabaseWidget::createGroup()
998
Q_ASSERT(m_groupView->currentGroup());
999
if (!m_groupView->currentGroup()) {
1003
m_newGroup.reset(new Group());
1004
m_newGroup->setUuid(QUuid::createUuid());
1005
m_newParent = m_groupView->currentGroup();
1006
switchToGroupEdit(m_newGroup.data(), true);
1009
void DatabaseWidget::cloneGroup()
1011
Group* currentGroup = m_groupView->currentGroup();
1012
Q_ASSERT(currentGroup && canCloneCurrentGroup());
1013
if (!currentGroup || !canCloneCurrentGroup()) {
1017
m_newGroup.reset(currentGroup->clone(Entry::CloneCopy, Group::CloneDefault | Group::CloneRenameTitle));
1018
m_newParent = currentGroup->parentGroup();
1019
switchToGroupEdit(m_newGroup.data(), true);
1022
void DatabaseWidget::deleteGroup()
1024
Group* currentGroup = m_groupView->currentGroup();
1025
Q_ASSERT(currentGroup && canDeleteCurrentGroup());
1026
if (!currentGroup || !canDeleteCurrentGroup()) {
1030
auto* recycleBin = m_db->metadata()->recycleBin();
1031
bool inRecycleBin = recycleBin && recycleBin->findGroupByUuid(currentGroup->uuid());
1032
bool isRecycleBin = recycleBin && (currentGroup == recycleBin);
1033
bool isRecycleBinSubgroup = recycleBin && currentGroup->findGroupByUuid(recycleBin->uuid());
1034
if (inRecycleBin || isRecycleBin || isRecycleBinSubgroup || !m_db->metadata()->recycleBinEnabled()) {
1035
auto result = MessageBox::question(
1038
tr("Do you really want to delete the group \"%1\" for good?").arg(currentGroup->name().toHtmlEscaped()),
1039
MessageBox::Delete | MessageBox::Cancel,
1040
MessageBox::Cancel);
1042
if (result == MessageBox::Delete) {
1043
delete currentGroup;
1046
auto result = MessageBox::question(this,
1047
tr("Move group to recycle bin?"),
1048
tr("Do you really want to move the group "
1049
"\"%1\" to the recycle bin?")
1050
.arg(currentGroup->name().toHtmlEscaped()),
1051
MessageBox::Move | MessageBox::Cancel,
1052
MessageBox::Cancel);
1053
if (result == MessageBox::Move) {
1054
m_db->recycleGroup(currentGroup);
1059
int DatabaseWidget::addChildWidget(QWidget* w)
1061
w->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
1062
int index = QStackedWidget::addWidget(w);
1067
void DatabaseWidget::switchToMainView(bool previousDialogAccepted)
1069
setCurrentWidget(m_mainWidget);
1072
if (previousDialogAccepted) {
1073
m_newGroup->setParent(m_newParent);
1074
m_groupView->setCurrentGroup(m_newGroup.take());
1075
m_groupView->expandGroup(m_newParent);
1080
m_newParent = nullptr;
1081
} else if (m_newEntry) {
1082
if (previousDialogAccepted) {
1083
m_newEntry->setGroup(m_newParent);
1084
m_entryView->setFocus();
1085
m_entryView->setCurrentEntry(m_newEntry.take());
1090
m_newParent = nullptr;
1092
// Workaround: ensure entries are focused so search doesn't reset
1093
m_entryView->setFocus();
1097
void DatabaseWidget::switchToHistoryView(Entry* entry)
1099
auto entryTitle = m_editEntryWidget->currentEntry() ? m_editEntryWidget->currentEntry()->title() : "";
1100
m_historyEditEntryWidget->loadEntry(entry, false, true, entryTitle, m_db);
1101
setCurrentWidget(m_historyEditEntryWidget);
1104
void DatabaseWidget::switchBackToEntryEdit()
1106
setCurrentWidget(m_editEntryWidget);
1109
void DatabaseWidget::switchToEntryEdit(Entry* entry)
1111
switchToEntryEdit(entry, false);
1114
void DatabaseWidget::switchToEntryEdit(Entry* entry, bool create)
1116
// If creating an entry, it will be in `currentGroup()` so it's
1117
// okay to use but when editing, the entry may not be in
1118
// `currentGroup()` so we get the entry's group.
1121
group = currentGroup();
1123
group = entry->group();
1124
// Ensure we have only this entry selected
1125
m_entryView->setCurrentEntry(entry);
1130
// Setup the entry edit widget and display
1131
m_editEntryWidget->loadEntry(entry, create, false, group->name(), m_db);
1132
setCurrentWidget(m_editEntryWidget);
1135
void DatabaseWidget::switchToGroupEdit(Group* group, bool create)
1137
m_editGroupWidget->loadGroup(group, create, m_db);
1138
setCurrentWidget(m_editGroupWidget);
1141
void DatabaseWidget::connectDatabaseSignals()
1143
// relayed Database events
1144
connect(m_db.data(),
1145
SIGNAL(filePathChanged(QString, QString)),
1147
SIGNAL(databaseFilePathChanged(QString, QString)));
1148
connect(m_db.data(), &Database::modified, this, &DatabaseWidget::databaseModified);
1149
connect(m_db.data(), &Database::modified, this, &DatabaseWidget::onDatabaseModified);
1150
connect(m_db.data(), &Database::databaseSaved, this, &DatabaseWidget::databaseSaved);
1151
connect(m_db.data(), &Database::databaseFileChanged, this, &DatabaseWidget::reloadDatabaseFile);
1152
connect(m_db.data(), &Database::databaseNonDataChanged, this, &DatabaseWidget::databaseNonDataChanged);
1153
connect(m_db.data(), &Database::databaseNonDataChanged, this, &DatabaseWidget::onDatabaseNonDataChanged);
1156
void DatabaseWidget::loadDatabase(bool accepted)
1158
auto* openWidget = qobject_cast<DatabaseOpenWidget*>(sender());
1159
Q_ASSERT(openWidget);
1165
replaceDatabase(openWidget->database());
1169
restoreGroupEntryFocus(m_groupBeforeLock, m_entryBeforeLock);
1171
// Only show expired entries if first unlock and option is enabled
1172
if (m_groupBeforeLock.isNull() && config()->get(Config::GUI_ShowExpiredEntriesOnDatabaseUnlock).toBool()) {
1173
int expirationOffset = config()->get(Config::GUI_ShowExpiredEntriesOnDatabaseUnlockOffsetDays).toInt();
1174
if (expirationOffset <= 0) {
1175
m_nextSearchLabelText = tr("Expired entries");
1177
m_nextSearchLabelText =
1178
tr("Entries expiring within %1 day(s)", "", expirationOffset).arg(expirationOffset);
1180
requestSearch(QString("is:expired-%1").arg(expirationOffset));
1183
m_groupBeforeLock = QUuid();
1184
m_entryBeforeLock = QUuid();
1186
emit databaseUnlocked();
1187
#ifdef WITH_XC_SSHAGENT
1188
sshAgent()->databaseUnlocked(m_db);
1190
if (config()->get(Config::MinimizeAfterUnlock).toBool()) {
1191
getMainWindow()->minimizeOrHide();
1194
if (m_databaseOpenWidget->database()) {
1195
m_databaseOpenWidget->database().reset();
1197
emit closeRequest();
1201
void DatabaseWidget::mergeDatabase(bool accepted)
1205
showMessage(tr("No current database."), MessageWidget::Error);
1209
auto* senderDialog = qobject_cast<DatabaseOpenDialog*>(sender());
1211
Q_ASSERT(senderDialog);
1212
if (!senderDialog) {
1215
auto srcDb = senderDialog->database();
1218
showMessage(tr("No source database, nothing to do."), MessageWidget::Error);
1222
Merger merger(srcDb.data(), m_db.data());
1223
QStringList changeList = merger.merge();
1225
if (!changeList.isEmpty()) {
1226
showMessage(tr("Successfully merged the database files."), MessageWidget::Information);
1228
showMessage(tr("Database was not modified by merge operation."), MessageWidget::Information);
1233
emit databaseMerged(m_db);
1237
* Unlock the database.
1239
* @param accepted true if the unlock dialog or widget was confirmed with OK
1241
void DatabaseWidget::unlockDatabase(bool accepted)
1243
auto* senderDialog = qobject_cast<DatabaseOpenDialog*>(sender());
1246
if (!senderDialog && (!m_db || !m_db->isInitialized())) {
1247
emit closeRequest();
1252
if (senderDialog && senderDialog->intent() == DatabaseOpenDialog::Intent::Merge) {
1253
mergeDatabase(accepted);
1257
QSharedPointer<Database> db;
1259
db = senderDialog->database();
1261
db = m_databaseOpenWidget->database();
1263
replaceDatabase(db);
1265
restoreGroupEntryFocus(m_groupBeforeLock, m_entryBeforeLock);
1266
m_groupBeforeLock = QUuid();
1267
m_entryBeforeLock = QUuid();
1271
emit databaseUnlocked();
1273
#ifdef WITH_XC_SSHAGENT
1274
sshAgent()->databaseUnlocked(m_db);
1277
if (config()->get(Config::MinimizeAfterUnlock).toBool()) {
1278
getMainWindow()->minimizeOrHide();
1281
if (senderDialog && senderDialog->intent() == DatabaseOpenDialog::Intent::AutoType) {
1282
// Rather than starting AutoType directly for this database, signal the parent DatabaseTabWidget to
1283
// restart AutoType now that this database is unlocked, so that other open+unlocked databases
1284
// can be included in the search.
1285
emit requestGlobalAutoType(m_searchStringForAutoType);
1289
void DatabaseWidget::entryActivationSignalReceived(Entry* entry, EntryModel::ModelColumn column)
1296
// Implement 'copy-on-doubleclick' functionality for certain columns
1298
case EntryModel::Username:
1299
if (config()->get(Config::Security_EnableCopyOnDoubleClick).toBool()) {
1300
setClipboardTextAndMinimize(entry->resolveMultiplePlaceholders(entry->username()));
1302
switchToEntryEdit(entry);
1305
case EntryModel::Password:
1306
if (config()->get(Config::Security_EnableCopyOnDoubleClick).toBool()) {
1307
setClipboardTextAndMinimize(entry->resolveMultiplePlaceholders(entry->password()));
1309
switchToEntryEdit(entry);
1312
case EntryModel::Url:
1313
if (!entry->url().isEmpty()) {
1314
openUrlForEntry(entry);
1317
case EntryModel::Totp:
1318
if (entry->hasTotp()) {
1319
setClipboardTextAndMinimize(entry->totp());
1324
case EntryModel::ParentGroup:
1325
// Call this first to clear out of search mode, otherwise
1326
// the desired entry is not properly selected
1328
m_groupView->setCurrentGroup(entry->group());
1329
m_entryView->setCurrentEntry(entry);
1331
// TODO: switch to 'Notes' tab in details view/pane
1332
// case EntryModel::Notes:
1334
// TODO: switch to 'Attachments' tab in details view/pane
1335
// case EntryModel::Attachments:
1338
switchToEntryEdit(entry);
1342
void DatabaseWidget::switchToDatabaseReports()
1344
m_reportsDialog->load(m_db);
1345
setCurrentWidget(m_reportsDialog);
1348
void DatabaseWidget::switchToDatabaseSettings()
1350
m_databaseSettingDialog->load(m_db);
1351
setCurrentWidget(m_databaseSettingDialog);
1354
void DatabaseWidget::switchToOpenDatabase()
1356
if (currentWidget() != m_databaseOpenWidget || m_databaseOpenWidget->filename() != m_db->filePath()) {
1357
switchToOpenDatabase(m_db->filePath());
1361
void DatabaseWidget::switchToOpenDatabase(const QString& filePath)
1363
m_databaseOpenWidget->load(filePath);
1364
setCurrentWidget(m_databaseOpenWidget);
1367
void DatabaseWidget::switchToOpenDatabase(const QString& filePath, const QString& password, const QString& keyFile)
1369
switchToOpenDatabase(filePath);
1370
m_databaseOpenWidget->enterKey(password, keyFile);
1373
void DatabaseWidget::switchToEntryEdit()
1375
auto entry = m_entryView->currentEntry();
1380
switchToEntryEdit(entry, false);
1383
void DatabaseWidget::switchToGroupEdit()
1385
auto group = m_groupView->currentGroup();
1390
switchToGroupEdit(group, false);
1393
void DatabaseWidget::sortGroupsAsc()
1395
m_groupView->sortGroups();
1398
void DatabaseWidget::sortGroupsDesc()
1400
m_groupView->sortGroups(true);
1403
void DatabaseWidget::switchToDatabaseSecurity()
1405
switchToDatabaseSettings();
1406
m_databaseSettingDialog->showDatabaseKeySettings();
1409
#ifdef WITH_XC_BROWSER_PASSKEYS
1410
void DatabaseWidget::switchToPasskeys()
1412
switchToDatabaseReports();
1413
m_reportsDialog->activatePasskeysPage();
1416
void DatabaseWidget::showImportPasskeyDialog(bool isEntry)
1418
PasskeyImporter passkeyImporter(this);
1421
auto currentEntry = currentSelectedEntry();
1422
if (!currentEntry) {
1426
passkeyImporter.importPasskey(m_db, currentEntry);
1428
passkeyImporter.importPasskey(m_db);
1433
void DatabaseWidget::performUnlockDatabase(const QString& password, const QString& keyfile)
1435
if (password.isEmpty() && keyfile.isEmpty()) {
1439
if (!m_db->isInitialized() || isLocked()) {
1440
switchToOpenDatabase();
1441
m_databaseOpenWidget->enterKey(password, keyfile);
1445
void DatabaseWidget::refreshSearch()
1447
if (isSearchActive()) {
1448
auto selectedEntry = m_entryView->currentEntry();
1449
search(m_lastSearchText);
1450
// Re-select the previous entry if it is still in the search
1451
m_entryView->setCurrentEntry(selectedEntry);
1455
void DatabaseWidget::search(const QString& searchtext)
1457
if (searchtext.isEmpty()) {
1462
auto searchGroup = m_db->rootGroup();
1463
if (m_searchLimitGroup && m_nextSearchLabelText.isEmpty()) {
1464
searchGroup = currentGroup();
1467
auto results = m_entrySearcher->search(searchtext, searchGroup);
1469
// Display a label detailing our search results
1470
if (!m_nextSearchLabelText.isEmpty()) {
1471
// Custom searches don't display if there are no results
1472
if (results.isEmpty()) {
1476
m_searchingLabel->setText(m_nextSearchLabelText);
1477
m_nextSearchLabelText.clear();
1478
} else if (!results.isEmpty()) {
1479
m_searchingLabel->setText(tr("Search Results (%1)").arg(results.size()));
1481
m_searchingLabel->setText(tr("No Results"));
1484
emit searchModeAboutToActivate();
1486
m_entryView->displaySearch(results);
1487
m_lastSearchText = searchtext;
1489
m_searchingLabel->setVisible(true);
1490
#ifdef WITH_XC_KEESHARE
1491
m_shareLabel->setVisible(false);
1494
emit searchModeActivated();
1497
void DatabaseWidget::saveSearch(const QString& searchtext)
1499
if (!m_db->isInitialized()) {
1503
// Pull the existing searches and prepend an empty string to allow
1504
// the user to input a new search name without seeing the first one
1505
QStringList searches(m_db->metadata()->savedSearches().keys());
1506
searches.prepend("");
1508
QInputDialog dialog(this);
1509
connect(this, &DatabaseWidget::databaseLockRequested, &dialog, &QInputDialog::reject);
1511
dialog.setComboBoxEditable(true);
1512
dialog.setComboBoxItems(searches);
1513
dialog.setOkButtonText(tr("Save"));
1514
dialog.setLabelText(tr("Enter a unique name or overwrite an existing search from the list:"));
1515
dialog.setWindowTitle(tr("Save Search"));
1518
auto name = dialog.textValue();
1519
if (!name.isEmpty()) {
1520
m_db->metadata()->addSavedSearch(name, searchtext);
1524
void DatabaseWidget::deleteSearch(const QString& name)
1526
if (m_db->isInitialized()) {
1527
m_db->metadata()->deleteSavedSearch(name);
1531
void DatabaseWidget::setSearchCaseSensitive(bool state)
1533
m_entrySearcher->setCaseSensitive(state);
1537
void DatabaseWidget::setSearchLimitGroup(bool state)
1539
m_searchLimitGroup = state;
1543
void DatabaseWidget::onGroupChanged()
1545
auto group = m_groupView->currentGroup();
1547
// Intercept group changes if in search mode
1548
if (isSearchActive() && m_searchLimitGroup) {
1549
search(m_lastSearchText);
1552
m_entryView->displayGroup(group);
1555
m_previewView->setGroup(group);
1557
#ifdef WITH_XC_KEESHARE
1558
auto shareLabel = KeeShare::sharingLabel(group);
1559
if (!shareLabel.isEmpty()) {
1560
m_shareLabel->setRawText(shareLabel);
1561
m_shareLabel->setVisible(true);
1563
m_shareLabel->setVisible(false);
1567
emit groupChanged();
1570
void DatabaseWidget::onDatabaseModified()
1573
int autosaveDelayMs = m_db->metadata()->autosaveDelayMin() * 60 * 1000; // min to msec for QTimer
1574
bool autosaveAfterEveryChangeConfig = config()->get(Config::AutoSaveAfterEveryChange).toBool();
1575
if (autosaveDelayMs > 0 && autosaveAfterEveryChangeConfig) {
1576
// reset delay when modified
1577
m_autosaveTimer->start(autosaveDelayMs);
1580
if (!m_blockAutoSave && autosaveAfterEveryChangeConfig) {
1583
// Only block once, then reset
1584
m_blockAutoSave = false;
1588
void DatabaseWidget::onAutosaveDelayTimeout()
1590
const bool isAutosaveDelayEnabled = m_db->metadata()->autosaveDelayMin() > 0;
1591
const bool autosaveAfterEveryChangeConfig = config()->get(Config::AutoSaveAfterEveryChange).toBool();
1592
if (!(isAutosaveDelayEnabled && autosaveAfterEveryChangeConfig)) {
1593
// User might disable the delay/autosave while the timer is running
1596
if (!m_blockAutoSave) {
1599
// Only block once, then reset
1600
m_blockAutoSave = false;
1604
void DatabaseWidget::triggerAutosaveTimer()
1606
m_autosaveTimer->stop();
1607
QMetaObject::invokeMethod(m_autosaveTimer, "timeout");
1610
void DatabaseWidget::onDatabaseNonDataChanged()
1612
// Force mark the database modified if we are not auto-saving non-data changes
1613
if (!config()->get(Config::AutoSaveNonDataChanges).toBool()) {
1614
m_db->markAsModified();
1618
QString DatabaseWidget::getCurrentSearch()
1620
return m_lastSearchText;
1623
void DatabaseWidget::endSearch()
1625
if (isSearchActive()) {
1626
// Show the normal entry view of the current group
1627
emit listModeAboutToActivate();
1628
m_entryView->displayGroup(currentGroup());
1629
emit listModeActivated();
1630
m_entryView->setFirstEntryActive();
1631
// Enforce preview view update (prevents stale information if focus group is empty)
1632
m_previewView->setEntry(currentSelectedEntry());
1633
// Reset selection on tag view
1634
m_tagView->selectionModel()->clearSelection();
1637
m_searchingLabel->setVisible(false);
1638
m_searchingLabel->setText(tr("Searching…"));
1640
m_lastSearchText.clear();
1641
m_nextSearchLabelText.clear();
1643
// Tell the search widget to clear
1647
void DatabaseWidget::emitGroupContextMenuRequested(const QPoint& pos)
1649
emit groupContextMenuRequested(m_groupView->viewport()->mapToGlobal(pos));
1652
void DatabaseWidget::emitEntryContextMenuRequested(const QPoint& pos)
1654
emit entryContextMenuRequested(m_entryView->viewport()->mapToGlobal(pos));
1657
void DatabaseWidget::onEntryChanged(Entry* entry)
1660
m_previewView->setEntry(entry);
1662
m_previewView->setGroup(groupView()->currentGroup());
1665
emit entrySelectionChanged();
1668
bool DatabaseWidget::canCloneCurrentGroup() const
1670
bool isRootGroup = m_db->rootGroup() == m_groupView->currentGroup();
1671
// bool isRecycleBin = isRecycleBinSelected();
1673
return !isRootGroup;
1676
bool DatabaseWidget::canDeleteCurrentGroup() const
1678
bool isRootGroup = m_db->rootGroup() == m_groupView->currentGroup();
1679
return !isRootGroup;
1682
Group* DatabaseWidget::currentGroup() const
1684
return m_groupView->currentGroup();
1687
void DatabaseWidget::closeEvent(QCloseEvent* event)
1689
if (!lock() || m_databaseOpenWidget->unlockingDatabase()) {
1694
m_databaseOpenWidget->resetQuickUnlock();
1698
void DatabaseWidget::showEvent(QShowEvent* event)
1700
if (!m_db->isInitialized() || isLocked()) {
1701
switchToOpenDatabase();
1707
bool DatabaseWidget::focusNextPrevChild(bool next)
1709
// [parent] <-> GroupView <-> TagView <-> EntryView <-> EntryPreview <-> [parent]
1710
QList<QWidget*> sequence = {m_groupView, m_tagView, m_entryView, m_previewView};
1711
auto widget = qApp->focusWidget();
1713
return QStackedWidget::focusNextPrevChild(next);
1716
// Find the nearest parent widget in the sequence list
1719
idx = sequence.indexOf(widget);
1720
widget = widget->parentWidget();
1721
} while (idx == -1 && widget);
1723
// Determine next/previous or wrap around
1725
idx = next ? 0 : sequence.size() - 1;
1727
idx = next ? idx + 1 : idx - 1;
1730
// Find the next visible element in the sequence and set the focus
1731
while (idx >= 0 && idx < sequence.size()) {
1732
widget = sequence[idx];
1733
if (widget && widget->isVisible() && widget->isEnabled() && widget->height() > 0 && widget->width() > 0) {
1737
idx = next ? idx + 1 : idx - 1;
1740
// Ran out of options, defer to the parent widget
1741
return QStackedWidget::focusNextPrevChild(next);
1744
bool DatabaseWidget::lock()
1750
// Don't try to lock the database while saving, this will cause a deadlock
1751
if (m_db->isSaving()) {
1752
QTimer::singleShot(200, this, SLOT(lock()));
1756
emit databaseLockRequested();
1758
// Force close any modal widgets associated with this widget
1759
auto modalWidget = QApplication::activeModalWidget();
1761
auto parent = modalWidget->parentWidget();
1763
if (parent == this) {
1764
modalWidget->close();
1767
parent = parent->parentWidget();
1771
clipboard()->clearCopiedText();
1773
if (isEditWidgetModified()) {
1774
auto result = MessageBox::question(this,
1775
tr("Lock Database?"),
1776
tr("You are editing an entry. Discard changes and lock anyway?"),
1777
MessageBox::Discard | MessageBox::Cancel,
1778
MessageBox::Cancel);
1779
if (result == MessageBox::Cancel) {
1784
if (m_db->isModified()) {
1786
// Attempt to save on exit, but don't block locking if it fails
1787
if (config()->get(Config::AutoSaveOnExit).toBool()
1788
|| config()->get(Config::AutoSaveAfterEveryChange).toBool()) {
1794
if (!m_db->metadata()->name().toHtmlEscaped().isEmpty()) {
1795
msg = tr("\"%1\" was modified.\nSave changes?").arg(m_db->metadata()->name().toHtmlEscaped());
1797
msg = tr("Database was modified.\nSave changes?");
1799
auto result = MessageBox::question(this,
1800
tr("Save changes?"),
1802
MessageBox::Save | MessageBox::Discard | MessageBox::Cancel,
1804
if (result == MessageBox::Save) {
1808
} else if (result == MessageBox::Cancel) {
1812
} else if (m_db->hasNonDataChanges() && config()->get(Config::AutoSaveNonDataChanges).toBool()) {
1813
// Silently auto-save non-data changes, ignore errors
1814
QString errorMessage;
1815
performSave(errorMessage);
1818
if (m_groupView->currentGroup()) {
1819
m_groupBeforeLock = m_groupView->currentGroup()->uuid();
1821
m_groupBeforeLock = m_db->rootGroup()->uuid();
1824
auto currentEntry = currentSelectedEntry();
1826
m_entryBeforeLock = currentEntry->uuid();
1829
#ifdef WITH_XC_SSHAGENT
1830
sshAgent()->databaseLocked(m_db);
1835
switchToOpenDatabase(m_db->filePath());
1837
auto newDb = QSharedPointer<Database>::create(m_db->filePath());
1838
replaceDatabase(newDb);
1840
emit databaseLocked();
1845
void DatabaseWidget::reloadDatabaseFile()
1847
// Ignore reload if we are locked, saving, or currently editing an entry or group
1848
if (!m_db || isLocked() || isEntryEditActive() || isGroupEditActive() || isSaving()) {
1852
m_blockAutoSave = true;
1854
if (!config()->get(Config::AutoReloadOnChange).toBool()) {
1855
// Ask if we want to reload the db
1856
auto result = MessageBox::question(this,
1857
tr("File has changed"),
1858
tr("The database file has changed. Do you want to load the changes?"),
1859
MessageBox::Yes | MessageBox::No);
1861
if (result == MessageBox::No) {
1862
// Notify everyone the database does not match the file
1863
m_db->markAsModified();
1868
// Lock out interactions
1869
m_entryView->setDisabled(true);
1870
m_groupView->setDisabled(true);
1871
m_tagView->setDisabled(true);
1872
QApplication::processEvents();
1875
auto db = QSharedPointer<Database>::create(m_db->filePath());
1876
if (db->open(database()->key(), &error)) {
1877
if (m_db->isModified() || db->hasNonDataChanges()) {
1878
// Ask if we want to merge changes into new database
1879
auto result = MessageBox::question(
1881
tr("Merge Request"),
1882
tr("The database file has changed and you have unsaved changes.\nDo you want to merge your changes?"),
1883
MessageBox::Merge | MessageBox::Discard,
1886
if (result == MessageBox::Merge) {
1887
// Merge the old database into the new one
1888
Merger merger(m_db.data(), db.data());
1893
QUuid groupBeforeReload = m_db->rootGroup()->uuid();
1894
if (m_groupView && m_groupView->currentGroup()) {
1895
groupBeforeReload = m_groupView->currentGroup()->uuid();
1898
QUuid entryBeforeReload;
1899
if (m_entryView && m_entryView->currentEntry()) {
1900
entryBeforeReload = m_entryView->currentEntry()->uuid();
1903
replaceDatabase(db);
1905
restoreGroupEntryFocus(groupBeforeReload, entryBeforeReload);
1906
m_blockAutoSave = false;
1908
showMessage(tr("Could not open the new database file while attempting to autoreload.\nError: %1").arg(error),
1909
MessageWidget::Error);
1910
// Mark db as modified since existing data may differ from file or file was deleted
1911
m_db->markAsModified();
1915
m_entryView->setDisabled(false);
1916
m_groupView->setDisabled(false);
1917
m_tagView->setDisabled(false);
1920
int DatabaseWidget::numberOfSelectedEntries() const
1922
return m_entryView->numberOfSelectedEntries();
1925
int DatabaseWidget::currentEntryIndex() const
1927
return m_entryView->currentEntryIndex();
1930
QStringList DatabaseWidget::customEntryAttributes() const
1932
Entry* entry = m_entryView->currentEntry();
1937
return entry->attributes()->customKeys();
1941
* Restores the focus on the group and entry provided
1943
void DatabaseWidget::restoreGroupEntryFocus(const QUuid& groupUuid, const QUuid& entryUuid)
1945
auto group = m_db->rootGroup()->findGroupByUuid(groupUuid);
1947
m_groupView->setCurrentGroup(group);
1948
auto entry = group->findEntryByUuid(entryUuid, false);
1950
m_entryView->setCurrentEntry(entry);
1955
bool DatabaseWidget::isGroupSelected() const
1957
return m_groupView->currentGroup();
1960
bool DatabaseWidget::currentEntryHasTitle()
1962
auto currentEntry = currentSelectedEntry();
1963
Q_ASSERT(currentEntry);
1964
if (!currentEntry) {
1967
return !currentEntry->title().isEmpty();
1970
bool DatabaseWidget::currentEntryHasUsername()
1972
auto currentEntry = currentSelectedEntry();
1973
Q_ASSERT(currentEntry);
1974
if (!currentEntry) {
1977
return !currentEntry->resolveMultiplePlaceholders(currentEntry->username()).isEmpty();
1980
bool DatabaseWidget::currentEntryHasPassword()
1982
auto currentEntry = currentSelectedEntry();
1983
Q_ASSERT(currentEntry);
1984
if (!currentEntry) {
1987
return !currentEntry->resolveMultiplePlaceholders(currentEntry->password()).isEmpty();
1990
bool DatabaseWidget::currentEntryHasUrl()
1992
auto currentEntry = currentSelectedEntry();
1993
Q_ASSERT(currentEntry);
1994
if (!currentEntry) {
1997
return !currentEntry->resolveMultiplePlaceholders(currentEntry->url()).isEmpty();
2000
bool DatabaseWidget::currentEntryHasTotp()
2002
auto currentEntry = currentSelectedEntry();
2003
Q_ASSERT(currentEntry);
2004
if (!currentEntry) {
2007
return currentEntry->hasTotp();
2010
#ifdef WITH_XC_SSHAGENT
2011
bool DatabaseWidget::currentEntryHasSshKey()
2013
Entry* currentEntry = m_entryView->currentEntry();
2014
Q_ASSERT(currentEntry);
2015
if (!currentEntry) {
2019
return KeeAgentSettings::inEntryAttachments(currentEntry->attachments());
2023
bool DatabaseWidget::currentEntryHasNotes()
2025
auto currentEntry = currentSelectedEntry();
2026
Q_ASSERT(currentEntry);
2027
if (!currentEntry) {
2030
return !currentEntry->resolveMultiplePlaceholders(currentEntry->notes()).isEmpty();
2033
bool DatabaseWidget::currentEntryHasAutoTypeEnabled()
2035
auto currentEntry = currentSelectedEntry();
2036
if (!currentEntry) {
2040
return currentEntry->autoTypeEnabled() && currentEntry->groupAutoTypeEnabled();
2043
GroupView* DatabaseWidget::groupView()
2048
EntryView* DatabaseWidget::entryView()
2054
* Save the database to disk.
2056
* This method will try to save several times in case of failure and
2057
* ask to disable safe saves if it is unable to save after the third attempt.
2058
* Set `attempt` to -1 to disable this behavior.
2060
* @return true on success
2062
bool DatabaseWidget::save()
2064
// Never allow saving a locked database; it causes corruption
2065
Q_ASSERT(!isLocked());
2066
// Release build interlock
2068
// We return true since a save is not required
2072
// Read-only and new databases ask for filename
2073
if (m_db->filePath().isEmpty()) {
2077
// Prevent recursions and infinite save loops
2078
m_blockAutoSave = true;
2081
QString errorMessage;
2082
if (performSave(errorMessage)) {
2084
m_blockAutoSave = false;
2085
m_autosaveTimer->stop(); // stop autosave delay to avoid triggering another save
2089
if (m_saveAttempts > 2 && config()->get(Config::UseAtomicSaves).toBool()) {
2090
// Saving failed 3 times, issue a warning and attempt to resolve
2091
auto result = MessageBox::question(this,
2092
tr("Disable safe saves?"),
2093
tr("KeePassXC has failed to save the database multiple times. "
2094
"This is likely caused by file sync services holding a lock on "
2095
"the save file.\nDisable safe saves and try again?"),
2096
MessageBox::Disable | MessageBox::Cancel,
2097
MessageBox::Disable);
2098
if (result == MessageBox::Disable) {
2099
config()->set(Config::UseAtomicSaves, false);
2104
showMessage(tr("Writing the database failed: %1").arg(errorMessage),
2105
MessageWidget::Error,
2107
MessageWidget::LongAutoHideTimeout);
2113
* Save database under a new user-selected filename.
2115
* @return true on success
2117
bool DatabaseWidget::saveAs()
2119
// Never allow saving a locked database; it causes corruption
2120
Q_ASSERT(!isLocked());
2121
// Release build interlock
2123
// We return true since a save is not required
2127
QString oldFilePath = m_db->filePath();
2128
if (!QFileInfo::exists(oldFilePath)) {
2129
QString defaultFileName = config()->get(Config::DefaultDatabaseFileName).toString();
2131
QDir::toNativeSeparators(FileDialog::getLastDir("db") + "/"
2132
+ (defaultFileName.isEmpty() ? tr("Passwords").append(".kdbx") : defaultFileName));
2134
const QString newFilePath = fileDialog()->getSaveFileName(
2135
this, tr("Save database as"), oldFilePath, tr("KeePass 2 Database").append(" (*.kdbx)"), nullptr, nullptr);
2138
if (!newFilePath.isEmpty()) {
2139
QString errorMessage;
2140
if (!performSave(errorMessage, newFilePath)) {
2141
showMessage(tr("Writing the database failed: %1").arg(errorMessage),
2142
MessageWidget::Error,
2144
MessageWidget::LongAutoHideTimeout);
2151
bool DatabaseWidget::performSave(QString& errorMessage, const QString& fileName)
2153
QPointer<QWidget> focusWidget(qApp->focusWidget());
2155
// Lock out interactions
2156
m_entryView->setDisabled(true);
2157
m_groupView->setDisabled(true);
2158
m_tagView->setDisabled(true);
2159
QApplication::processEvents();
2161
Database::SaveAction saveAction = Database::Atomic;
2162
if (!config()->get(Config::UseAtomicSaves).toBool()) {
2163
if (config()->get(Config::UseDirectWriteSaves).toBool()) {
2164
saveAction = Database::DirectWrite;
2166
saveAction = Database::TempFile;
2170
QString backupFilePath;
2171
if (config()->get(Config::BackupBeforeSave).toBool()) {
2172
backupFilePath = config()->get(Config::BackupFilePathPattern).toString();
2173
// Fall back to default
2174
if (backupFilePath.isEmpty()) {
2175
backupFilePath = config()->getDefault(Config::BackupFilePathPattern).toString();
2178
QFileInfo dbFileInfo(m_db->filePath());
2179
backupFilePath = Tools::substituteBackupFilePath(backupFilePath, dbFileInfo.canonicalFilePath());
2180
if (!backupFilePath.isNull()) {
2181
// Note that we cannot guarantee that backupFilePath is actually a valid filename. QT currently provides
2182
// no function for this. Moreover, we don't check if backupFilePath is a file and not a directory.
2183
// If this isn't the case, just let the backup fail.
2184
if (QDir::isRelativePath(backupFilePath)) {
2185
backupFilePath = QDir::cleanPath(dbFileInfo.absolutePath() + QDir::separator() + backupFilePath);
2191
if (fileName.isEmpty()) {
2192
ok = m_db->save(saveAction, backupFilePath, &errorMessage);
2194
ok = m_db->saveAs(fileName, saveAction, backupFilePath, &errorMessage);
2198
m_entryView->setDisabled(false);
2199
m_groupView->setDisabled(false);
2200
m_tagView->setDisabled(false);
2202
if (focusWidget && focusWidget->isVisible()) {
2203
focusWidget->setFocus();
2210
* Save copy of database under a new user-selected filename.
2212
* @return true on success
2214
bool DatabaseWidget::saveBackup()
2217
QString oldFilePath = m_db->filePath();
2218
if (!QFileInfo::exists(oldFilePath)) {
2219
QString defaultFileName = config()->get(Config::DefaultDatabaseFileName).toString();
2220
oldFilePath = QDir::toNativeSeparators(
2221
FileDialog::getLastDir("db") + "/"
2222
+ (defaultFileName.isEmpty() ? tr("Passwords").append(".kdbx") : defaultFileName));
2225
const QString newFilePath = fileDialog()->getSaveFileName(this,
2226
tr("Save database backup"),
2227
FileDialog::getLastDir("backup", oldFilePath),
2228
tr("KeePass 2 Database").append(" (*.kdbx)"),
2232
if (!newFilePath.isEmpty()) {
2233
// Ensure we don't recurse back into this function
2234
m_db->setFilePath(newFilePath);
2237
bool modified = m_db->isModified();
2240
// Failed to save, try again
2241
m_db->setFilePath(oldFilePath);
2245
m_db->setFilePath(oldFilePath);
2247
// Source database is marked as clean when copy is saved, even if source has unsaved changes
2248
m_db->markAsModified();
2250
FileDialog::saveLastDir("backup", newFilePath, true);
2254
// Canceled file selection
2259
void DatabaseWidget::showMessage(const QString& text,
2260
MessageWidget::MessageType type,
2261
bool showClosebutton,
2262
int autoHideTimeout)
2264
m_messageWidget->setCloseButtonVisible(showClosebutton);
2265
m_messageWidget->showMessage(text, type, autoHideTimeout);
2268
void DatabaseWidget::showErrorMessage(const QString& errorMessage)
2270
showMessage(errorMessage, MessageWidget::MessageType::Error);
2273
void DatabaseWidget::hideMessage()
2275
if (m_messageWidget->isVisible()) {
2276
m_messageWidget->animatedHide();
2280
bool DatabaseWidget::isRecycleBinSelected() const
2282
return m_groupView->currentGroup() && m_groupView->currentGroup() == m_db->metadata()->recycleBin();
2285
void DatabaseWidget::emptyRecycleBin()
2287
if (!isRecycleBinSelected()) {
2292
MessageBox::question(this,
2293
tr("Empty recycle bin?"),
2294
tr("Are you sure you want to permanently delete everything from your recycle bin?"),
2295
MessageBox::Empty | MessageBox::Cancel,
2296
MessageBox::Cancel);
2298
if (result == MessageBox::Empty) {
2299
m_db->emptyRecycleBin();
2303
void DatabaseWidget::processAutoOpen()
2307
auto* autoopenGroup = m_db->rootGroup()->findGroupByPath("/AutoOpen");
2308
if (!autoopenGroup) {
2312
for (const auto* entry : autoopenGroup->entries()) {
2313
if (entry->url().isEmpty() || (entry->password().isEmpty() && entry->username().isEmpty())) {
2317
// Support ifDevice advanced entry, a comma separated list of computer names
2318
// that control whether to perform AutoOpen on this entry or not. Can be
2319
// negated using '!'
2320
auto ifDevice = entry->attribute("IfDevice");
2321
if (!ifDevice.isEmpty()) {
2322
bool loadDb = false;
2323
auto hostName = QHostInfo::localHostName();
2324
for (auto& device : ifDevice.split(",")) {
2325
device = device.trimmed();
2326
if (device.startsWith("!")) {
2327
if (device.mid(1).compare(hostName, Qt::CaseInsensitive) == 0) {
2328
// Machine name matched an exclusion, don't load this database
2332
// Not matching an exclusion allows loading on all machines
2335
} else if (device.compare(hostName, Qt::CaseInsensitive) == 0) {
2336
// Explicitly named for loading
2345
openDatabaseFromEntry(entry);
2349
void DatabaseWidget::openDatabaseFromEntry(const Entry* entry, bool inBackground)
2351
auto keyFile = entry->resolveMultiplePlaceholders(entry->username());
2352
auto password = entry->resolveMultiplePlaceholders(entry->password());
2353
auto databaseUrl = entry->resolveMultiplePlaceholders(entry->url());
2354
if (databaseUrl.startsWith("kdbx://")) {
2355
databaseUrl = databaseUrl.mid(7);
2358
QFileInfo dbFileInfo;
2359
if (databaseUrl.startsWith("file://")) {
2360
QUrl url(databaseUrl);
2361
dbFileInfo.setFile(url.toLocalFile());
2363
dbFileInfo.setFile(databaseUrl);
2364
if (dbFileInfo.isRelative()) {
2365
QFileInfo currentpath(m_db->filePath());
2366
dbFileInfo.setFile(currentpath.absoluteDir(), databaseUrl);
2370
if (!dbFileInfo.isFile()) {
2371
showErrorMessage(tr("Could not find database file: %1").arg(databaseUrl));
2375
QFileInfo keyFileInfo;
2376
if (!keyFile.isEmpty()) {
2377
if (keyFile.startsWith("file://")) {
2378
QUrl keyfileUrl(keyFile);
2379
keyFileInfo.setFile(keyfileUrl.toLocalFile());
2381
keyFileInfo.setFile(keyFile);
2382
if (keyFileInfo.isRelative()) {
2383
QFileInfo currentpath(m_db->filePath());
2384
keyFileInfo.setFile(currentpath.absoluteDir(), keyFile);
2389
// Request to open the database file in the background with a password and keyfile
2390
emit requestOpenDatabase(dbFileInfo.canonicalFilePath(), inBackground, password, keyFileInfo.canonicalFilePath());