keepassxc

Форк
0
/
DatabaseWidget.cpp 
2391 строка · 72.7 Кб
1
/*
2
 * Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
3
 * Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
4
 *
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.
9
 *
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.
14
 *
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/>.
17
 */
18

19
#include "DatabaseWidget.h"
20

21
#include <QApplication>
22
#include <QBoxLayout>
23
#include <QCheckBox>
24
#include <QDesktopServices>
25
#include <QHostInfo>
26
#include <QInputDialog>
27
#include <QKeyEvent>
28
#include <QPlainTextEdit>
29
#include <QProcess>
30
#include <QSplitter>
31
#include <QTextDocumentFragment>
32
#include <QTextEdit>
33

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"
58

59
#ifdef WITH_XC_NETWORKING
60
#include "gui/IconDownloaderDialog.h"
61
#endif
62

63
#ifdef WITH_XC_SSHAGENT
64
#include "sshagent/SSHAgent.h"
65
#endif
66

67
#ifdef WITH_XC_BROWSER_PASSKEYS
68
#include "gui/passkeys/PasskeyImporter.h"
69
#endif
70

71
DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
72
    : QStackedWidget(parent)
73
    , m_db(std::move(db))
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))
90
    , m_saveAttempts(0)
91
    , m_entrySearcher(new EntrySearcher(false))
92
{
93
    Q_ASSERT(m_db);
94

95
    m_messageWidget->setHidden(true);
96

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);
103

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()));
109

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);
119

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);
126

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);
133
#endif
134
    rightHandSideVBox->addWidget(m_previewSplitter);
135
    rightHandSideWidget->setLayout(rightHandSideVBox);
136
    m_entryView = new EntryView(rightHandSideWidget);
137

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);
143

144
    m_previewSplitter->setOrientation(Qt::Vertical);
145
    m_previewSplitter->setChildrenCollapsible(true);
146

147
    m_groupView->setObjectName("groupView");
148
    m_groupView->setContextMenuPolicy(Qt::CustomContextMenu);
149
    connect(m_groupView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(emitGroupContextMenuRequested(QPoint)));
150

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)));
155

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);
161

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);
167
#endif
168

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});
176

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");
182

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);
190

191
    // clang-format off
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&)));
212
    // clang-format on
213

214
    connectDatabaseSignals();
215

216
    m_blockAutoSave = false;
217

218
    m_autosaveTimer = new QTimer(this);
219
    m_autosaveTimer->setSingleShot(true);
220
    connect(m_autosaveTimer, SIGNAL(timeout()), this, SLOT(onAutosaveDelayTimeout()));
221

222
    m_searchLimitGroup = config()->get(Config::SearchLimitGroup).toBool();
223

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, {});
228
#endif
229

230
    if (m_db->isInitialized()) {
231
        switchToMainView();
232
    } else {
233
        switchToOpenDatabase();
234
    }
235
}
236

237
DatabaseWidget::DatabaseWidget(const QString& filePath, QWidget* parent)
238
    : DatabaseWidget(QSharedPointer<Database>::create(filePath), parent)
239
{
240
}
241

242
DatabaseWidget::~DatabaseWidget()
243
{
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.
250
    m_db.clear();
251
}
252

253
QSharedPointer<Database> DatabaseWidget::database() const
254
{
255
    return m_db;
256
}
257

258
DatabaseWidget::Mode DatabaseWidget::currentMode() const
259
{
260
    if (currentWidget() == nullptr) {
261
        return Mode::None;
262
    } else if (currentWidget() == m_mainWidget) {
263
        return Mode::ViewMode;
264
    } else if (currentWidget() == m_databaseOpenWidget) {
265
        return Mode::LockedMode;
266
    } else {
267
        return Mode::EditMode;
268
    }
269
}
270

271
bool DatabaseWidget::isLocked() const
272
{
273
    return currentMode() == Mode::LockedMode;
274
}
275

276
bool DatabaseWidget::isSaving() const
277
{
278
    return m_db->isSaving();
279
}
280

281
bool DatabaseWidget::isSorted() const
282
{
283
    return m_entryView->isSorted();
284
}
285

286
bool DatabaseWidget::isSearchActive() const
287
{
288
    return m_entryView->inSearchMode();
289
}
290

291
bool DatabaseWidget::isEntryViewActive() const
292
{
293
    return currentWidget() == m_mainWidget;
294
}
295

296
bool DatabaseWidget::isEntryEditActive() const
297
{
298
    return currentWidget() == m_editEntryWidget;
299
}
300

301
bool DatabaseWidget::isGroupEditActive() const
302
{
303
    return currentWidget() == m_editGroupWidget;
304
}
305

306
bool DatabaseWidget::isEditWidgetModified() const
307
{
308
    if (currentWidget() == m_editEntryWidget) {
309
        return m_editEntryWidget->isModified();
310
    } else if (currentWidget() == m_editGroupWidget) {
311
        return m_editGroupWidget->isModified();
312
    }
313
    return false;
314
}
315

316
QString DatabaseWidget::displayName() const
317
{
318
    if (!m_db) {
319
        return {};
320
    }
321

322
    auto displayName = m_db->metadata()->name();
323
    if (!m_db->filePath().isEmpty()) {
324
        if (displayName.isEmpty()) {
325
            displayName = displayFileName();
326
        }
327
    } else {
328
        if (displayName.isEmpty()) {
329
            displayName = tr("New Database");
330
        } else {
331
            displayName = tr("%1 [New Database]", "Database tab name modifier").arg(displayName);
332
        }
333
    }
334

335
    return displayName;
336
}
337

338
QString DatabaseWidget::displayFileName() const
339
{
340
    if (m_db) {
341
        QFileInfo fileinfo(m_db->filePath());
342
        return fileinfo.fileName();
343
    }
344
    return {};
345
}
346

347
QString DatabaseWidget::displayFilePath() const
348
{
349
    if (m_db) {
350
        return m_db->canonicalFilePath();
351
    }
352
    return {};
353
}
354

355
QHash<Config::ConfigKey, QList<int>> DatabaseWidget::splitterSizes() const
356
{
357
    return {{Config::GUI_SplitterState, m_mainSplitter->sizes()},
358
            {Config::GUI_PreviewSplitterState, m_previewSplitter->sizes()},
359
            {Config::GUI_GroupSplitterState, m_groupSplitter->sizes()}};
360
}
361

362
void DatabaseWidget::setSplitterSizes(const QHash<Config::ConfigKey, QList<int>>& sizes)
363
{
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) {
367
            continue;
368
        }
369
        switch (itr.key()) {
370
        case Config::GUI_SplitterState:
371
            m_mainSplitter->setSizes(itr.value());
372
            break;
373
        case Config::GUI_PreviewSplitterState:
374
            m_previewSplitter->setSizes(itr.value());
375
            break;
376
        case Config::GUI_GroupSplitterState:
377
            m_groupSplitter->setSizes(itr.value());
378
            break;
379
        default:
380
            break;
381
        }
382
    }
383
}
384

385
void DatabaseWidget::setSearchStringForAutoType(const QString& search)
386
{
387
    m_searchStringForAutoType = search;
388
}
389

390
/**
391
 * Get current view state of entry view
392
 */
393
QByteArray DatabaseWidget::entryViewState() const
394
{
395
    return m_entryView->viewState();
396
}
397

398
/**
399
 * Set view state of entry view
400
 */
401
bool DatabaseWidget::setEntryViewState(const QByteArray& state) const
402
{
403
    return m_entryView->setViewState(state);
404
}
405

406
void DatabaseWidget::clearAllWidgets()
407
{
408
    m_editEntryWidget->clear();
409
    m_historyEditEntryWidget->clear();
410
    m_editGroupWidget->clear();
411
    m_previewView->clear();
412
}
413

414
void DatabaseWidget::emitCurrentModeChanged()
415
{
416
    emit currentModeChanged(currentMode());
417
}
418

419
void DatabaseWidget::createEntry()
420
{
421
    Q_ASSERT(m_groupView->currentGroup());
422
    if (!m_groupView->currentGroup()) {
423
        return;
424
    }
425

426
    m_newEntry.reset(new Entry());
427

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);
433
}
434

435
void DatabaseWidget::replaceDatabase(QSharedPointer<Database> db)
436
{
437
    Q_ASSERT(!isEntryEditActive() && !isGroupEditActive());
438

439
    // Save off new parent UUID which will be valid when creating a new entry
440
    QUuid newParentUuid;
441
    if (m_newParent) {
442
        newParentUuid = m_newParent->uuid();
443
    }
444

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.
448
    auto oldDb = m_db;
449
    m_db = std::move(db);
450
    connectDatabaseSignals();
451
    m_groupView->changeDatabase(m_db);
452
    m_tagView->setDatabase(m_db);
453

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);
458
        if (!m_newParent) {
459
            m_newParent = m_db->rootGroup();
460
        }
461
    }
462

463
    emit databaseReplaced(oldDb, m_db);
464

465
#if defined(WITH_XC_KEESHARE)
466
    KeeShare::instance()->connectDatabase(m_db, oldDb);
467
#else
468
    // Keep the instance active till the end of this function
469
    Q_UNUSED(oldDb);
470
#endif
471

472
    oldDb->releaseData();
473
}
474

475
void DatabaseWidget::cloneEntry()
476
{
477
    auto currentEntry = currentSelectedEntry();
478
    Q_ASSERT(currentEntry);
479
    if (!currentEntry) {
480
        return;
481
    }
482

483
    auto cloneDialog = new CloneDialog(this, m_db.data(), currentEntry);
484
    connect(cloneDialog, &CloneDialog::entryCloned, this, [this](auto entry) {
485
        refreshSearch();
486
        m_entryView->setCurrentEntry(entry);
487
    });
488

489
    cloneDialog->show();
490
}
491

492
void DatabaseWidget::showTotp()
493
{
494
    auto currentEntry = currentSelectedEntry();
495
    Q_ASSERT(currentEntry);
496
    if (!currentEntry) {
497
        return;
498
    }
499

500
    auto totpDialog = new TotpDialog(this, currentEntry);
501
    connect(this, &DatabaseWidget::databaseLockRequested, totpDialog, &TotpDialog::close);
502
    totpDialog->open();
503
}
504

505
void DatabaseWidget::copyTotp()
506
{
507
    auto currentEntry = currentSelectedEntry();
508
    Q_ASSERT(currentEntry);
509
    if (!currentEntry) {
510
        return;
511
    }
512
    setClipboardTextAndMinimize(currentEntry->totp());
513
}
514

515
void DatabaseWidget::setupTotp()
516
{
517
    auto currentEntry = currentSelectedEntry();
518
    Q_ASSERT(currentEntry);
519
    if (!currentEntry) {
520
        return;
521
    }
522

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()));
528
    }
529
    connect(this, &DatabaseWidget::databaseLockRequested, setupTotpDialog, &TotpSetupDialog::close);
530
    setupTotpDialog->open();
531
}
532

533
void DatabaseWidget::deleteSelectedEntries()
534
{
535
    const QModelIndexList selected = m_entryView->selectionModel()->selectedRows();
536
    if (selected.isEmpty()) {
537
        return;
538
    }
539

540
    // Resolve entries from the selection model
541
    QList<Entry*> selectedEntries;
542
    for (const QModelIndex& index : selected) {
543
        selectedEntries.append(m_entryView->entryFromIndex(index));
544
    }
545

546
    deleteEntries(std::move(selectedEntries));
547
}
548

549
void DatabaseWidget::restoreSelectedEntries()
550
{
551
    const QModelIndexList selected = m_entryView->selectionModel()->selectedRows();
552
    if (selected.isEmpty()) {
553
        return;
554
    }
555

556
    // Resolve entries from the selection model
557
    QList<Entry*> selectedEntries;
558
    for (auto& index : selected) {
559
        selectedEntries.append(m_entryView->entryFromIndex(index));
560
    }
561

562
    for (auto* entry : selectedEntries) {
563
        if (entry->previousParentGroup()) {
564
            entry->setGroup(entry->previousParentGroup());
565
        }
566
    }
567
}
568

569
void DatabaseWidget::deleteEntries(QList<Entry*> selectedEntries, bool confirm)
570
{
571
    if (selectedEntries.isEmpty()) {
572
        return;
573
    }
574

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);
578

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();
583

584
    if (confirm && !GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) {
585
        return;
586
    }
587

588
    GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent);
589

590
    // Select the row above the deleted entries
591
    if (index.isValid()) {
592
        m_entryView->setCurrentIndex(index);
593
    } else {
594
        m_entryView->setFirstEntryActive();
595
    }
596
}
597

598
void DatabaseWidget::setFocus(Qt::FocusReason reason)
599
{
600
    focusNextPrevChild(reason == Qt::TabFocusReason);
601
}
602

603
void DatabaseWidget::focusOnEntries(bool editIfFocused)
604
{
605
    if (isEntryViewActive()) {
606
        if (editIfFocused && m_entryView->hasFocus()) {
607
            switchToEntryEdit();
608
        } else {
609
            m_entryView->setFocus();
610
        }
611
    }
612
}
613

614
void DatabaseWidget::focusOnGroups(bool editIfFocused)
615
{
616
    if (isEntryViewActive()) {
617
        if (editIfFocused && m_groupView->hasFocus()) {
618
            switchToGroupEdit();
619
        } else {
620
            m_groupView->setFocus();
621
        }
622
    }
623
}
624

625
void DatabaseWidget::moveEntryUp()
626
{
627
    auto currentEntry = currentSelectedEntry();
628
    if (currentEntry) {
629
        currentEntry->moveUp();
630
        m_entryView->setCurrentEntry(currentEntry);
631
    }
632
}
633

634
void DatabaseWidget::moveEntryDown()
635
{
636
    auto currentEntry = currentSelectedEntry();
637
    if (currentEntry) {
638
        currentEntry->moveDown();
639
        m_entryView->setCurrentEntry(currentEntry);
640
    }
641
}
642

643
void DatabaseWidget::copyTitle()
644
{
645
    auto currentEntry = currentSelectedEntry();
646
    if (currentEntry) {
647
        setClipboardTextAndMinimize(currentEntry->resolveMultiplePlaceholders(currentEntry->title()));
648
    }
649
}
650

651
void DatabaseWidget::copyUsername()
652
{
653
    auto currentEntry = currentSelectedEntry();
654
    if (currentEntry) {
655
        setClipboardTextAndMinimize(currentEntry->resolveMultiplePlaceholders(currentEntry->username()));
656
    }
657
}
658

659
void DatabaseWidget::copyPassword()
660
{
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
663

664
    bool clearClipboard = config()->get(Config::Security_ClearClipboard).toBool();
665

666
    auto plainTextEdit = qobject_cast<QPlainTextEdit*>(focusWidget());
667
    if (plainTextEdit && plainTextEdit->textCursor().hasSelection()) {
668
        clipboard()->setText(plainTextEdit->textCursor().selectedText(), clearClipboard);
669
        return;
670
    }
671

672
    auto label = qobject_cast<QLabel*>(focusWidget());
673
    if (label && label->hasSelectedText()) {
674
        clipboard()->setText(label->selectedText(), clearClipboard);
675
        return;
676
    }
677

678
    auto textEdit = qobject_cast<QTextEdit*>(focusWidget());
679
    if (textEdit && textEdit->textCursor().hasSelection()) {
680
        clipboard()->setText(textEdit->textCursor().selection().toPlainText(), clearClipboard);
681
        return;
682
    }
683

684
    auto currentEntry = currentSelectedEntry();
685
    if (currentEntry) {
686
        setClipboardTextAndMinimize(currentEntry->resolveMultiplePlaceholders(currentEntry->password()));
687
    }
688
}
689

690
void DatabaseWidget::copyPasswordTotp()
691
{
692
    auto currentEntry = currentSelectedEntry();
693
    if (currentEntry) {
694
        setClipboardTextAndMinimize(
695
            currentEntry->resolveMultiplePlaceholders(currentEntry->password()).append(currentEntry->totp()));
696
    }
697
}
698

699
void DatabaseWidget::copyURL()
700
{
701
    auto currentEntry = currentSelectedEntry();
702
    if (currentEntry) {
703
        setClipboardTextAndMinimize(currentEntry->resolveMultiplePlaceholders(currentEntry->url()));
704
    }
705
}
706

707
void DatabaseWidget::copyNotes()
708
{
709
    auto currentEntry = currentSelectedEntry();
710
    if (currentEntry) {
711
        setClipboardTextAndMinimize(currentEntry->resolveMultiplePlaceholders(currentEntry->notes()));
712
    }
713
}
714

715
void DatabaseWidget::copyAttribute(QAction* action)
716
{
717
    auto currentEntry = currentSelectedEntry();
718
    if (currentEntry) {
719
        setClipboardTextAndMinimize(
720
            currentEntry->resolveMultiplePlaceholders(currentEntry->attributes()->value(action->data().toString())));
721
    }
722
}
723

724
void DatabaseWidget::filterByTag()
725
{
726
    QStringList searchTerms;
727
    const auto selections = m_tagView->selectionModel()->selectedIndexes();
728
    for (const auto& index : selections) {
729
        searchTerms << index.data(Qt::UserRole).toString();
730
    }
731
    emit requestSearch(searchTerms.join(" "));
732
}
733

734
void DatabaseWidget::setTag(QAction* action)
735
{
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);
740
    }
741
}
742

743
void DatabaseWidget::showTotpKeyQrCode()
744
{
745
    auto currentEntry = currentSelectedEntry();
746
    if (currentEntry) {
747
        auto totpDisplayDialog = new TotpExportSettingsDialog(this, currentEntry);
748
        connect(this, &DatabaseWidget::databaseLockRequested, totpDisplayDialog, &TotpExportSettingsDialog::close);
749
        totpDisplayDialog->open();
750
    }
751
}
752

753
void DatabaseWidget::setClipboardTextAndMinimize(const QString& text)
754
{
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()) {
760
            window()->lower();
761
        }
762
    }
763
}
764

765
#ifdef WITH_XC_SSHAGENT
766
void DatabaseWidget::addToAgent()
767
{
768
    Entry* currentEntry = m_entryView->currentEntry();
769
    Q_ASSERT(currentEntry);
770
    if (!currentEntry) {
771
        return;
772
    }
773

774
    KeeAgentSettings settings;
775
    if (!settings.fromEntry(currentEntry)) {
776
        return;
777
    }
778

779
    SSHAgent* agent = SSHAgent::instance();
780
    OpenSSHKey key;
781
    if (settings.toOpenSSHKey(currentEntry, key, true)) {
782
        if (!agent->addIdentity(key, settings, database()->uuid())) {
783
            m_messageWidget->showMessage(agent->errorString(), MessageWidget::Error);
784
        }
785
    } else {
786
        m_messageWidget->showMessage(settings.errorString(), MessageWidget::Error);
787
    }
788
}
789

790
void DatabaseWidget::removeFromAgent()
791
{
792
    Entry* currentEntry = m_entryView->currentEntry();
793
    Q_ASSERT(currentEntry);
794
    if (!currentEntry) {
795
        return;
796
    }
797

798
    KeeAgentSettings settings;
799
    if (!settings.fromEntry(currentEntry)) {
800
        return;
801
    }
802

803
    SSHAgent* agent = SSHAgent::instance();
804
    OpenSSHKey key;
805
    if (settings.toOpenSSHKey(currentEntry, key, false)) {
806
        if (!agent->removeIdentity(key)) {
807
            m_messageWidget->showMessage(agent->errorString(), MessageWidget::Error);
808
        }
809
    } else {
810
        m_messageWidget->showMessage(settings.errorString(), MessageWidget::Error);
811
    }
812
}
813
#endif
814

815
void DatabaseWidget::performAutoType(const QString& sequence)
816
{
817
    auto currentEntry = currentSelectedEntry();
818
    if (currentEntry) {
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) {
824
            return;
825
        }
826

827
        if (sequence.isEmpty()) {
828
            autoType()->performAutoType(currentEntry);
829
        } else {
830
            autoType()->performAutoTypeWithSequence(currentEntry, sequence);
831
        }
832
    }
833
}
834

835
void DatabaseWidget::performAutoTypeUsername()
836
{
837
    performAutoType(QStringLiteral("{USERNAME}"));
838
}
839

840
void DatabaseWidget::performAutoTypeUsernameEnter()
841
{
842
    performAutoType(QStringLiteral("{USERNAME}{ENTER}"));
843
}
844

845
void DatabaseWidget::performAutoTypePassword()
846
{
847
    performAutoType(QStringLiteral("{PASSWORD}"));
848
}
849

850
void DatabaseWidget::performAutoTypePasswordEnter()
851
{
852
    performAutoType(QStringLiteral("{PASSWORD}{ENTER}"));
853
}
854

855
void DatabaseWidget::performAutoTypeTOTP()
856
{
857
    performAutoType(QStringLiteral("{TOTP}"));
858
}
859

860
void DatabaseWidget::openUrl()
861
{
862
    auto currentEntry = currentSelectedEntry();
863
    if (currentEntry) {
864
        openUrlForEntry(currentEntry);
865
    }
866
}
867

868
void DatabaseWidget::downloadSelectedFavicons()
869
{
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));
874
    }
875

876
    // Force download even if icon already exists
877
    performIconDownloads(selectedEntries, true);
878
#endif
879
}
880

881
void DatabaseWidget::downloadAllFavicons()
882
{
883
#ifdef WITH_XC_NETWORKING
884
    auto currentGroup = m_groupView->currentGroup();
885
    if (currentGroup) {
886
        performIconDownloads(currentGroup->entries());
887
    }
888
#endif
889
}
890

891
void DatabaseWidget::downloadFaviconInBackground(Entry* entry)
892
{
893
#ifdef WITH_XC_NETWORKING
894
    performIconDownloads({entry}, true, true);
895
#else
896
    Q_UNUSED(entry);
897
#endif
898
}
899

900
void DatabaseWidget::performIconDownloads(const QList<Entry*>& entries, bool force, bool downloadInBackground)
901
{
902
#ifdef WITH_XC_NETWORKING
903
    auto* iconDownloaderDialog = new IconDownloaderDialog(this);
904
    connect(this, SIGNAL(databaseLockRequested()), iconDownloaderDialog, SLOT(close()));
905

906
    if (downloadInBackground && entries.count() > 0) {
907
        iconDownloaderDialog->downloadFaviconInBackground(m_db, entries.first());
908
    } else {
909
        iconDownloaderDialog->downloadFavicons(m_db, entries, force);
910
    }
911
#else
912
    Q_UNUSED(entries);
913
    Q_UNUSED(force);
914
    Q_UNUSED(downloadInBackground);
915
#endif
916
}
917

918
void DatabaseWidget::openUrlForEntry(Entry* entry)
919
{
920
    Q_ASSERT(entry);
921
    if (!entry) {
922
        return;
923
    }
924

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");
929

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) + " […]";
936
            }
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,
942
                               this);
943
            msgbox.setDefaultButton(QMessageBox::No);
944

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) {
950
                    remember = true;
951
                }
952
            });
953

954
            int result = msgbox.exec();
955
            launch = (result == QMessageBox::Yes);
956

957
            if (remember) {
958
                entry->attributes()->set(EntryAttributes::RememberCmdExecAttr, result == QMessageBox::Yes ? "1" : "0");
959
            }
960
        }
961

962
        if (launch) {
963
            QProcess::startDetached(cmdString.mid(6));
964

965
            if (config()->get(Config::MinimizeOnOpenUrl).toBool()) {
966
                getMainWindow()->minimizeOrHide();
967
            }
968
        }
969
    } else if (cmdString.startsWith("kdbx://")) {
970
        openDatabaseFromEntry(entry, false);
971
    } else {
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)});
976
#else
977
            QDesktopServices::openUrl(url);
978
#endif
979

980
            if (config()->get(Config::MinimizeOnOpenUrl).toBool()) {
981
                getMainWindow()->minimizeOrHide();
982
            }
983
        }
984
    }
985
}
986

987
Entry* DatabaseWidget::currentSelectedEntry()
988
{
989
    if (currentWidget() == m_editEntryWidget) {
990
        return m_editEntryWidget->currentEntry();
991
    }
992

993
    return m_entryView->currentEntry();
994
}
995

996
void DatabaseWidget::createGroup()
997
{
998
    Q_ASSERT(m_groupView->currentGroup());
999
    if (!m_groupView->currentGroup()) {
1000
        return;
1001
    }
1002

1003
    m_newGroup.reset(new Group());
1004
    m_newGroup->setUuid(QUuid::createUuid());
1005
    m_newParent = m_groupView->currentGroup();
1006
    switchToGroupEdit(m_newGroup.data(), true);
1007
}
1008

1009
void DatabaseWidget::cloneGroup()
1010
{
1011
    Group* currentGroup = m_groupView->currentGroup();
1012
    Q_ASSERT(currentGroup && canCloneCurrentGroup());
1013
    if (!currentGroup || !canCloneCurrentGroup()) {
1014
        return;
1015
    }
1016

1017
    m_newGroup.reset(currentGroup->clone(Entry::CloneCopy, Group::CloneDefault | Group::CloneRenameTitle));
1018
    m_newParent = currentGroup->parentGroup();
1019
    switchToGroupEdit(m_newGroup.data(), true);
1020
}
1021

1022
void DatabaseWidget::deleteGroup()
1023
{
1024
    Group* currentGroup = m_groupView->currentGroup();
1025
    Q_ASSERT(currentGroup && canDeleteCurrentGroup());
1026
    if (!currentGroup || !canDeleteCurrentGroup()) {
1027
        return;
1028
    }
1029

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(
1036
            this,
1037
            tr("Delete group"),
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);
1041

1042
        if (result == MessageBox::Delete) {
1043
            delete currentGroup;
1044
        }
1045
    } else {
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);
1055
        }
1056
    }
1057
}
1058

1059
int DatabaseWidget::addChildWidget(QWidget* w)
1060
{
1061
    w->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
1062
    int index = QStackedWidget::addWidget(w);
1063
    adjustSize();
1064
    return index;
1065
}
1066

1067
void DatabaseWidget::switchToMainView(bool previousDialogAccepted)
1068
{
1069
    setCurrentWidget(m_mainWidget);
1070

1071
    if (m_newGroup) {
1072
        if (previousDialogAccepted) {
1073
            m_newGroup->setParent(m_newParent);
1074
            m_groupView->setCurrentGroup(m_newGroup.take());
1075
            m_groupView->expandGroup(m_newParent);
1076
        } else {
1077
            m_newGroup.reset();
1078
        }
1079

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());
1086
        } else {
1087
            m_newEntry.reset();
1088
        }
1089

1090
        m_newParent = nullptr;
1091
    } else {
1092
        // Workaround: ensure entries are focused so search doesn't reset
1093
        m_entryView->setFocus();
1094
    }
1095
}
1096

1097
void DatabaseWidget::switchToHistoryView(Entry* entry)
1098
{
1099
    auto entryTitle = m_editEntryWidget->currentEntry() ? m_editEntryWidget->currentEntry()->title() : "";
1100
    m_historyEditEntryWidget->loadEntry(entry, false, true, entryTitle, m_db);
1101
    setCurrentWidget(m_historyEditEntryWidget);
1102
}
1103

1104
void DatabaseWidget::switchBackToEntryEdit()
1105
{
1106
    setCurrentWidget(m_editEntryWidget);
1107
}
1108

1109
void DatabaseWidget::switchToEntryEdit(Entry* entry)
1110
{
1111
    switchToEntryEdit(entry, false);
1112
}
1113

1114
void DatabaseWidget::switchToEntryEdit(Entry* entry, bool create)
1115
{
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.
1119
    Group* group;
1120
    if (create) {
1121
        group = currentGroup();
1122
    } else {
1123
        group = entry->group();
1124
        // Ensure we have only this entry selected
1125
        m_entryView->setCurrentEntry(entry);
1126
    }
1127

1128
    Q_ASSERT(group);
1129

1130
    // Setup the entry edit widget and display
1131
    m_editEntryWidget->loadEntry(entry, create, false, group->name(), m_db);
1132
    setCurrentWidget(m_editEntryWidget);
1133
}
1134

1135
void DatabaseWidget::switchToGroupEdit(Group* group, bool create)
1136
{
1137
    m_editGroupWidget->loadGroup(group, create, m_db);
1138
    setCurrentWidget(m_editGroupWidget);
1139
}
1140

1141
void DatabaseWidget::connectDatabaseSignals()
1142
{
1143
    // relayed Database events
1144
    connect(m_db.data(),
1145
            SIGNAL(filePathChanged(QString, QString)),
1146

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);
1154
}
1155

1156
void DatabaseWidget::loadDatabase(bool accepted)
1157
{
1158
    auto* openWidget = qobject_cast<DatabaseOpenWidget*>(sender());
1159
    Q_ASSERT(openWidget);
1160
    if (!openWidget) {
1161
        return;
1162
    }
1163

1164
    if (accepted) {
1165
        replaceDatabase(openWidget->database());
1166
        switchToMainView();
1167
        processAutoOpen();
1168

1169
        restoreGroupEntryFocus(m_groupBeforeLock, m_entryBeforeLock);
1170

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");
1176
            } else {
1177
                m_nextSearchLabelText =
1178
                    tr("Entries expiring within %1 day(s)", "", expirationOffset).arg(expirationOffset);
1179
            }
1180
            requestSearch(QString("is:expired-%1").arg(expirationOffset));
1181
        }
1182

1183
        m_groupBeforeLock = QUuid();
1184
        m_entryBeforeLock = QUuid();
1185
        m_saveAttempts = 0;
1186
        emit databaseUnlocked();
1187
#ifdef WITH_XC_SSHAGENT
1188
        sshAgent()->databaseUnlocked(m_db);
1189
#endif
1190
        if (config()->get(Config::MinimizeAfterUnlock).toBool()) {
1191
            getMainWindow()->minimizeOrHide();
1192
        }
1193
    } else {
1194
        if (m_databaseOpenWidget->database()) {
1195
            m_databaseOpenWidget->database().reset();
1196
        }
1197
        emit closeRequest();
1198
    }
1199
}
1200

1201
void DatabaseWidget::mergeDatabase(bool accepted)
1202
{
1203
    if (accepted) {
1204
        if (!m_db) {
1205
            showMessage(tr("No current database."), MessageWidget::Error);
1206
            return;
1207
        }
1208

1209
        auto* senderDialog = qobject_cast<DatabaseOpenDialog*>(sender());
1210

1211
        Q_ASSERT(senderDialog);
1212
        if (!senderDialog) {
1213
            return;
1214
        }
1215
        auto srcDb = senderDialog->database();
1216

1217
        if (!srcDb) {
1218
            showMessage(tr("No source database, nothing to do."), MessageWidget::Error);
1219
            return;
1220
        }
1221

1222
        Merger merger(srcDb.data(), m_db.data());
1223
        QStringList changeList = merger.merge();
1224

1225
        if (!changeList.isEmpty()) {
1226
            showMessage(tr("Successfully merged the database files."), MessageWidget::Information);
1227
        } else {
1228
            showMessage(tr("Database was not modified by merge operation."), MessageWidget::Information);
1229
        }
1230
    }
1231

1232
    switchToMainView();
1233
    emit databaseMerged(m_db);
1234
}
1235

1236
/**
1237
 * Unlock the database.
1238
 *
1239
 * @param accepted true if the unlock dialog or widget was confirmed with OK
1240
 */
1241
void DatabaseWidget::unlockDatabase(bool accepted)
1242
{
1243
    auto* senderDialog = qobject_cast<DatabaseOpenDialog*>(sender());
1244

1245
    if (!accepted) {
1246
        if (!senderDialog && (!m_db || !m_db->isInitialized())) {
1247
            emit closeRequest();
1248
        }
1249
        return;
1250
    }
1251

1252
    if (senderDialog && senderDialog->intent() == DatabaseOpenDialog::Intent::Merge) {
1253
        mergeDatabase(accepted);
1254
        return;
1255
    }
1256

1257
    QSharedPointer<Database> db;
1258
    if (senderDialog) {
1259
        db = senderDialog->database();
1260
    } else {
1261
        db = m_databaseOpenWidget->database();
1262
    }
1263
    replaceDatabase(db);
1264

1265
    restoreGroupEntryFocus(m_groupBeforeLock, m_entryBeforeLock);
1266
    m_groupBeforeLock = QUuid();
1267
    m_entryBeforeLock = QUuid();
1268

1269
    switchToMainView();
1270
    processAutoOpen();
1271
    emit databaseUnlocked();
1272

1273
#ifdef WITH_XC_SSHAGENT
1274
    sshAgent()->databaseUnlocked(m_db);
1275
#endif
1276

1277
    if (config()->get(Config::MinimizeAfterUnlock).toBool()) {
1278
        getMainWindow()->minimizeOrHide();
1279
    }
1280

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);
1286
    }
1287
}
1288

1289
void DatabaseWidget::entryActivationSignalReceived(Entry* entry, EntryModel::ModelColumn column)
1290
{
1291
    Q_ASSERT(entry);
1292
    if (!entry) {
1293
        return;
1294
    }
1295

1296
    // Implement 'copy-on-doubleclick' functionality for certain columns
1297
    switch (column) {
1298
    case EntryModel::Username:
1299
        if (config()->get(Config::Security_EnableCopyOnDoubleClick).toBool()) {
1300
            setClipboardTextAndMinimize(entry->resolveMultiplePlaceholders(entry->username()));
1301
        } else {
1302
            switchToEntryEdit(entry);
1303
        }
1304
        break;
1305
    case EntryModel::Password:
1306
        if (config()->get(Config::Security_EnableCopyOnDoubleClick).toBool()) {
1307
            setClipboardTextAndMinimize(entry->resolveMultiplePlaceholders(entry->password()));
1308
        } else {
1309
            switchToEntryEdit(entry);
1310
        }
1311
        break;
1312
    case EntryModel::Url:
1313
        if (!entry->url().isEmpty()) {
1314
            openUrlForEntry(entry);
1315
        }
1316
        break;
1317
    case EntryModel::Totp:
1318
        if (entry->hasTotp()) {
1319
            setClipboardTextAndMinimize(entry->totp());
1320
        } else {
1321
            setupTotp();
1322
        }
1323
        break;
1324
    case EntryModel::ParentGroup:
1325
        // Call this first to clear out of search mode, otherwise
1326
        // the desired entry is not properly selected
1327
        endSearch();
1328
        m_groupView->setCurrentGroup(entry->group());
1329
        m_entryView->setCurrentEntry(entry);
1330
        break;
1331
    // TODO: switch to 'Notes' tab in details view/pane
1332
    // case EntryModel::Notes:
1333
    //    break;
1334
    // TODO: switch to 'Attachments' tab in details view/pane
1335
    // case EntryModel::Attachments:
1336
    //    break;
1337
    default:
1338
        switchToEntryEdit(entry);
1339
    }
1340
}
1341

1342
void DatabaseWidget::switchToDatabaseReports()
1343
{
1344
    m_reportsDialog->load(m_db);
1345
    setCurrentWidget(m_reportsDialog);
1346
}
1347

1348
void DatabaseWidget::switchToDatabaseSettings()
1349
{
1350
    m_databaseSettingDialog->load(m_db);
1351
    setCurrentWidget(m_databaseSettingDialog);
1352
}
1353

1354
void DatabaseWidget::switchToOpenDatabase()
1355
{
1356
    if (currentWidget() != m_databaseOpenWidget || m_databaseOpenWidget->filename() != m_db->filePath()) {
1357
        switchToOpenDatabase(m_db->filePath());
1358
    }
1359
}
1360

1361
void DatabaseWidget::switchToOpenDatabase(const QString& filePath)
1362
{
1363
    m_databaseOpenWidget->load(filePath);
1364
    setCurrentWidget(m_databaseOpenWidget);
1365
}
1366

1367
void DatabaseWidget::switchToOpenDatabase(const QString& filePath, const QString& password, const QString& keyFile)
1368
{
1369
    switchToOpenDatabase(filePath);
1370
    m_databaseOpenWidget->enterKey(password, keyFile);
1371
}
1372

1373
void DatabaseWidget::switchToEntryEdit()
1374
{
1375
    auto entry = m_entryView->currentEntry();
1376
    if (!entry) {
1377
        return;
1378
    }
1379

1380
    switchToEntryEdit(entry, false);
1381
}
1382

1383
void DatabaseWidget::switchToGroupEdit()
1384
{
1385
    auto group = m_groupView->currentGroup();
1386
    if (!group) {
1387
        return;
1388
    }
1389

1390
    switchToGroupEdit(group, false);
1391
}
1392

1393
void DatabaseWidget::sortGroupsAsc()
1394
{
1395
    m_groupView->sortGroups();
1396
}
1397

1398
void DatabaseWidget::sortGroupsDesc()
1399
{
1400
    m_groupView->sortGroups(true);
1401
}
1402

1403
void DatabaseWidget::switchToDatabaseSecurity()
1404
{
1405
    switchToDatabaseSettings();
1406
    m_databaseSettingDialog->showDatabaseKeySettings();
1407
}
1408

1409
#ifdef WITH_XC_BROWSER_PASSKEYS
1410
void DatabaseWidget::switchToPasskeys()
1411
{
1412
    switchToDatabaseReports();
1413
    m_reportsDialog->activatePasskeysPage();
1414
}
1415

1416
void DatabaseWidget::showImportPasskeyDialog(bool isEntry)
1417
{
1418
    PasskeyImporter passkeyImporter(this);
1419

1420
    if (isEntry) {
1421
        auto currentEntry = currentSelectedEntry();
1422
        if (!currentEntry) {
1423
            return;
1424
        }
1425

1426
        passkeyImporter.importPasskey(m_db, currentEntry);
1427
    } else {
1428
        passkeyImporter.importPasskey(m_db);
1429
    }
1430
}
1431
#endif
1432

1433
void DatabaseWidget::performUnlockDatabase(const QString& password, const QString& keyfile)
1434
{
1435
    if (password.isEmpty() && keyfile.isEmpty()) {
1436
        return;
1437
    }
1438

1439
    if (!m_db->isInitialized() || isLocked()) {
1440
        switchToOpenDatabase();
1441
        m_databaseOpenWidget->enterKey(password, keyfile);
1442
    }
1443
}
1444

1445
void DatabaseWidget::refreshSearch()
1446
{
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);
1452
    }
1453
}
1454

1455
void DatabaseWidget::search(const QString& searchtext)
1456
{
1457
    if (searchtext.isEmpty()) {
1458
        endSearch();
1459
        return;
1460
    }
1461

1462
    auto searchGroup = m_db->rootGroup();
1463
    if (m_searchLimitGroup && m_nextSearchLabelText.isEmpty()) {
1464
        searchGroup = currentGroup();
1465
    }
1466

1467
    auto results = m_entrySearcher->search(searchtext, searchGroup);
1468

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()) {
1473
            endSearch();
1474
            return;
1475
        }
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()));
1480
    } else {
1481
        m_searchingLabel->setText(tr("No Results"));
1482
    }
1483

1484
    emit searchModeAboutToActivate();
1485

1486
    m_entryView->displaySearch(results);
1487
    m_lastSearchText = searchtext;
1488

1489
    m_searchingLabel->setVisible(true);
1490
#ifdef WITH_XC_KEESHARE
1491
    m_shareLabel->setVisible(false);
1492
#endif
1493

1494
    emit searchModeActivated();
1495
}
1496

1497
void DatabaseWidget::saveSearch(const QString& searchtext)
1498
{
1499
    if (!m_db->isInitialized()) {
1500
        return;
1501
    }
1502

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("");
1507

1508
    QInputDialog dialog(this);
1509
    connect(this, &DatabaseWidget::databaseLockRequested, &dialog, &QInputDialog::reject);
1510

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"));
1516
    dialog.exec();
1517

1518
    auto name = dialog.textValue();
1519
    if (!name.isEmpty()) {
1520
        m_db->metadata()->addSavedSearch(name, searchtext);
1521
    }
1522
}
1523

1524
void DatabaseWidget::deleteSearch(const QString& name)
1525
{
1526
    if (m_db->isInitialized()) {
1527
        m_db->metadata()->deleteSavedSearch(name);
1528
    }
1529
}
1530

1531
void DatabaseWidget::setSearchCaseSensitive(bool state)
1532
{
1533
    m_entrySearcher->setCaseSensitive(state);
1534
    refreshSearch();
1535
}
1536

1537
void DatabaseWidget::setSearchLimitGroup(bool state)
1538
{
1539
    m_searchLimitGroup = state;
1540
    refreshSearch();
1541
}
1542

1543
void DatabaseWidget::onGroupChanged()
1544
{
1545
    auto group = m_groupView->currentGroup();
1546

1547
    // Intercept group changes if in search mode
1548
    if (isSearchActive() && m_searchLimitGroup) {
1549
        search(m_lastSearchText);
1550
    } else {
1551
        endSearch();
1552
        m_entryView->displayGroup(group);
1553
    }
1554

1555
    m_previewView->setGroup(group);
1556

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);
1562
    } else {
1563
        m_shareLabel->setVisible(false);
1564
    }
1565
#endif
1566

1567
    emit groupChanged();
1568
}
1569

1570
void DatabaseWidget::onDatabaseModified()
1571
{
1572
    refreshSearch();
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);
1578
        return;
1579
    }
1580
    if (!m_blockAutoSave && autosaveAfterEveryChangeConfig) {
1581
        save();
1582
    } else {
1583
        // Only block once, then reset
1584
        m_blockAutoSave = false;
1585
    }
1586
}
1587

1588
void DatabaseWidget::onAutosaveDelayTimeout()
1589
{
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
1594
        return;
1595
    }
1596
    if (!m_blockAutoSave) {
1597
        save();
1598
    } else {
1599
        // Only block once, then reset
1600
        m_blockAutoSave = false;
1601
    }
1602
}
1603

1604
void DatabaseWidget::triggerAutosaveTimer()
1605
{
1606
    m_autosaveTimer->stop();
1607
    QMetaObject::invokeMethod(m_autosaveTimer, "timeout");
1608
}
1609

1610
void DatabaseWidget::onDatabaseNonDataChanged()
1611
{
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();
1615
    }
1616
}
1617

1618
QString DatabaseWidget::getCurrentSearch()
1619
{
1620
    return m_lastSearchText;
1621
}
1622

1623
void DatabaseWidget::endSearch()
1624
{
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();
1635
    }
1636

1637
    m_searchingLabel->setVisible(false);
1638
    m_searchingLabel->setText(tr("Searching…"));
1639

1640
    m_lastSearchText.clear();
1641
    m_nextSearchLabelText.clear();
1642

1643
    // Tell the search widget to clear
1644
    emit clearSearch();
1645
}
1646

1647
void DatabaseWidget::emitGroupContextMenuRequested(const QPoint& pos)
1648
{
1649
    emit groupContextMenuRequested(m_groupView->viewport()->mapToGlobal(pos));
1650
}
1651

1652
void DatabaseWidget::emitEntryContextMenuRequested(const QPoint& pos)
1653
{
1654
    emit entryContextMenuRequested(m_entryView->viewport()->mapToGlobal(pos));
1655
}
1656

1657
void DatabaseWidget::onEntryChanged(Entry* entry)
1658
{
1659
    if (entry) {
1660
        m_previewView->setEntry(entry);
1661
    } else {
1662
        m_previewView->setGroup(groupView()->currentGroup());
1663
    }
1664

1665
    emit entrySelectionChanged();
1666
}
1667

1668
bool DatabaseWidget::canCloneCurrentGroup() const
1669
{
1670
    bool isRootGroup = m_db->rootGroup() == m_groupView->currentGroup();
1671
    // bool isRecycleBin = isRecycleBinSelected();
1672

1673
    return !isRootGroup;
1674
}
1675

1676
bool DatabaseWidget::canDeleteCurrentGroup() const
1677
{
1678
    bool isRootGroup = m_db->rootGroup() == m_groupView->currentGroup();
1679
    return !isRootGroup;
1680
}
1681

1682
Group* DatabaseWidget::currentGroup() const
1683
{
1684
    return m_groupView->currentGroup();
1685
}
1686

1687
void DatabaseWidget::closeEvent(QCloseEvent* event)
1688
{
1689
    if (!lock() || m_databaseOpenWidget->unlockingDatabase()) {
1690
        event->ignore();
1691
        return;
1692
    }
1693

1694
    m_databaseOpenWidget->resetQuickUnlock();
1695
    event->accept();
1696
}
1697

1698
void DatabaseWidget::showEvent(QShowEvent* event)
1699
{
1700
    if (!m_db->isInitialized() || isLocked()) {
1701
        switchToOpenDatabase();
1702
    }
1703

1704
    event->accept();
1705
}
1706

1707
bool DatabaseWidget::focusNextPrevChild(bool next)
1708
{
1709
    // [parent] <-> GroupView <-> TagView <-> EntryView <-> EntryPreview <-> [parent]
1710
    QList<QWidget*> sequence = {m_groupView, m_tagView, m_entryView, m_previewView};
1711
    auto widget = qApp->focusWidget();
1712
    if (!widget) {
1713
        return QStackedWidget::focusNextPrevChild(next);
1714
    }
1715

1716
    // Find the nearest parent widget in the sequence list
1717
    int idx;
1718
    do {
1719
        idx = sequence.indexOf(widget);
1720
        widget = widget->parentWidget();
1721
    } while (idx == -1 && widget);
1722

1723
    // Determine next/previous or wrap around
1724
    if (idx == -1) {
1725
        idx = next ? 0 : sequence.size() - 1;
1726
    } else {
1727
        idx = next ? idx + 1 : idx - 1;
1728
    }
1729

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) {
1734
            widget->setFocus();
1735
            return widget;
1736
        }
1737
        idx = next ? idx + 1 : idx - 1;
1738
    }
1739

1740
    // Ran out of options, defer to the parent widget
1741
    return QStackedWidget::focusNextPrevChild(next);
1742
}
1743

1744
bool DatabaseWidget::lock()
1745
{
1746
    if (isLocked()) {
1747
        return true;
1748
    }
1749

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()));
1753
        return false;
1754
    }
1755

1756
    emit databaseLockRequested();
1757

1758
    // Force close any modal widgets associated with this widget
1759
    auto modalWidget = QApplication::activeModalWidget();
1760
    if (modalWidget) {
1761
        auto parent = modalWidget->parentWidget();
1762
        while (parent) {
1763
            if (parent == this) {
1764
                modalWidget->close();
1765
                break;
1766
            }
1767
            parent = parent->parentWidget();
1768
        }
1769
    }
1770

1771
    clipboard()->clearCopiedText();
1772

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) {
1780
            return false;
1781
        }
1782
    }
1783

1784
    if (m_db->isModified()) {
1785
        bool saved = false;
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()) {
1789
            saved = save();
1790
        }
1791

1792
        if (!saved) {
1793
            QString msg;
1794
            if (!m_db->metadata()->name().toHtmlEscaped().isEmpty()) {
1795
                msg = tr("\"%1\" was modified.\nSave changes?").arg(m_db->metadata()->name().toHtmlEscaped());
1796
            } else {
1797
                msg = tr("Database was modified.\nSave changes?");
1798
            }
1799
            auto result = MessageBox::question(this,
1800
                                               tr("Save changes?"),
1801
                                               msg,
1802
                                               MessageBox::Save | MessageBox::Discard | MessageBox::Cancel,
1803
                                               MessageBox::Save);
1804
            if (result == MessageBox::Save) {
1805
                if (!save()) {
1806
                    return false;
1807
                }
1808
            } else if (result == MessageBox::Cancel) {
1809
                return false;
1810
            }
1811
        }
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);
1816
    }
1817

1818
    if (m_groupView->currentGroup()) {
1819
        m_groupBeforeLock = m_groupView->currentGroup()->uuid();
1820
    } else {
1821
        m_groupBeforeLock = m_db->rootGroup()->uuid();
1822
    }
1823

1824
    auto currentEntry = currentSelectedEntry();
1825
    if (currentEntry) {
1826
        m_entryBeforeLock = currentEntry->uuid();
1827
    }
1828

1829
#ifdef WITH_XC_SSHAGENT
1830
    sshAgent()->databaseLocked(m_db);
1831
#endif
1832

1833
    endSearch();
1834
    clearAllWidgets();
1835
    switchToOpenDatabase(m_db->filePath());
1836

1837
    auto newDb = QSharedPointer<Database>::create(m_db->filePath());
1838
    replaceDatabase(newDb);
1839

1840
    emit databaseLocked();
1841

1842
    return true;
1843
}
1844

1845
void DatabaseWidget::reloadDatabaseFile()
1846
{
1847
    // Ignore reload if we are locked, saving, or currently editing an entry or group
1848
    if (!m_db || isLocked() || isEntryEditActive() || isGroupEditActive() || isSaving()) {
1849
        return;
1850
    }
1851

1852
    m_blockAutoSave = true;
1853

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);
1860

1861
        if (result == MessageBox::No) {
1862
            // Notify everyone the database does not match the file
1863
            m_db->markAsModified();
1864
            return;
1865
        }
1866
    }
1867

1868
    // Lock out interactions
1869
    m_entryView->setDisabled(true);
1870
    m_groupView->setDisabled(true);
1871
    m_tagView->setDisabled(true);
1872
    QApplication::processEvents();
1873

1874
    QString error;
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(
1880
                this,
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,
1884
                MessageBox::Merge);
1885

1886
            if (result == MessageBox::Merge) {
1887
                // Merge the old database into the new one
1888
                Merger merger(m_db.data(), db.data());
1889
                merger.merge();
1890
            }
1891
        }
1892

1893
        QUuid groupBeforeReload = m_db->rootGroup()->uuid();
1894
        if (m_groupView && m_groupView->currentGroup()) {
1895
            groupBeforeReload = m_groupView->currentGroup()->uuid();
1896
        }
1897

1898
        QUuid entryBeforeReload;
1899
        if (m_entryView && m_entryView->currentEntry()) {
1900
            entryBeforeReload = m_entryView->currentEntry()->uuid();
1901
        }
1902

1903
        replaceDatabase(db);
1904
        processAutoOpen();
1905
        restoreGroupEntryFocus(groupBeforeReload, entryBeforeReload);
1906
        m_blockAutoSave = false;
1907
    } else {
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();
1912
    }
1913

1914
    // Return control
1915
    m_entryView->setDisabled(false);
1916
    m_groupView->setDisabled(false);
1917
    m_tagView->setDisabled(false);
1918
}
1919

1920
int DatabaseWidget::numberOfSelectedEntries() const
1921
{
1922
    return m_entryView->numberOfSelectedEntries();
1923
}
1924

1925
int DatabaseWidget::currentEntryIndex() const
1926
{
1927
    return m_entryView->currentEntryIndex();
1928
}
1929

1930
QStringList DatabaseWidget::customEntryAttributes() const
1931
{
1932
    Entry* entry = m_entryView->currentEntry();
1933
    if (!entry) {
1934
        return {};
1935
    }
1936

1937
    return entry->attributes()->customKeys();
1938
}
1939

1940
/*
1941
 * Restores the focus on the group and entry provided
1942
 */
1943
void DatabaseWidget::restoreGroupEntryFocus(const QUuid& groupUuid, const QUuid& entryUuid)
1944
{
1945
    auto group = m_db->rootGroup()->findGroupByUuid(groupUuid);
1946
    if (group) {
1947
        m_groupView->setCurrentGroup(group);
1948
        auto entry = group->findEntryByUuid(entryUuid, false);
1949
        if (entry) {
1950
            m_entryView->setCurrentEntry(entry);
1951
        }
1952
    }
1953
}
1954

1955
bool DatabaseWidget::isGroupSelected() const
1956
{
1957
    return m_groupView->currentGroup();
1958
}
1959

1960
bool DatabaseWidget::currentEntryHasTitle()
1961
{
1962
    auto currentEntry = currentSelectedEntry();
1963
    Q_ASSERT(currentEntry);
1964
    if (!currentEntry) {
1965
        return false;
1966
    }
1967
    return !currentEntry->title().isEmpty();
1968
}
1969

1970
bool DatabaseWidget::currentEntryHasUsername()
1971
{
1972
    auto currentEntry = currentSelectedEntry();
1973
    Q_ASSERT(currentEntry);
1974
    if (!currentEntry) {
1975
        return false;
1976
    }
1977
    return !currentEntry->resolveMultiplePlaceholders(currentEntry->username()).isEmpty();
1978
}
1979

1980
bool DatabaseWidget::currentEntryHasPassword()
1981
{
1982
    auto currentEntry = currentSelectedEntry();
1983
    Q_ASSERT(currentEntry);
1984
    if (!currentEntry) {
1985
        return false;
1986
    }
1987
    return !currentEntry->resolveMultiplePlaceholders(currentEntry->password()).isEmpty();
1988
}
1989

1990
bool DatabaseWidget::currentEntryHasUrl()
1991
{
1992
    auto currentEntry = currentSelectedEntry();
1993
    Q_ASSERT(currentEntry);
1994
    if (!currentEntry) {
1995
        return false;
1996
    }
1997
    return !currentEntry->resolveMultiplePlaceholders(currentEntry->url()).isEmpty();
1998
}
1999

2000
bool DatabaseWidget::currentEntryHasTotp()
2001
{
2002
    auto currentEntry = currentSelectedEntry();
2003
    Q_ASSERT(currentEntry);
2004
    if (!currentEntry) {
2005
        return false;
2006
    }
2007
    return currentEntry->hasTotp();
2008
}
2009

2010
#ifdef WITH_XC_SSHAGENT
2011
bool DatabaseWidget::currentEntryHasSshKey()
2012
{
2013
    Entry* currentEntry = m_entryView->currentEntry();
2014
    Q_ASSERT(currentEntry);
2015
    if (!currentEntry) {
2016
        return false;
2017
    }
2018

2019
    return KeeAgentSettings::inEntryAttachments(currentEntry->attachments());
2020
}
2021
#endif
2022

2023
bool DatabaseWidget::currentEntryHasNotes()
2024
{
2025
    auto currentEntry = currentSelectedEntry();
2026
    Q_ASSERT(currentEntry);
2027
    if (!currentEntry) {
2028
        return false;
2029
    }
2030
    return !currentEntry->resolveMultiplePlaceholders(currentEntry->notes()).isEmpty();
2031
}
2032

2033
bool DatabaseWidget::currentEntryHasAutoTypeEnabled()
2034
{
2035
    auto currentEntry = currentSelectedEntry();
2036
    if (!currentEntry) {
2037
        return false;
2038
    }
2039

2040
    return currentEntry->autoTypeEnabled() && currentEntry->groupAutoTypeEnabled();
2041
}
2042

2043
GroupView* DatabaseWidget::groupView()
2044
{
2045
    return m_groupView;
2046
}
2047

2048
EntryView* DatabaseWidget::entryView()
2049
{
2050
    return m_entryView;
2051
}
2052

2053
/**
2054
 * Save the database to disk.
2055
 *
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.
2059
 *
2060
 * @return true on success
2061
 */
2062
bool DatabaseWidget::save()
2063
{
2064
    // Never allow saving a locked database; it causes corruption
2065
    Q_ASSERT(!isLocked());
2066
    // Release build interlock
2067
    if (isLocked()) {
2068
        // We return true since a save is not required
2069
        return true;
2070
    }
2071

2072
    // Read-only and new databases ask for filename
2073
    if (m_db->filePath().isEmpty()) {
2074
        return saveAs();
2075
    }
2076

2077
    // Prevent recursions and infinite save loops
2078
    m_blockAutoSave = true;
2079
    ++m_saveAttempts;
2080

2081
    QString errorMessage;
2082
    if (performSave(errorMessage)) {
2083
        m_saveAttempts = 0;
2084
        m_blockAutoSave = false;
2085
        m_autosaveTimer->stop(); // stop autosave delay to avoid triggering another save
2086
        return true;
2087
    }
2088

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);
2100
            return save();
2101
        }
2102
    }
2103

2104
    showMessage(tr("Writing the database failed: %1").arg(errorMessage),
2105
                MessageWidget::Error,
2106
                true,
2107
                MessageWidget::LongAutoHideTimeout);
2108

2109
    return false;
2110
}
2111

2112
/**
2113
 * Save database under a new user-selected filename.
2114
 *
2115
 * @return true on success
2116
 */
2117
bool DatabaseWidget::saveAs()
2118
{
2119
    // Never allow saving a locked database; it causes corruption
2120
    Q_ASSERT(!isLocked());
2121
    // Release build interlock
2122
    if (isLocked()) {
2123
        // We return true since a save is not required
2124
        return true;
2125
    }
2126

2127
    QString oldFilePath = m_db->filePath();
2128
    if (!QFileInfo::exists(oldFilePath)) {
2129
        QString defaultFileName = config()->get(Config::DefaultDatabaseFileName).toString();
2130
        oldFilePath =
2131
            QDir::toNativeSeparators(FileDialog::getLastDir("db") + "/"
2132
                                     + (defaultFileName.isEmpty() ? tr("Passwords").append(".kdbx") : defaultFileName));
2133
    }
2134
    const QString newFilePath = fileDialog()->getSaveFileName(
2135
        this, tr("Save database as"), oldFilePath, tr("KeePass 2 Database").append(" (*.kdbx)"), nullptr, nullptr);
2136

2137
    bool ok = false;
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,
2143
                        true,
2144
                        MessageWidget::LongAutoHideTimeout);
2145
        }
2146
    }
2147

2148
    return ok;
2149
}
2150

2151
bool DatabaseWidget::performSave(QString& errorMessage, const QString& fileName)
2152
{
2153
    QPointer<QWidget> focusWidget(qApp->focusWidget());
2154

2155
    // Lock out interactions
2156
    m_entryView->setDisabled(true);
2157
    m_groupView->setDisabled(true);
2158
    m_tagView->setDisabled(true);
2159
    QApplication::processEvents();
2160

2161
    Database::SaveAction saveAction = Database::Atomic;
2162
    if (!config()->get(Config::UseAtomicSaves).toBool()) {
2163
        if (config()->get(Config::UseDirectWriteSaves).toBool()) {
2164
            saveAction = Database::DirectWrite;
2165
        } else {
2166
            saveAction = Database::TempFile;
2167
        }
2168
    }
2169

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();
2176
        }
2177

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);
2186
            }
2187
        }
2188
    }
2189

2190
    bool ok;
2191
    if (fileName.isEmpty()) {
2192
        ok = m_db->save(saveAction, backupFilePath, &errorMessage);
2193
    } else {
2194
        ok = m_db->saveAs(fileName, saveAction, backupFilePath, &errorMessage);
2195
    }
2196

2197
    // Return control
2198
    m_entryView->setDisabled(false);
2199
    m_groupView->setDisabled(false);
2200
    m_tagView->setDisabled(false);
2201

2202
    if (focusWidget && focusWidget->isVisible()) {
2203
        focusWidget->setFocus();
2204
    }
2205

2206
    return ok;
2207
}
2208

2209
/**
2210
 * Save copy of database under a new user-selected filename.
2211
 *
2212
 * @return true on success
2213
 */
2214
bool DatabaseWidget::saveBackup()
2215
{
2216
    while (true) {
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));
2223
        }
2224

2225
        const QString newFilePath = fileDialog()->getSaveFileName(this,
2226
                                                                  tr("Save database backup"),
2227
                                                                  FileDialog::getLastDir("backup", oldFilePath),
2228
                                                                  tr("KeePass 2 Database").append(" (*.kdbx)"),
2229
                                                                  nullptr,
2230
                                                                  nullptr);
2231

2232
        if (!newFilePath.isEmpty()) {
2233
            // Ensure we don't recurse back into this function
2234
            m_db->setFilePath(newFilePath);
2235
            m_saveAttempts = 0;
2236

2237
            bool modified = m_db->isModified();
2238

2239
            if (!save()) {
2240
                // Failed to save, try again
2241
                m_db->setFilePath(oldFilePath);
2242
                continue;
2243
            }
2244

2245
            m_db->setFilePath(oldFilePath);
2246
            if (modified) {
2247
                // Source database is marked as clean when copy is saved, even if source has unsaved changes
2248
                m_db->markAsModified();
2249
            }
2250
            FileDialog::saveLastDir("backup", newFilePath, true);
2251
            return true;
2252
        }
2253

2254
        // Canceled file selection
2255
        return false;
2256
    }
2257
}
2258

2259
void DatabaseWidget::showMessage(const QString& text,
2260
                                 MessageWidget::MessageType type,
2261
                                 bool showClosebutton,
2262
                                 int autoHideTimeout)
2263
{
2264
    m_messageWidget->setCloseButtonVisible(showClosebutton);
2265
    m_messageWidget->showMessage(text, type, autoHideTimeout);
2266
}
2267

2268
void DatabaseWidget::showErrorMessage(const QString& errorMessage)
2269
{
2270
    showMessage(errorMessage, MessageWidget::MessageType::Error);
2271
}
2272

2273
void DatabaseWidget::hideMessage()
2274
{
2275
    if (m_messageWidget->isVisible()) {
2276
        m_messageWidget->animatedHide();
2277
    }
2278
}
2279

2280
bool DatabaseWidget::isRecycleBinSelected() const
2281
{
2282
    return m_groupView->currentGroup() && m_groupView->currentGroup() == m_db->metadata()->recycleBin();
2283
}
2284

2285
void DatabaseWidget::emptyRecycleBin()
2286
{
2287
    if (!isRecycleBinSelected()) {
2288
        return;
2289
    }
2290

2291
    auto result =
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);
2297

2298
    if (result == MessageBox::Empty) {
2299
        m_db->emptyRecycleBin();
2300
    }
2301
}
2302

2303
void DatabaseWidget::processAutoOpen()
2304
{
2305
    Q_ASSERT(m_db);
2306

2307
    auto* autoopenGroup = m_db->rootGroup()->findGroupByPath("/AutoOpen");
2308
    if (!autoopenGroup) {
2309
        return;
2310
    }
2311

2312
    for (const auto* entry : autoopenGroup->entries()) {
2313
        if (entry->url().isEmpty() || (entry->password().isEmpty() && entry->username().isEmpty())) {
2314
            continue;
2315
        }
2316

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
2329
                        loadDb = false;
2330
                        break;
2331
                    } else {
2332
                        // Not matching an exclusion allows loading on all machines
2333
                        loadDb = true;
2334
                    }
2335
                } else if (device.compare(hostName, Qt::CaseInsensitive) == 0) {
2336
                    // Explicitly named for loading
2337
                    loadDb = true;
2338
                }
2339
            }
2340
            if (!loadDb) {
2341
                continue;
2342
            }
2343
        }
2344

2345
        openDatabaseFromEntry(entry);
2346
    }
2347
}
2348

2349
void DatabaseWidget::openDatabaseFromEntry(const Entry* entry, bool inBackground)
2350
{
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);
2356
    }
2357

2358
    QFileInfo dbFileInfo;
2359
    if (databaseUrl.startsWith("file://")) {
2360
        QUrl url(databaseUrl);
2361
        dbFileInfo.setFile(url.toLocalFile());
2362
    } else {
2363
        dbFileInfo.setFile(databaseUrl);
2364
        if (dbFileInfo.isRelative()) {
2365
            QFileInfo currentpath(m_db->filePath());
2366
            dbFileInfo.setFile(currentpath.absoluteDir(), databaseUrl);
2367
        }
2368
    }
2369

2370
    if (!dbFileInfo.isFile()) {
2371
        showErrorMessage(tr("Could not find database file: %1").arg(databaseUrl));
2372
        return;
2373
    }
2374

2375
    QFileInfo keyFileInfo;
2376
    if (!keyFile.isEmpty()) {
2377
        if (keyFile.startsWith("file://")) {
2378
            QUrl keyfileUrl(keyFile);
2379
            keyFileInfo.setFile(keyfileUrl.toLocalFile());
2380
        } else {
2381
            keyFileInfo.setFile(keyFile);
2382
            if (keyFileInfo.isRelative()) {
2383
                QFileInfo currentpath(m_db->filePath());
2384
                keyFileInfo.setFile(currentpath.absoluteDir(), keyFile);
2385
            }
2386
        }
2387
    }
2388

2389
    // Request to open the database file in the background with a password and keyfile
2390
    emit requestOpenDatabase(dbFileInfo.canonicalFilePath(), inBackground, password, keyFileInfo.canonicalFilePath());
2391
}
2392

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.