From d5c87874511ac694b923dcff937dd92bf4e8d2e3 Mon Sep 17 00:00:00 2001 From: Francois Ferrand Date: Wed, 24 Apr 2013 22:38:34 +0200 Subject: [PATCH] Detect background changes to database file. This gives the option to reload the database. TODO: - Settings for reloadBehavior (ask, reloadUnchanged, ignore) - Improve notification, by using a header instead of dialog: nicer, less intrusive, gives more options to user, and works better when multiple databases are open. - Keep tab order on reload. --- src/core/Database.cpp | 5 ++ src/core/Database.h | 1 + src/gui/DatabaseOpenWidget.cpp | 14 +++- src/gui/DatabaseOpenWidget.h | 2 + src/gui/DatabaseTabWidget.cpp | 136 +++++++++++++++++++++++++++++++-- src/gui/DatabaseTabWidget.h | 20 ++++- src/gui/DatabaseWidget.cpp | 16 +++- src/gui/DatabaseWidget.h | 7 +- src/gui/MainWindow.cpp | 9 +++ src/gui/MainWindow.h | 3 +- 10 files changed, 200 insertions(+), 13 deletions(-) diff --git a/src/core/Database.cpp b/src/core/Database.cpp index 3e656873b..d18b1bf87 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -227,6 +227,11 @@ bool Database::verifyKey(const CompositeKey& key) const return (m_key.rawKey() == key.rawKey()); } +CompositeKey Database::key() const +{ + return m_key; +} + void Database::createRecycleBin() { Group* recycleBin = Group::createRecycleBin(); diff --git a/src/core/Database.h b/src/core/Database.h index f4173441f..07ae3aea8 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -88,6 +88,7 @@ public: void setKey(const CompositeKey& key); bool hasKey() const; bool verifyKey(const CompositeKey& key) const; + CompositeKey key() const; void recycleEntry(Entry* entry); void recycleGroup(Group* group); void setEmitModified(bool value); diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp index 4c1e0b4cb..3095572a7 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -91,14 +91,26 @@ void DatabaseOpenWidget::enterKey(const QString& pw, const QString& keyFile) openDatabase(); } +void DatabaseOpenWidget::enterKey(const CompositeKey& masterKey) +{ + if (masterKey.isEmpty()) { + return; + } + openDatabase(masterKey); +} + void DatabaseOpenWidget::openDatabase() { - KeePass2Reader reader; CompositeKey masterKey = databaseKey(); if (masterKey.isEmpty()) { return; } + openDatabase(masterKey); +} +void DatabaseOpenWidget::openDatabase(const CompositeKey& masterKey) +{ + KeePass2Reader reader; QFile file(m_filename); if (!file.open(QIODevice::ReadOnly)) { // TODO: error message diff --git a/src/gui/DatabaseOpenWidget.h b/src/gui/DatabaseOpenWidget.h index 0216de586..9182b4795 100644 --- a/src/gui/DatabaseOpenWidget.h +++ b/src/gui/DatabaseOpenWidget.h @@ -39,6 +39,7 @@ public: ~DatabaseOpenWidget(); void load(const QString& filename); void enterKey(const QString& pw, const QString& keyFile); + void enterKey(const CompositeKey& masterKey); Database* database(); Q_SIGNALS: @@ -49,6 +50,7 @@ protected: protected Q_SLOTS: virtual void openDatabase(); + void openDatabase(const CompositeKey& masterKey); void reject(); private Q_SLOTS: diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index e81db035c..34bf41209 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -20,6 +20,9 @@ #include #include #include +#include +#include +#include #include "autotype/AutoType.h" #include "core/Config.h" @@ -45,7 +48,9 @@ DatabaseManagerStruct::DatabaseManagerStruct() const int DatabaseTabWidget::LastDatabasesCount = 5; DatabaseTabWidget::DatabaseTabWidget(QWidget* parent) - : QTabWidget(parent) + : QTabWidget(parent), + m_fileWatcher(new QFileSystemWatcher(this)), + m_reloadBehavior(ReloadUnmodified) //TODO: setting { DragTabBar* tabBar = new DragTabBar(this); tabBar->setDrawBase(false); @@ -53,6 +58,7 @@ DatabaseTabWidget::DatabaseTabWidget(QWidget* parent) connect(this, SIGNAL(tabCloseRequested(int)), SLOT(closeDatabase(int))); connect(autoType(), SIGNAL(globalShortcutTriggered()), SLOT(performGlobalAutoType())); + connect(m_fileWatcher, SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString))); } DatabaseTabWidget::~DatabaseTabWidget() @@ -92,7 +98,7 @@ void DatabaseTabWidget::openDatabase() } void DatabaseTabWidget::openDatabase(const QString& fileName, const QString& pw, - const QString& keyFile) + const QString& keyFile, const CompositeKey& key) { QFileInfo fileInfo(fileName); QString canonicalFilePath = fileInfo.canonicalFilePath(); @@ -136,12 +142,17 @@ void DatabaseTabWidget::openDatabase(const QString& fileName, const QString& pw, dbStruct.filePath = fileInfo.absoluteFilePath(); dbStruct.canonicalFilePath = canonicalFilePath; dbStruct.fileName = fileInfo.fileName(); + dbStruct.lastModified = fileInfo.lastModified(); insertDatabase(db, dbStruct); + m_fileWatcher->addPath(dbStruct.filePath); updateRecentDatabases(dbStruct.filePath); - if (!pw.isNull() || !keyFile.isEmpty()) { + if (!key.isEmpty()) { + dbStruct.dbWidget->switchToOpenDatabase(dbStruct.filePath, key); + } + else if (!pw.isNull() || !keyFile.isEmpty()) { dbStruct.dbWidget->switchToOpenDatabase(dbStruct.filePath, pw, keyFile); } else { @@ -168,6 +179,111 @@ void DatabaseTabWidget::importKeePass1Database() dbStruct.dbWidget->switchToImportKeepass1(fileName); } +void DatabaseTabWidget::fileChanged(const QString &fileName) +{ + const bool wasEmpty = m_changedFiles.isEmpty(); + m_changedFiles.insert(fileName); + if (wasEmpty && !m_changedFiles.isEmpty()) + QTimer::singleShot(200, this, SLOT(checkReloadDatabases())); +} + +void DatabaseTabWidget::expectFileChange(const DatabaseManagerStruct& dbStruct) +{ + if (dbStruct.filePath.isEmpty()) + return; + m_expectedFileChanges.insert(dbStruct.filePath); +} + +void DatabaseTabWidget::unexpectFileChange(DatabaseManagerStruct& dbStruct) +{ + if (dbStruct.filePath.isEmpty()) + return; + m_expectedFileChanges.remove(dbStruct.filePath); + dbStruct.lastModified = QFileInfo(dbStruct.filePath).lastModified(); +} + +void DatabaseTabWidget::checkReloadDatabases() +{ + QSet changedFiles; + + changedFiles = m_changedFiles.subtract(m_expectedFileChanges); + m_changedFiles.clear(); + + if (changedFiles.isEmpty()) + return; + + Q_FOREACH (DatabaseManagerStruct dbStruct, m_dbList) { + QString filePath = dbStruct.filePath; + Database * db = dbStruct.dbWidget->database(); + + if (!changedFiles.contains(filePath)) + continue; + + QFileInfo fi(filePath); + QDateTime lastModified = fi.lastModified(); + if (dbStruct.lastModified == lastModified) + continue; + + DatabaseWidget::Mode mode = dbStruct.dbWidget->currentMode(); + if (mode == DatabaseWidget::None || mode == DatabaseWidget::LockedMode || !db->hasKey()) + continue; + + if ( (m_reloadBehavior == AlwaysAsk) + || (m_reloadBehavior == ReloadUnmodified && mode == DatabaseWidget::EditMode) + || (m_reloadBehavior == ReloadUnmodified && dbStruct.modified)) { + //TODO: display banner instead, to let user now file has changed and choose to Reload, Overwrite, and SaveAs + // --> less obstrubsive (esp. if multiple DB are open), cleaner UI + if (QMessageBox::warning(this, fi.exists() ? tr("Database file changed") : tr("Database file removed"), + tr("Do you want to discard your changes and reload?"), + QMessageBox::Yes|QMessageBox::No) == QMessageBox::No) + continue; + } + + if (fi.exists()) { + //Ignore/cancel all edits + dbStruct.dbWidget->switchToView(false); + dbStruct.modified = false; + + //Save current group/entry + Uuid currentGroup; + if (Group* group = dbStruct.dbWidget->groupView()->currentGroup()) + currentGroup = group->uuid(); + Uuid currentEntry; + if (Entry* entry = dbStruct.dbWidget->entryView()->currentEntry()) + currentEntry = entry->uuid(); + QString searchText = dbStruct.dbWidget->searchText(); + + //Reload updated db + CompositeKey key = db->key(); + closeDatabase(db); + openDatabase(filePath, QString(), QString(), key); + + //Restore current group/entry + dbStruct = indexDatabaseManagerStruct(count() - 1); + if (dbStruct.dbWidget) { + Database * db = dbStruct.dbWidget->database(); + if (!searchText.isEmpty()) + dbStruct.dbWidget->showSearch(searchText); + if (!currentGroup.isNull()) + if (Group* group = db->resolveGroup(currentGroup)) + dbStruct.dbWidget->groupView()->setCurrentGroup(group); + if (!currentEntry.isNull()) + if (Entry* entry = db->resolveEntry(currentEntry)) + dbStruct.dbWidget->entryView()->setCurrentEntry(entry); + } + + //TODO: keep tab order... + } else { + //Ignore/cancel all edits + dbStruct.dbWidget->switchToView(false); + dbStruct.modified = false; + + //Close database + closeDatabase(dbStruct.dbWidget->database()); + } + } +} + bool DatabaseTabWidget::closeDatabase(Database* db) { Q_ASSERT(db); @@ -219,6 +335,7 @@ void DatabaseTabWidget::deleteDatabase(Database* db) const DatabaseManagerStruct dbStruct = m_dbList.value(db); int index = databaseIndex(db); + m_fileWatcher->removePath(dbStruct.filePath); removeTab(index); toggleTabbar(); m_dbList.remove(db); @@ -260,12 +377,16 @@ void DatabaseTabWidget::saveDatabase(Database* db) if (dbStruct.saveToFilename) { bool result = false; + expectFileChange(dbStruct); + QSaveFile saveFile(dbStruct.filePath); if (saveFile.open(QIODevice::WriteOnly)) { m_writer.writeDatabase(&saveFile, db); result = saveFile.commit(); } + unexpectFileChange(dbStruct); + if (result) { dbStruct.modified = false; updateTabName(db); @@ -283,12 +404,12 @@ void DatabaseTabWidget::saveDatabase(Database* db) void DatabaseTabWidget::saveDatabaseAs(Database* db) { DatabaseManagerStruct& dbStruct = m_dbList[db]; - QString oldFileName; + QString oldFilePath; if (dbStruct.saveToFilename) { - oldFileName = dbStruct.filePath; + oldFilePath = dbStruct.filePath; } QString fileName = fileDialog()->getSaveFileName(this, tr("Save database as"), - oldFileName, tr("KeePass 2 Database").append(" (*.kdbx)")); + oldFilePath, tr("KeePass 2 Database").append(" (*.kdbx)")); if (!fileName.isEmpty()) { bool result = false; @@ -299,15 +420,18 @@ void DatabaseTabWidget::saveDatabaseAs(Database* db) } if (result) { + m_fileWatcher->removePath(oldFilePath); dbStruct.modified = false; dbStruct.saveToFilename = true; QFileInfo fileInfo(fileName); dbStruct.filePath = fileInfo.absoluteFilePath(); dbStruct.canonicalFilePath = fileInfo.canonicalFilePath(); dbStruct.fileName = fileInfo.fileName(); + dbStruct.lastModified = fileInfo.lastModified(); dbStruct.dbWidget->updateFilename(dbStruct.filePath); updateTabName(db); updateRecentDatabases(dbStruct.filePath); + m_fileWatcher->addPath(dbStruct.filePath); } else { QMessageBox::critical(this, tr("Error"), tr("Writing the database failed.") + "\n\n" diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h index bdbe8034c..6b473eb7f 100644 --- a/src/gui/DatabaseTabWidget.h +++ b/src/gui/DatabaseTabWidget.h @@ -18,7 +18,9 @@ #ifndef KEEPASSX_DATABASETABWIDGET_H #define KEEPASSX_DATABASETABWIDGET_H +#include #include +#include #include #include "format/KeePass2Writer.h" @@ -27,6 +29,7 @@ class DatabaseWidget; class DatabaseOpenWidget; class QFile; +class QFileSystemWatcher; struct DatabaseManagerStruct { @@ -39,6 +42,7 @@ struct DatabaseManagerStruct bool saveToFilename; bool modified; bool readOnly; + QDateTime lastModified; }; Q_DECLARE_TYPEINFO(DatabaseManagerStruct, Q_MOVABLE_TYPE); @@ -51,16 +55,24 @@ public: explicit DatabaseTabWidget(QWidget* parent = Q_NULLPTR); ~DatabaseTabWidget(); void openDatabase(const QString& fileName, const QString& pw = QString(), - const QString& keyFile = QString()); + const QString& keyFile = QString(), const CompositeKey& key = CompositeKey()); DatabaseWidget* currentDatabaseWidget(); bool hasLockableDatabases(); static const int LastDatabasesCount; + enum ReloadBehavior { + AlwaysAsk, + ReloadUnmodified, + IgnoreAll + }; + public Q_SLOTS: void newDatabase(); void openDatabase(); void importKeePass1Database(); + void fileChanged(const QString& fileName); + void checkReloadDatabases(); void saveDatabase(int index = -1); void saveDatabaseAs(int index = -1); bool closeDatabase(int index = -1); @@ -96,9 +108,15 @@ private: void insertDatabase(Database* db, const DatabaseManagerStruct& dbStruct); void updateRecentDatabases(const QString& filename); void connectDatabase(Database* newDb, Database* oldDb = Q_NULLPTR); + void expectFileChange(const DatabaseManagerStruct& dbStruct); + void unexpectFileChange(DatabaseManagerStruct& dbStruct); KeePass2Writer m_writer; QHash m_dbList; + QSet m_changedFiles; + QSet m_expectedFileChanges; + QFileSystemWatcher* m_fileWatcher; + ReloadBehavior m_reloadBehavior; }; #endif // KEEPASSX_DATABASETABWIDGET_H diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 277853e5b..68b89761f 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -533,6 +533,13 @@ void DatabaseWidget::switchToOpenDatabase(const QString& fileName, const QString m_databaseOpenWidget->enterKey(password, keyFile); } +void DatabaseWidget::switchToOpenDatabase(const QString &fileName, const CompositeKey& masterKey) +{ + updateFilename(fileName); + switchToOpenDatabase(fileName); + m_databaseOpenWidget->enterKey(masterKey); +} + void DatabaseWidget::switchToImportKeepass1(const QString& fileName) { updateFilename(fileName); @@ -556,10 +563,10 @@ void DatabaseWidget::closeSearch() m_groupView->setCurrentGroup(m_lastGroup); } -void DatabaseWidget::showSearch() +void DatabaseWidget::showSearch(const QString & searchString) { m_searchUi->searchEdit->blockSignals(true); - m_searchUi->searchEdit->clear(); + m_searchUi->searchEdit->setText(searchString); m_searchUi->searchEdit->blockSignals(false); m_searchUi->searchCurrentRadioButton->blockSignals(true); @@ -665,6 +672,11 @@ bool DatabaseWidget::isInSearchMode() return m_entryView->inEntryListMode(); } +QString DatabaseWidget::searchText() +{ + return m_entryView->inEntryListMode() ? m_searchUi->searchEdit->text() : QString(); +} + void DatabaseWidget::clearLastGroup(Group* group) { if (group) { diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index a39b085da..92cb79734 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -37,6 +37,7 @@ class KeePass1OpenWidget; class QFile; class QMenu; class UnlockDatabaseWidget; +class CompositeKey; namespace Ui { class SearchWidget; @@ -63,6 +64,7 @@ public: bool dbHasKey(); bool canDeleteCurrentGoup(); bool isInSearchMode(); + QString searchText(); int addWidget(QWidget* w); void setCurrentIndex(int index); void setCurrentWidget(QWidget* widget); @@ -97,14 +99,16 @@ public Q_SLOTS: void switchToDatabaseSettings(); void switchToOpenDatabase(const QString& fileName); void switchToOpenDatabase(const QString& fileName, const QString& password, const QString& keyFile); + void switchToOpenDatabase(const QString &fileName, const CompositeKey &masterKey); void switchToImportKeepass1(const QString& fileName); + void switchToView(bool accepted); void toggleSearch(); + void showSearch(const QString & searchString = QString()); void emitGroupContextMenuRequested(const QPoint& pos); void emitEntryContextMenuRequested(const QPoint& pos); private Q_SLOTS: void switchBackToEntryEdit(); - void switchToView(bool accepted); void switchToHistoryView(Entry* entry); void switchToEntryEdit(Entry* entry); void switchToEntryEdit(Entry* entry, bool create); @@ -117,7 +121,6 @@ private Q_SLOTS: void search(); void startSearch(); void startSearchTimer(); - void showSearch(); void closeSearch(); private: diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 97a098071..657ecce4b 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -284,6 +284,15 @@ void MainWindow::clearLastDatabases() config()->set("LastDatabases", QVariant()); } +void MainWindow::changeEvent(QEvent *e) +{ + QMainWindow::changeEvent(e); + if (e->type() == QEvent::ActivationChange) { + if (isActiveWindow()) + m_ui->tabWidget->checkReloadDatabases(); + } +} + void MainWindow::openDatabase(const QString& fileName, const QString& pw, const QString& keyFile) { m_ui->tabWidget->openDatabase(fileName, pw, keyFile); diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 60da08010..52472a429 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -41,7 +41,8 @@ public Q_SLOTS: const QString& keyFile = QString()); protected: - void closeEvent(QCloseEvent* event) Q_DECL_OVERRIDE; + void changeEvent(QEvent *e); + void closeEvent(QCloseEvent* event) Q_DECL_OVERRIDE; private Q_SLOTS: void setMenuActionState(DatabaseWidget::Mode mode = DatabaseWidget::None);