keepassxc/src/gui/DatabaseTabWidget.cpp

644 lines
18 KiB
C++
Raw Normal View History

2011-11-13 08:55:20 -05:00
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
2011-11-13 08:55:20 -05:00
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "DatabaseTabWidget.h"
2011-11-13 08:55:20 -05:00
#include <QFileInfo>
#include <QPushButton>
2018-03-31 16:01:30 -04:00
#include <QTabWidget>
2011-11-13 08:55:20 -05:00
#include "autotype/AutoType.h"
2018-03-31 16:01:30 -04:00
#include "core/AsyncTask.h"
2012-05-27 17:01:14 -04:00
#include "core/Config.h"
2011-11-13 08:55:20 -05:00
#include "core/Database.h"
2018-03-31 16:01:30 -04:00
#include "core/Global.h"
#include "core/Group.h"
2011-11-13 08:55:20 -05:00
#include "core/Metadata.h"
#include "core/Tools.h"
#include "format/CsvExporter.h"
#include "gui/Clipboard.h"
#include "gui/DatabaseOpenDialog.h"
2011-11-13 08:55:20 -05:00
#include "gui/DatabaseWidget.h"
#include "gui/DatabaseWidgetStateSync.h"
#include "gui/DragTabBar.h"
#include "gui/FileDialog.h"
#include "gui/MessageBox.h"
#include "gui/entry/EntryView.h"
#include "gui/group/GroupView.h"
#ifdef Q_OS_MACOS
#include "gui/macutils/MacUtils.h"
#endif
#include "gui/wizard/NewDatabaseWizard.h"
2011-11-13 08:55:20 -05:00
DatabaseTabWidget::DatabaseTabWidget(QWidget* parent)
: QTabWidget(parent)
, m_dbWidgetStateSync(new DatabaseWidgetStateSync(this))
, m_dbWidgetPendingLock(nullptr)
, m_databaseOpenDialog(new DatabaseOpenDialog())
2011-11-13 08:55:20 -05:00
{
auto* tabBar = new DragTabBar(this);
setTabBar(tabBar);
setDocumentMode(true);
// clang-format off
connect(this, SIGNAL(tabCloseRequested(int)), SLOT(closeDatabaseTab(int)));
connect(this, SIGNAL(currentChanged(int)), SLOT(emitActivateDatabaseChanged()));
connect(this, SIGNAL(activateDatabaseChanged(DatabaseWidget*)),
m_dbWidgetStateSync, SLOT(setActive(DatabaseWidget*)));
connect(autoType(), SIGNAL(globalShortcutTriggered()), SLOT(performGlobalAutoType()));
connect(autoType(), SIGNAL(autotypePerformed()), SLOT(relockPendingDatabase()));
connect(autoType(), SIGNAL(autotypeRejected()), SLOT(relockPendingDatabase()));
// clang-format on
2011-11-13 08:55:20 -05:00
}
DatabaseTabWidget::~DatabaseTabWidget()
{
}
2012-06-29 18:22:07 -04:00
void DatabaseTabWidget::toggleTabbar()
{
if (count() > 1) {
tabBar()->show();
} else {
tabBar()->hide();
}
}
/**
* Helper method for invoking the new database wizard.
* The user of this method MUST take ownership of the returned pointer.
*
* @return pointer to the configured new database, nullptr on failure
*/
QSharedPointer<Database> DatabaseTabWidget::execNewDatabaseWizard()
{
// use QScopedPointer to ensure deletion after scope ends, but still parent
// it to this to make it modal and allow easier access in unit tests
QScopedPointer<NewDatabaseWizard> wizard(new NewDatabaseWizard(this));
if (!wizard->exec()) {
return {};
}
auto db = wizard->takeDatabase();
if (!db) {
return {};
}
Q_ASSERT(db->key());
Q_ASSERT(db->kdf());
if (!db->key() || !db->kdf()) {
MessageBox::critical(this,
tr("Database creation error"),
tr("The created database has no key or KDF, refusing to save it.\n"
"This is definitely a bug, please report it to the developers."),
Customize buttons on MessageBox and confirm before recycling (#2376) * Add confirmation prompt before moving groups to the recycling bin Spawn a yes/no QMessage box when "Delete Group" is selected on a group that is not already in the recycle bin (note: the prompt for deletion from the recycle bin was already implemented). This follows the same pattern and language as entry deletion. Fixes #2125 * Make prompts for destructive operations use action words on buttons Replace yes/no, yes/cancel (and other such buttons on prompts that cause data to be destroyed) use language that indicates the action that it is going to take. This makes destructive/unsafe and/or irreversible operations more clear to the user. Address feedback on PR #2376 * Refactor MessageBox class to allow for custom buttons Replaces arguments and return values of type QMessageBox::StandardButton(s) with MessageBox::Button(s), which reimplements the entire set of QMessageBox::StandardButton and allows for custom KeePassXC buttons, such as "Skip". Modifies all calls to MessageBox functions to use MessageBox::Button(s). Addresses feedback on #2376 * Remove MessageBox::addButton in favor of map lookup Replaced the switch statement mechanism in MessageBox::addButton with a map lookup to address CodeFactor Complex Method issue. This has a side-effect of a small performance/cleanliness increase, as an extra QPushButton is no longer created/destroyed (to obtain it's label text) everytime a MessageBox button based on QMessageBox::StandardButton is created; now the text is obtained once, at application start up.
2018-12-19 23:14:11 -05:00
MessageBox::Ok,
MessageBox::Ok);
return {};
}
return db;
}
void DatabaseTabWidget::newDatabase()
{
auto db = execNewDatabaseWizard();
if (!db) {
return;
}
addDatabaseTab(new DatabaseWidget(db, this));
db->markAsModified();
}
void DatabaseTabWidget::openDatabase()
2011-11-13 08:55:20 -05:00
{
QString filter = QString("%1 (*.kdbx);;%2 (*)").arg(tr("KeePass 2 Database"), tr("All files"));
QString fileName = fileDialog()->getOpenFileName(this, tr("Open database"), "", filter);
2011-11-13 08:55:20 -05:00
if (!fileName.isEmpty()) {
addDatabaseTab(fileName);
2011-11-13 08:55:20 -05:00
}
}
/**
* Add a new database tab or switch to an existing one if the
* database has been opened already.
*
* @param filePath database file path
* @param password optional, password to unlock database
* @param inBackground optional, don't focus tab after opening
*/
void DatabaseTabWidget::addDatabaseTab(const QString& filePath, bool inBackground, const QString& password)
2011-11-13 08:55:20 -05:00
{
QFileInfo fileInfo(filePath);
QString canonicalFilePath = fileInfo.canonicalFilePath();
if (canonicalFilePath.isEmpty()) {
emit messageGlobal(tr("The database file does not exist or is not accessible."), MessageWidget::Error);
return;
}
for (int i = 0, c = count(); i < c; ++i) {
auto* dbWidget = databaseWidgetFromIndex(i);
Q_ASSERT(dbWidget);
if (dbWidget && dbWidget->database()->filePath() == canonicalFilePath) {
if (!password.isEmpty()) {
dbWidget->performUnlockDatabase(password);
}
if (!inBackground) {
// switch to existing tab if file is already open
setCurrentIndex(indexOf(dbWidget));
}
return;
}
}
auto* dbWidget = new DatabaseWidget(QSharedPointer<Database>::create(filePath), this);
addDatabaseTab(dbWidget, inBackground);
if (!password.isEmpty()) {
dbWidget->performUnlockDatabase(password);
}
updateLastDatabases(filePath);
}
/**
* Add a new database tab containing the given DatabaseWidget
* @param filePath
* @param inBackground optional, don't focus tab after opening
*/
void DatabaseTabWidget::addDatabaseTab(DatabaseWidget* dbWidget, bool inBackground)
{
Q_ASSERT(dbWidget->database());
int index = addTab(dbWidget, "");
updateTabName(index);
toggleTabbar();
2018-03-31 16:01:30 -04:00
if (!inBackground) {
setCurrentIndex(index);
}
connect(dbWidget, SIGNAL(databaseFilePathChanged(QString, QString)), SLOT(updateTabName()));
connect(
dbWidget, SIGNAL(requestOpenDatabase(QString, bool, QString)), SLOT(addDatabaseTab(QString, bool, QString)));
connect(dbWidget, SIGNAL(closeRequest()), SLOT(closeDatabaseTabFromSender()));
connect(dbWidget, SIGNAL(databaseModified()), SLOT(updateTabName()));
connect(dbWidget, SIGNAL(databaseSaved()), SLOT(updateTabName()));
connect(dbWidget, SIGNAL(databaseUnlocked()), SLOT(updateTabName()));
connect(dbWidget, SIGNAL(databaseUnlocked()), SLOT(emitDatabaseLockChanged()));
connect(dbWidget, SIGNAL(databaseLocked()), SLOT(updateTabName()));
connect(dbWidget, SIGNAL(databaseLocked()), SLOT(emitDatabaseLockChanged()));
}
2011-11-13 08:55:20 -05:00
void DatabaseTabWidget::importCsv()
{
2018-03-13 15:55:37 -04:00
QString filter = QString("%1 (*.csv);;%2 (*)").arg(tr("CSV file"), tr("All files"));
QString fileName = fileDialog()->getOpenFileName(this, tr("Select CSV file"), "", filter);
if (fileName.isEmpty()) {
return;
}
auto db = execNewDatabaseWizard();
if (!db) {
return;
}
auto* dbWidget = new DatabaseWidget(db, this);
addDatabaseTab(dbWidget);
dbWidget->switchToCsvImport(fileName);
}
void DatabaseTabWidget::mergeDatabase()
{
2018-05-18 17:06:28 -04:00
auto dbWidget = currentDatabaseWidget();
if (dbWidget && !dbWidget->isLocked()) {
2018-05-18 17:06:28 -04:00
QString filter = QString("%1 (*.kdbx);;%2 (*)").arg(tr("KeePass 2 Database"), tr("All files"));
const QString fileName = fileDialog()->getOpenFileName(this, tr("Merge database"), QString(), filter);
2018-05-18 17:06:28 -04:00
if (!fileName.isEmpty()) {
mergeDatabase(fileName);
}
}
}
void DatabaseTabWidget::mergeDatabase(const QString& filePath)
{
unlockDatabaseInDialog(currentDatabaseWidget(), DatabaseOpenDialog::Intent::Merge, filePath);
}
void DatabaseTabWidget::importKeePass1Database()
{
2018-03-13 15:55:37 -04:00
QString filter = QString("%1 (*.kdb);;%2 (*)").arg(tr("KeePass 1 database"), tr("All files"));
2018-03-31 16:01:30 -04:00
QString fileName = fileDialog()->getOpenFileName(this, tr("Open KeePass 1 database"), QString(), filter);
if (fileName.isEmpty()) {
return;
}
auto db = QSharedPointer<Database>::create();
auto* dbWidget = new DatabaseWidget(db, this);
addDatabaseTab(dbWidget);
dbWidget->switchToImportKeepass1(fileName);
2011-11-13 08:55:20 -05:00
}
/**
* Attempt to close the current database and remove its tab afterwards.
*
* @param index index of the database tab to close
* @return true if database was closed successully
*/
bool DatabaseTabWidget::closeCurrentDatabaseTab()
2011-11-13 08:55:20 -05:00
{
return closeDatabaseTab(currentIndex());
}
/**
* Attempt to close the database tab that sent the close request.
*
* @param index index of the database tab to close
* @return true if database was closed successully
*/
bool DatabaseTabWidget::closeDatabaseTabFromSender()
{
return closeDatabaseTab(qobject_cast<DatabaseWidget*>(sender()));
}
/**
* Attempt to close a database and remove its tab afterwards.
*
* @param index index of the database tab to close
* @return true if database was closed successully
*/
bool DatabaseTabWidget::closeDatabaseTab(int index)
2012-06-29 18:22:07 -04:00
{
return closeDatabaseTab(qobject_cast<DatabaseWidget*>(widget(index)));
2011-11-13 08:55:20 -05:00
}
/**
* Attempt to close a database and remove its tab afterwards.
*
* @param dbWidget \link DatabaseWidget to close
* @return true if database was closed successully
*/
bool DatabaseTabWidget::closeDatabaseTab(DatabaseWidget* dbWidget)
{
int tabIndex = indexOf(dbWidget);
if (!dbWidget || tabIndex < 0) {
return false;
2011-12-24 13:22:42 -05:00
}
QString filePath = dbWidget->database()->filePath();
if (!dbWidget->close()) {
return false;
}
removeTab(tabIndex);
dbWidget->deleteLater();
toggleTabbar();
emit databaseClosed(filePath);
return true;
}
/**
* Attempt to close all opened databases.
* The attempt will be aborted with the first database that cannot be closed.
*
* @return true if all databases could be closed.
*/
bool DatabaseTabWidget::closeAllDatabaseTabs()
2011-11-13 08:55:20 -05:00
{
while (count() > 0) {
if (!closeDatabaseTab(0)) {
return false;
}
}
return true;
2011-11-13 08:55:20 -05:00
}
bool DatabaseTabWidget::saveDatabase(int index)
{
if (index == -1) {
index = currentIndex();
}
return databaseWidgetFromIndex(index)->save();
}
bool DatabaseTabWidget::saveDatabaseAs(int index)
2011-12-24 13:22:42 -05:00
{
if (index == -1) {
index = currentIndex();
2011-12-24 13:22:42 -05:00
}
auto* dbWidget = databaseWidgetFromIndex(index);
bool ok = dbWidget->saveAs();
if (ok) {
updateLastDatabases(dbWidget->database()->filePath());
}
return ok;
}
void DatabaseTabWidget::closeDatabaseFromSender()
{
auto* dbWidget = qobject_cast<DatabaseWidget*>(sender());
Q_ASSERT(dbWidget);
closeDatabaseTab(dbWidget);
2011-12-24 13:22:42 -05:00
}
void DatabaseTabWidget::exportToCsv()
{
auto db = databaseWidgetFromIndex(currentIndex())->database();
if (!db) {
Q_ASSERT(false);
return;
}
2018-03-31 16:01:30 -04:00
QString fileName = fileDialog()->getSaveFileName(
this, tr("Export database to CSV file"), QString(), tr("CSV file").append(" (*.csv)"), nullptr, nullptr, "csv");
if (fileName.isEmpty()) {
return;
}
CsvExporter csvExporter;
if (!csvExporter.exportDatabase(fileName, db)) {
2018-03-31 16:01:30 -04:00
emit messageGlobal(tr("Writing the CSV file failed.").append("\n").append(csvExporter.errorString()),
MessageWidget::Error);
}
}
void DatabaseTabWidget::changeMasterKey()
{
currentDatabaseWidget()->switchToMasterKeyChange();
}
void DatabaseTabWidget::changeDatabaseSettings()
{
currentDatabaseWidget()->switchToDatabaseSettings();
}
bool DatabaseTabWidget::isReadOnly(int index) const
{
if (count() == 0) {
return false;
}
if (index == -1) {
index = currentIndex();
}
auto db = databaseWidgetFromIndex(index)->database();
return db && db->isReadOnly();
}
bool DatabaseTabWidget::isModified(int index) const
{
if (count() == 0) {
return false;
}
if (index == -1) {
index = currentIndex();
}
auto db = databaseWidgetFromIndex(index)->database();
return db && db->isModified();
}
bool DatabaseTabWidget::canSave(int index) const
{
return !isReadOnly(index) && isModified(index);
}
bool DatabaseTabWidget::hasLockableDatabases() const
2017-03-21 22:01:44 -04:00
{
for (int i = 0, c = count(); i < c; ++i) {
if (!databaseWidgetFromIndex(i)->isLocked()) {
return true;
}
2017-03-21 22:01:44 -04:00
}
return false;
2017-03-21 22:01:44 -04:00
}
/**
* Get the tab's (original) display name without platform-specific
* mangling that may occur when reading back the actual widget's \link tabText()
*
* @param index tab index
* @return tab name
*/
QString DatabaseTabWidget::tabName(int index)
2011-11-13 08:55:20 -05:00
{
if (index == -1 || index > count()) {
return "";
}
2011-11-13 08:55:20 -05:00
auto* dbWidget = databaseWidgetFromIndex(index);
auto db = dbWidget->database();
Q_ASSERT(db);
if (!db) {
return "";
}
2011-11-13 08:55:20 -05:00
QString tabName;
if (!db->filePath().isEmpty()) {
QFileInfo fileInfo(db->filePath());
if (db->metadata()->name().isEmpty()) {
tabName = fileInfo.fileName();
} else {
2011-12-25 13:47:41 -05:00
tabName = db->metadata()->name();
}
2011-12-25 13:47:41 -05:00
setTabToolTip(index, fileInfo.absoluteFilePath());
} else {
if (db->metadata()->name().isEmpty()) {
tabName = tr("New Database");
} else {
tabName = tr("%1 [New Database]", "Database tab name modifier").arg(db->metadata()->name());
}
2011-11-13 08:55:20 -05:00
}
2012-10-12 06:12:00 -04:00
if (dbWidget->isLocked()) {
tabName = tr("%1 [Locked]", "Database tab name modifier").arg(tabName);
2012-10-12 06:12:00 -04:00
}
if (db->isReadOnly()) {
tabName = tr("%1 [Read-only]", "Database tab name modifier").arg(tabName);
}
2012-10-12 06:12:00 -04:00
if (db->isModified()) {
tabName.append("*");
2011-11-13 08:55:20 -05:00
}
return tabName;
2011-11-13 08:55:20 -05:00
}
/**
* Update of the given tab index or of the sending
* DatabaseWidget if `index` == -1.
*/
void DatabaseTabWidget::updateTabName(int index)
{
auto* dbWidget = databaseWidgetFromIndex(index);
if (!dbWidget) {
dbWidget = qobject_cast<DatabaseWidget*>(sender());
}
Q_ASSERT(dbWidget);
if (!dbWidget) {
return;
}
index = indexOf(dbWidget);
setTabText(index, tabName(index));
emit tabNameChanged();
}
DatabaseWidget* DatabaseTabWidget::databaseWidgetFromIndex(int index) const
{
return qobject_cast<DatabaseWidget*>(widget(index));
}
DatabaseWidget* DatabaseTabWidget::currentDatabaseWidget()
{
return qobject_cast<DatabaseWidget*>(currentWidget());
2012-10-12 06:12:00 -04:00
}
void DatabaseTabWidget::lockDatabases()
{
for (int i = 0, c = count(); i < c; ++i) {
auto dbWidget = databaseWidgetFromIndex(i);
if (dbWidget->lock() && dbWidget->database()->filePath().isEmpty()) {
// If we locked a database without a file close the tab
closeDatabaseTab(dbWidget);
}
2012-10-12 06:12:00 -04:00
}
}
/**
* Unlock a database with an unlock popup dialog.
*
* @param dbWidget DatabaseWidget which to connect signals to
* @param intent intent for unlocking
*/
void DatabaseTabWidget::unlockDatabaseInDialog(DatabaseWidget* dbWidget, DatabaseOpenDialog::Intent intent)
{
unlockDatabaseInDialog(dbWidget, intent, dbWidget->database()->filePath());
}
/**
* Unlock a database with an unlock popup dialog.
*
* @param dbWidget DatabaseWidget which to connect signals to
* @param intent intent for unlocking
* @param file path of the database to be unlocked
*/
void DatabaseTabWidget::unlockDatabaseInDialog(DatabaseWidget* dbWidget,
DatabaseOpenDialog::Intent intent,
const QString& filePath)
{
m_databaseOpenDialog->setTargetDatabaseWidget(dbWidget);
m_databaseOpenDialog->setIntent(intent);
m_databaseOpenDialog->setFilePath(filePath);
#ifdef Q_OS_MACOS
if (intent == DatabaseOpenDialog::Intent::AutoType || intent == DatabaseOpenDialog::Intent::Browser) {
macUtils()->raiseOwnWindow();
Tools::wait(500);
}
#endif
m_databaseOpenDialog->show();
m_databaseOpenDialog->raise();
m_databaseOpenDialog->activateWindow();
}
/**
* 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
*/
void DatabaseTabWidget::relockPendingDatabase()
{
if (!m_dbWidgetPendingLock || !config()->get("security/relockautotype").toBool()) {
return;
}
if (m_dbWidgetPendingLock->isLocked() || !m_dbWidgetPendingLock->database()->hasKey()) {
m_dbWidgetPendingLock = nullptr;
return;
}
m_dbWidgetPendingLock->lock();
m_dbWidgetPendingLock = nullptr;
}
2012-05-27 17:01:14 -04:00
void DatabaseTabWidget::updateLastDatabases(const QString& filename)
{
if (!config()->get("RememberLastDatabases").toBool()) {
config()->set("LastDatabases", QVariant());
2018-03-31 16:01:30 -04:00
} else {
2012-05-27 17:01:14 -04:00
QStringList lastDatabases = config()->get("LastDatabases", QVariant()).toStringList();
lastDatabases.prepend(filename);
lastDatabases.removeDuplicates();
while (lastDatabases.count() > config()->get("NumberOfRememberedLastDatabases").toInt()) {
2012-05-27 17:01:14 -04:00
lastDatabases.removeLast();
}
config()->set("LastDatabases", lastDatabases);
}
}
void DatabaseTabWidget::emitActivateDatabaseChanged()
{
emit activateDatabaseChanged(currentDatabaseWidget());
}
void DatabaseTabWidget::emitDatabaseLockChanged()
2016-10-08 12:55:05 -04:00
{
auto* dbWidget = qobject_cast<DatabaseWidget*>(sender());
Q_ASSERT(dbWidget);
if (!dbWidget) {
return;
}
if (dbWidget->isLocked()) {
emit databaseLocked(dbWidget);
} else {
emit databaseUnlocked(dbWidget);
}
}
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());
}
}
// TODO: allow for database selection during Auto-Type instead of using the current tab
if (!unlockedDatabases.isEmpty()) {
autoType()->performGlobalAutoType(unlockedDatabases);
} else if (count() > 0) {
if (config()->get("security/relockautotype").toBool()) {
m_dbWidgetPendingLock = currentDatabaseWidget();
}
unlockDatabaseInDialog(currentDatabaseWidget(), DatabaseOpenDialog::Intent::AutoType);
}
}