Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow selecting any open database in unlock dialog #5427

Merged
merged 2 commits into from
Aug 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions src/browser/BrowserService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -814,11 +814,8 @@ QList<Entry*> BrowserService::confirmEntries(QList<Entry*>& pwEntriesToConfirm,
}
}

#ifdef Q_OS_MAC
// Re-hide the application if it wasn't visible before
// only affects macOS because dialogs force the main window to show
hideWindow();
#endif

m_dialogActive = false;

Expand Down Expand Up @@ -1306,12 +1303,10 @@ void BrowserService::databaseLocked(DatabaseWidget* dbWidget)
void BrowserService::databaseUnlocked(DatabaseWidget* dbWidget)
{
if (dbWidget) {
#ifdef Q_OS_MAC
if (m_bringToFrontRequested) {
m_bringToFrontRequested = false;
hideWindow();
}
#endif

QJsonObject msg;
msg["action"] = QString("database-unlocked");
Expand Down
118 changes: 101 additions & 17 deletions src/gui/DatabaseOpenDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@
#include "DatabaseOpenDialog.h"

#include "DatabaseOpenWidget.h"
#include "DatabaseTabWidget.h"
#include "DatabaseWidget.h"

#include <QFileInfo>
#include <QLayout>
#include <QShortcut>

#ifdef Q_OS_WIN
#include <QtPlatformHeaders/QWindowsWindowFunctions>
Expand All @@ -29,37 +32,109 @@
DatabaseOpenDialog::DatabaseOpenDialog(QWidget* parent)
: QDialog(parent)
, m_view(new DatabaseOpenWidget(this))
, m_tabBar(new QTabBar(this))
{
setWindowTitle(tr("Unlock Database - KeePassXC"));
setWindowFlags(Qt::Dialog | Qt::WindowStaysOnTopHint);
// block input to the main window/application while the dialog is open
setWindowModality(Qt::ApplicationModal);
#ifdef Q_OS_WIN
QWindowsWindowFunctions::setWindowActivationBehavior(QWindowsWindowFunctions::AlwaysActivateWindow);
#endif
connect(m_view, SIGNAL(dialogFinished(bool)), this, SLOT(complete(bool)));
connect(m_view, &DatabaseOpenWidget::dialogFinished, this, &DatabaseOpenDialog::complete);

m_tabBar->setAutoHide(true);
m_tabBar->setExpanding(false);
connect(m_tabBar, &QTabBar::currentChanged, this, &DatabaseOpenDialog::tabChanged);

auto* layout = new QVBoxLayout();
layout->setMargin(0);
setLayout(layout);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
layout->addWidget(m_tabBar);
layout->addWidget(m_view);
setLayout(layout);
setMinimumWidth(700);

// set up Ctrl+PageUp and Ctrl+PageDown shortcuts to cycle tabs
auto* shortcut = new QShortcut(Qt::CTRL + Qt::Key_PageUp, this);
shortcut->setContext(Qt::WidgetWithChildrenShortcut);
connect(shortcut, &QShortcut::activated, this, [this]() { selectTabOffset(-1); });
shortcut = new QShortcut(Qt::CTRL + Qt::Key_PageDown, this);
shortcut->setContext(Qt::WidgetWithChildrenShortcut);
connect(shortcut, &QShortcut::activated, this, [this]() { selectTabOffset(1); });
}

void DatabaseOpenDialog::setFilePath(const QString& filePath)
void DatabaseOpenDialog::selectTabOffset(int offset)
{
m_view->load(filePath);
if (offset == 0 || m_tabBar->count() <= 1) {
return;
}
int tab = m_tabBar->currentIndex() + offset;
int last = m_tabBar->count() - 1;
if (tab < 0) {
tab = last;
} else if (tab > last) {
tab = 0;
}
m_tabBar->setCurrentIndex(tab);
}

void DatabaseOpenDialog::addDatabaseTab(DatabaseWidget* dbWidget)
{
Q_ASSERT(dbWidget);
if (!dbWidget) {
return;
}

// important - we must add the DB widget first, because addTab will fire
// tabChanged immediately which will look for a dbWidget in the list
m_tabDbWidgets.append(dbWidget);
QFileInfo fileInfo(dbWidget->database()->filePath());
m_tabBar->addTab(fileInfo.fileName());
Q_ASSERT(m_tabDbWidgets.count() == m_tabBar->count());
}

void DatabaseOpenDialog::setActiveDatabaseTab(DatabaseWidget* dbWidget)
{
if (!dbWidget) {
return;
}
int index = m_tabDbWidgets.indexOf(dbWidget);
if (index != -1) {
m_tabBar->setCurrentIndex(index);
}
}

void DatabaseOpenDialog::tabChanged(int index)
{
if (index < 0 || index >= m_tabDbWidgets.count()) {
return;
}

if (m_tabDbWidgets.count() == m_tabBar->count()) {
DatabaseWidget* dbWidget = m_tabDbWidgets[index];
setTarget(dbWidget, dbWidget->database()->filePath());
} else {
// if these list sizes don't match, there's a bug somewhere nearby
qWarning("DatabaseOpenDialog: mismatch between tab count %d and DB count %d",
m_tabBar->count(),
m_tabDbWidgets.count());
}
}

/**
* Set target DatabaseWidget to which signals are connected.
*
* @param dbWidget database widget
* Sets the target DB and reloads the UI.
*/
void DatabaseOpenDialog::setTargetDatabaseWidget(DatabaseWidget* dbWidget)
void DatabaseOpenDialog::setTarget(DatabaseWidget* dbWidget, const QString& filePath)
{
if (m_dbWidget) {
disconnect(this, nullptr, m_dbWidget, nullptr);
// reconnect finished signal to new dbWidget, then reload the UI
if (m_currentDbWidget) {
disconnect(this, &DatabaseOpenDialog::dialogFinished, m_currentDbWidget, nullptr);
}
m_dbWidget = dbWidget;
connect(this, &DatabaseOpenDialog::dialogFinished, dbWidget, &DatabaseWidget::unlockDatabase);

m_currentDbWidget = dbWidget;
m_view->load(filePath);
}

void DatabaseOpenDialog::setIntent(DatabaseOpenDialog::Intent intent)
Expand All @@ -77,13 +152,21 @@ void DatabaseOpenDialog::clearForms()
m_view->clearForms();
m_db.reset();
m_intent = Intent::None;
if (m_dbWidget) {
disconnect(this, nullptr, m_dbWidget, nullptr);
m_dbWidget = nullptr;
if (m_currentDbWidget) {
disconnect(this, &DatabaseOpenDialog::dialogFinished, m_currentDbWidget, nullptr);
}
m_currentDbWidget.clear();
m_tabDbWidgets.clear();

// block signals while removing tabs so that tabChanged doesn't get called
m_tabBar->blockSignals(true);
while (m_tabBar->count() > 0) {
m_tabBar->removeTab(0);
}
m_tabBar->blockSignals(false);
}

QSharedPointer<Database> DatabaseOpenDialog::database()
QSharedPointer<Database> DatabaseOpenDialog::database() const
{
return m_db;
}
Expand All @@ -98,6 +181,7 @@ void DatabaseOpenDialog::complete(bool accepted)
} else {
reject();
}
emit dialogFinished(accepted, m_dbWidget);

emit dialogFinished(accepted, m_currentDbWidget);
clearForms();
}
16 changes: 12 additions & 4 deletions src/gui/DatabaseOpenDialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
#include "core/Global.h"

#include <QDialog>
#include <QList>
#include <QPointer>
#include <QTabBar>

class Database;
class DatabaseWidget;
Expand All @@ -41,23 +43,29 @@ class DatabaseOpenDialog : public QDialog
};

explicit DatabaseOpenDialog(QWidget* parent = nullptr);
void setFilePath(const QString& filePath);
void setTargetDatabaseWidget(DatabaseWidget* dbWidget);
void setTarget(DatabaseWidget* dbWidget, const QString& filePath);
void addDatabaseTab(DatabaseWidget* dbWidget);
void setActiveDatabaseTab(DatabaseWidget* dbWidget);
void setIntent(Intent intent);
Intent intent() const;
QSharedPointer<Database> database();
QSharedPointer<Database> database() const;
void clearForms();

signals:
void dialogFinished(bool accepted, DatabaseWidget* dbWidget);

public slots:
void complete(bool accepted);
void tabChanged(int index);

private:
void selectTabOffset(int offset);

QPointer<DatabaseOpenWidget> m_view;
QPointer<QTabBar> m_tabBar;
QSharedPointer<Database> m_db;
QPointer<DatabaseWidget> m_dbWidget;
QList<QPointer<DatabaseWidget>> m_tabDbWidgets;
QPointer<DatabaseWidget> m_currentDbWidget;
Intent m_intent = Intent::None;
};

Expand Down
94 changes: 76 additions & 18 deletions src/gui/DatabaseTabWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ DatabaseTabWidget::DatabaseTabWidget(QWidget* parent)
connect(autoType(), SIGNAL(autotypePerformed()), SLOT(relockPendingDatabase()));
connect(autoType(), SIGNAL(autotypeRejected()), SLOT(relockPendingDatabase()));
connect(m_databaseOpenDialog.data(), &DatabaseOpenDialog::dialogFinished,
this, &DatabaseTabWidget::databaseUnlockDialogFinished);
this, &DatabaseTabWidget::handleDatabaseUnlockDialogFinished);
// clang-format on

#ifdef Q_OS_MACOS
Expand Down Expand Up @@ -664,11 +664,43 @@ void DatabaseTabWidget::unlockDatabaseInDialog(DatabaseWidget* dbWidget,
DatabaseOpenDialog::Intent intent,
const QString& filePath)
{
m_databaseOpenDialog->setTargetDatabaseWidget(dbWidget);
m_databaseOpenDialog->clearForms();
m_databaseOpenDialog->setIntent(intent);
m_databaseOpenDialog->setTarget(dbWidget, filePath);
displayUnlockDialog();
}

/**
* Unlock a database with an unlock popup dialog.
* The dialog allows the user to select any open & locked database.
*
* @param intent intent for unlocking
*/
void DatabaseTabWidget::unlockAnyDatabaseInDialog(DatabaseOpenDialog::Intent intent)
{
m_databaseOpenDialog->clearForms();
m_databaseOpenDialog->setIntent(intent);
m_databaseOpenDialog->setFilePath(filePath);

// add a tab to the dialog for each open unlocked database
for (int i = 0, c = count(); i < c; ++i) {
auto* dbWidget = databaseWidgetFromIndex(i);
if (dbWidget && dbWidget->isLocked()) {
m_databaseOpenDialog->addDatabaseTab(dbWidget);
}
}
// default to the current tab
m_databaseOpenDialog->setActiveDatabaseTab(currentDatabaseWidget());
displayUnlockDialog();
}

/**
* Display the unlock dialog after it's been initialized.
* This is an internal method, it should only be called by unlockDatabaseInDialog or unlockAnyDatabaseInDialog.
*/
void DatabaseTabWidget::displayUnlockDialog()
{
#ifdef Q_OS_MACOS
auto intent = m_databaseOpenDialog->intent();
if (intent == DatabaseOpenDialog::Intent::AutoType || intent == DatabaseOpenDialog::Intent::Browser) {
macUtils()->raiseOwnWindow();
Tools::wait(200);
Expand All @@ -680,6 +712,29 @@ void DatabaseTabWidget::unlockDatabaseInDialog(DatabaseWidget* dbWidget,
m_databaseOpenDialog->activateWindow();
}

/**
* Actions to take when the unlock dialog has completed.
*/
void DatabaseTabWidget::handleDatabaseUnlockDialogFinished(bool accepted, DatabaseWidget* dbWidget)
{
// change the active tab to the database that was just unlocked in the dialog
auto intent = m_databaseOpenDialog->intent();
if (accepted && intent != DatabaseOpenDialog::Intent::Merge) {
int index = indexOf(dbWidget);
if (index != -1) {
setCurrentIndex(index);
}
}

// if unlocked for AutoType, set pending lock flag if needed
if (intent == DatabaseOpenDialog::Intent::AutoType && config()->get(Config::Security_RelockAutoType).toBool()) {
m_dbWidgetPendingLock = dbWidget;
}

// signal other objects that the dialog finished
emit databaseUnlockDialogFinished(accepted, dbWidget);
}

/**
* This function relock the pending database when autotype has been performed successfully
* A database is marked as pending when it's unlocked after a global Auto-Type invocation
Expand Down Expand Up @@ -737,30 +792,33 @@ void DatabaseTabWidget::emitDatabaseLockChanged()

void DatabaseTabWidget::performGlobalAutoType()
{
QList<QSharedPointer<Database>> unlockedDatabases;

for (int i = 0, c = count(); i < c; ++i) {
auto* dbWidget = databaseWidgetFromIndex(i);
if (!dbWidget->isLocked()) {
unlockedDatabases.append(dbWidget->database());
auto currentDbWidget = currentDatabaseWidget();
if (!currentDbWidget) {
// no open databases, nothing to do
return;
} else if (currentDbWidget->isLocked()) {
// Current database tab is locked, match behavior of browser unlock - prompt with
// the unlock dialog even if there are additional unlocked open database tabs.
unlockAnyDatabaseInDialog(DatabaseOpenDialog::Intent::AutoType);
} else {
// current database is unlocked, use it for AutoType along with any other unlocked databases
QList<QSharedPointer<Database>> unlockedDatabases;
for (int i = 0, c = count(); i < c; ++i) {
auto* dbWidget = databaseWidgetFromIndex(i);
if (!dbWidget->isLocked()) {
unlockedDatabases.append(dbWidget->database());
}
}
}

// TODO: allow for database selection during Auto-Type instead of using the current tab
if (!unlockedDatabases.isEmpty()) {
Q_ASSERT(!unlockedDatabases.isEmpty());
autoType()->performGlobalAutoType(unlockedDatabases);
} else if (count() > 0) {
if (config()->get(Config::Security_RelockAutoType).toBool()) {
m_dbWidgetPendingLock = currentDatabaseWidget();
}
unlockDatabaseInDialog(currentDatabaseWidget(), DatabaseOpenDialog::Intent::AutoType);
}
}

void DatabaseTabWidget::performBrowserUnlock()
{
auto dbWidget = currentDatabaseWidget();
if (dbWidget && dbWidget->isLocked()) {
unlockDatabaseInDialog(dbWidget, DatabaseOpenDialog::Intent::Browser);
unlockAnyDatabaseInDialog(DatabaseOpenDialog::Intent::Browser);
}
}
3 changes: 3 additions & 0 deletions src/gui/DatabaseTabWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,14 @@ private slots:
void toggleTabbar();
void emitActiveDatabaseChanged();
void emitDatabaseLockChanged();
void handleDatabaseUnlockDialogFinished(bool accepted, DatabaseWidget* dbWidget);

private:
QSharedPointer<Database> execNewDatabaseWizard();
void updateLastDatabases(const QString& filename);
bool warnOnExport();
void unlockAnyDatabaseInDialog(DatabaseOpenDialog::Intent intent);
void displayUnlockDialog();

QPointer<DatabaseWidgetStateSync> m_dbWidgetStateSync;
QPointer<DatabaseWidget> m_dbWidgetPendingLock;
Expand Down
Loading