From 7fb33653addd4138ee157617a03ac8b09ec4253f Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Fri, 11 Nov 2016 17:58:47 -0500 Subject: [PATCH] Implemented major autoreload functionality * Ignore autoreload on save / save-as * Consolidated db save code * Corrected bug (crash) in merge entry code due to not cloning the entry * Enhanced known modified status of database * Implemented test cases for autoreload --- src/core/Group.cpp | 16 +++--- src/gui/DatabaseTabWidget.cpp | 46 ++++++++-------- src/gui/DatabaseTabWidget.h | 2 +- src/gui/DatabaseWidget.cpp | 78 +++++++++++++++++++++++---- src/gui/DatabaseWidget.h | 9 +++- src/gui/SettingsWidgetGeneral.ui | 4 +- tests/data/NewDatabase.kdbx | Bin 15150 -> 8350 bytes tests/gui/TestGui.cpp | 88 +++++++++++++++++++++++++++---- tests/gui/TestGui.h | 2 + 9 files changed, 190 insertions(+), 55 deletions(-) diff --git a/src/core/Group.cpp b/src/core/Group.cpp index eb293a935..2028c7828 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -547,7 +547,7 @@ void Group::merge(const Group* other) if (!findEntry(entry->uuid())) { entry->clone(Entry::CloneNoFlags)->setGroup(this); } else { - resolveConflict(this->findEntry(entry->uuid()), entry); + resolveConflict(findEntry(entry->uuid()), entry); } } @@ -555,8 +555,8 @@ void Group::merge(const Group* other) const QList dbChildren = other->children(); for (Group* group : dbChildren) { // groups are searched by name instead of uuid - if (this->findChildByName(group->name())) { - this->findChildByName(group->name())->merge(group); + if (findChildByName(group->name())) { + findChildByName(group->name())->merge(group); } else { group->setParent(this); } @@ -765,24 +765,24 @@ void Group::resolveConflict(Entry* existingEntry, Entry* otherEntry) Entry* clonedEntry; - switch(this->mergeMode()) { + switch(mergeMode()) { case KeepBoth: // if one entry is newer, create a clone and add it to the group if (timeExisting > timeOther) { clonedEntry = otherEntry->clone(Entry::CloneNoFlags); clonedEntry->setGroup(this); - this->markOlderEntry(clonedEntry); + markOlderEntry(clonedEntry); } else if (timeExisting < timeOther) { clonedEntry = otherEntry->clone(Entry::CloneNoFlags); clonedEntry->setGroup(this); - this->markOlderEntry(existingEntry); + markOlderEntry(existingEntry); } break; case KeepNewer: if (timeExisting < timeOther) { // only if other entry is newer, replace existing one - this->removeEntry(existingEntry); - this->addEntry(otherEntry); + removeEntry(existingEntry); + addEntry(otherEntry->clone(Entry::CloneNoFlags)); } break; diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index d4001501d..af6907001 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -221,6 +221,7 @@ void DatabaseTabWidget::importKeePass1Database() Database* db = new Database(); DatabaseManagerStruct dbStruct; dbStruct.dbWidget = new DatabaseWidget(db, this); + dbStruct.dbWidget->databaseModified(); dbStruct.modified = true; insertDatabase(db, dbStruct); @@ -312,17 +313,28 @@ bool DatabaseTabWidget::closeAllDatabases() bool DatabaseTabWidget::saveDatabase(Database* db) { DatabaseManagerStruct& dbStruct = m_dbList[db]; + // temporarily disable autoreload + dbStruct.dbWidget->ignoreNextAutoreload(); if (dbStruct.saveToFilename) { QSaveFile saveFile(dbStruct.canonicalFilePath); if (saveFile.open(QIODevice::WriteOnly)) { + // write the database to the file m_writer.writeDatabase(&saveFile, db); if (m_writer.hasError()) { MessageBox::critical(this, tr("Error"), tr("Writing the database failed.") + "\n\n" + m_writer.errorString()); return false; } - if (!saveFile.commit()) { + + if (saveFile.commit()) { + // successfully saved database file + dbStruct.modified = false; + dbStruct.dbWidget->databaseSaved(); + updateTabName(db); + return true; + } + else { MessageBox::critical(this, tr("Error"), tr("Writing the database failed.") + "\n\n" + saveFile.errorString()); return false; @@ -333,10 +345,6 @@ bool DatabaseTabWidget::saveDatabase(Database* db) + saveFile.errorString()); return false; } - - dbStruct.modified = false; - updateTabName(db); - return true; } else { return saveDatabaseAs(db); @@ -390,22 +398,14 @@ bool DatabaseTabWidget::saveDatabaseAs(Database* db) } } - QSaveFile saveFile(fileName); - if (!saveFile.open(QIODevice::WriteOnly)) { - MessageBox::critical(this, tr("Error"), tr("Writing the database failed.") + "\n\n" - + saveFile.errorString()); - return false; - } + // setup variables so saveDatabase succeeds + dbStruct.saveToFilename = true; + dbStruct.canonicalFilePath = fileName; - m_writer.writeDatabase(&saveFile, db); - if (m_writer.hasError()) { - MessageBox::critical(this, tr("Error"), tr("Writing the database failed.") + "\n\n" - + m_writer.errorString()); - return false; - } - if (!saveFile.commit()) { - MessageBox::critical(this, tr("Error"), tr("Writing the database failed.") + "\n\n" - + saveFile.errorString()); + if (!saveDatabase(db)) { + // failed to save, revert back + dbStruct.saveToFilename = false; + dbStruct.canonicalFilePath = oldFileName; return false; } @@ -626,7 +626,7 @@ void DatabaseTabWidget::insertDatabase(Database* db, const DatabaseManagerStruct setCurrentIndex(index); connectDatabase(db); connect(dbStruct.dbWidget, SIGNAL(closeRequest()), SLOT(closeDatabaseFromSender())); - connect(dbStruct.dbWidget, SIGNAL(databaseChanged(Database*)), SLOT(changeDatabase(Database*))); + connect(dbStruct.dbWidget, SIGNAL(databaseChanged(Database*, bool)), SLOT(changeDatabase(Database*, bool))); connect(dbStruct.dbWidget, SIGNAL(unlockedDatabase()), SLOT(updateTabNameFromDbWidgetSender())); connect(dbStruct.dbWidget, SIGNAL(unlockedDatabase()), SLOT(emitDatabaseUnlockedFromDbWidgetSender())); } @@ -744,6 +744,7 @@ void DatabaseTabWidget::modified() if (!dbStruct.modified) { dbStruct.modified = true; + dbStruct.dbWidget->databaseModified(); updateTabName(db); } } @@ -765,7 +766,7 @@ void DatabaseTabWidget::updateLastDatabases(const QString& filename) } } -void DatabaseTabWidget::changeDatabase(Database* newDb) +void DatabaseTabWidget::changeDatabase(Database* newDb, bool unsavedChanges) { Q_ASSERT(sender()); Q_ASSERT(!m_dbList.contains(newDb)); @@ -773,6 +774,7 @@ void DatabaseTabWidget::changeDatabase(Database* newDb) DatabaseWidget* dbWidget = static_cast(sender()); Database* oldDb = databaseFromDatabaseWidget(dbWidget); DatabaseManagerStruct dbStruct = m_dbList[oldDb]; + dbStruct.modified = unsavedChanges; m_dbList.remove(oldDb); m_dbList.insert(newDb, dbStruct); diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h index 7d095b560..24bdbde2f 100644 --- a/src/gui/DatabaseTabWidget.h +++ b/src/gui/DatabaseTabWidget.h @@ -91,7 +91,7 @@ private Q_SLOTS: void updateTabNameFromDbWidgetSender(); void modified(); void toggleTabbar(); - void changeDatabase(Database* newDb); + void changeDatabase(Database* newDb, bool unsavedChanges); void emitActivateDatabaseChanged(); void emitDatabaseUnlockedFromDbWidgetSender(); diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index bf620b6d5..89e7bf689 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -29,7 +29,6 @@ #include #include #include -#include #include "autotype/AutoType.h" #include "core/Config.h" @@ -162,9 +161,14 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) connect(m_unlockDatabaseDialog, SIGNAL(unlockDone(bool)), SLOT(unlockDatabase(bool))); connect(&m_fileWatcher, SIGNAL(fileChanged(QString)), this, SLOT(onWatchedFileChanged())); connect(&m_fileWatchTimer, SIGNAL(timeout()), this, SLOT(reloadDatabaseFile())); + connect(&m_ignoreWatchTimer, SIGNAL(timeout()), this, SLOT(onWatchedFileChanged())); connect(this, SIGNAL(currentChanged(int)), this, SLOT(emitCurrentModeChanged())); + m_databaseModified = false; + m_fileWatchTimer.setSingleShot(true); + m_ignoreWatchTimer.setSingleShot(true); + m_ignoreNextAutoreload = false; m_searchCaseSensitive = false; m_searchCurrentGroup = false; @@ -298,7 +302,7 @@ void DatabaseWidget::replaceDatabase(Database* db) Database* oldDb = m_db; m_db = db; m_groupView->changeDatabase(m_db); - Q_EMIT databaseChanged(m_db); + Q_EMIT databaseChanged(m_db, m_databaseModified); delete oldDb; } @@ -818,6 +822,16 @@ void DatabaseWidget::switchToImportKeepass1(const QString& fileName) setCurrentWidget(m_keepass1OpenWidget); } +void DatabaseWidget::databaseModified() +{ + m_databaseModified = true; +} + +void DatabaseWidget::databaseSaved() +{ + m_databaseModified = false; +} + void DatabaseWidget::search(const QString& searchtext) { if (searchtext.isEmpty()) @@ -956,15 +970,34 @@ void DatabaseWidget::lock() void DatabaseWidget::updateFilename(const QString& fileName) { + if (! m_filename.isEmpty()) { + m_fileWatcher.removePath(m_filename); + } + + m_fileWatcher.addPath(fileName); m_filename = fileName; } +void DatabaseWidget::ignoreNextAutoreload() +{ + m_ignoreNextAutoreload = true; + m_ignoreWatchTimer.start(100); +} + void DatabaseWidget::onWatchedFileChanged() { - if (m_fileWatchTimer.isActive()) - return; + if (m_ignoreNextAutoreload) { + // Reset the watch + m_ignoreNextAutoreload = false; + m_ignoreWatchTimer.stop(); + m_fileWatcher.addPath(m_filename); + } + else { + if (m_fileWatchTimer.isActive()) + return; - m_fileWatchTimer.start(500); + m_fileWatchTimer.start(500); + } } void DatabaseWidget::reloadDatabaseFile() @@ -972,16 +1005,16 @@ void DatabaseWidget::reloadDatabaseFile() if (m_db == nullptr) return; - // TODO: Also check if db is currently modified before reloading if (! config()->get("AutoReloadOnChange").toBool()) { // Ask if we want to reload the db - QMessageBox::StandardButton mb = MessageBox::question(this, tr("Reload database file"), + QMessageBox::StandardButton mb = MessageBox::question(this, tr("Autoreload Request"), tr("The database file has changed. Do you want to load the changes?"), QMessageBox::Yes | QMessageBox::No); if (mb == QMessageBox::No) { - // TODO: taint database - + // Notify everyone the database does not match the file + emit m_db->modified(); + m_databaseModified = true; // Rewatch the database file m_fileWatcher.addPath(m_filename); return; @@ -993,14 +1026,37 @@ void DatabaseWidget::reloadDatabaseFile() if (file.open(QIODevice::ReadOnly)) { Database* db = reader.readDatabase(&file, database()->key()); if (db != nullptr) { + if (m_databaseModified) { + // Ask if we want to merge changes into new database + QMessageBox::StandardButton mb = MessageBox::question(this, tr("Merge Request"), + tr("The database file has changed and you have unsaved changes." + "Do you want to merge your changes?"), + QMessageBox::Yes | QMessageBox::No); + + if (mb == QMessageBox::Yes) { + // Merge the old database into the new one + m_db->setEmitModified(false); + db->merge(m_db); + } + else { + // Since we are accepting the new file as-is, internally mark as unmodified + // TODO: when saving is moved out of DatabaseTabWidget, this should be replaced + m_databaseModified = false; + } + } + replaceDatabase(db); } else { - // TODO: error message for failure to read the new db + MessageBox::critical(this, tr("Autoreload Failed"), + tr("Could not parse or unlock the new database file while attempting" + "to autoreload this database.")); } } else { - // TODO: error message for failure to open db file + MessageBox::critical(this, tr("Autoreload Failed"), + tr("Could not open the new database file while attempting to autoreload" + "this database.")); } // Rewatch the database file diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 0f52ea08d..1031def44 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -92,13 +92,14 @@ public: EntryView* entryView(); void showUnlockDialog(); void closeUnlockDialog(); + void ignoreNextAutoreload(); Q_SIGNALS: void closeRequest(); void currentModeChanged(DatabaseWidget::Mode mode); void groupChanged(); void entrySelectionChanged(); - void databaseChanged(Database* newDb); + void databaseChanged(Database* newDb, bool unsavedChanges); void databaseMerged(Database* mergedDb); void groupContextMenuRequested(const QPoint& globalPos); void entryContextMenuRequested(const QPoint& globalPos); @@ -136,6 +137,8 @@ public Q_SLOTS: void switchToOpenMergeDatabase(const QString& fileName); void switchToOpenMergeDatabase(const QString& fileName, const QString& password, const QString& keyFile); void switchToImportKeepass1(const QString& fileName); + void databaseModified(); + void databaseSaved(); // Search related slots void search(const QString& searchtext); void setSearchCaseSensitive(bool state); @@ -194,8 +197,12 @@ private: bool m_searchCaseSensitive; bool m_searchCurrentGroup; + // Autoreload QFileSystemWatcher m_fileWatcher; QTimer m_fileWatchTimer; + bool m_ignoreNextAutoreload; + QTimer m_ignoreWatchTimer; + bool m_databaseModified; }; #endif // KEEPASSX_DATABASEWIDGET_H diff --git a/src/gui/SettingsWidgetGeneral.ui b/src/gui/SettingsWidgetGeneral.ui index 223e2b9af..dabc1eea4 100644 --- a/src/gui/SettingsWidgetGeneral.ui +++ b/src/gui/SettingsWidgetGeneral.ui @@ -7,7 +7,7 @@ 0 0 684 - 430 + 459 @@ -58,7 +58,7 @@ - Automatically reload when the database is expernally modified + Automatically reload the database when modified externally diff --git a/tests/data/NewDatabase.kdbx b/tests/data/NewDatabase.kdbx index f45929de27c5bd3a879623ab351175917c85b06a..4e77724c7e8cfdf22bcd951ccfcaa4ae7f49d460 100644 GIT binary patch delta 8348 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 literal 15150 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 KWiOj9qs7dxo(k&$g=mPi;WD*t_-6> zy1-#MdL^Z%`RhFg!gCJy$UZmg@Ne%R&{TXFzMaF#DKs(vkbwWWBI}oKI8YucYOvb8 zbDHRHgr`e0&zA4s@WnVl3Vptjj(2u36XBBPjO{HhenahHdZOHzztp$b8*0$Mub_gx zz%zWi#ig^Xf%>kqkzICm*_-tYcKJFsAka3_q+~f?ml&2!rRxR=FquK*Ukbyo zFf!EoB^GHl&5x}{Jm^_S4A|K8A!Q3YSydQLon;Yy_iS?C%SO<$Q6nWN%M##=95gcl!ixWQ*`8QDm0~o|7yOy*7p3exJ?v z{bfR{_$Z1Is16vKi8elJ(`;Bt@1;aRw3qT{20pAn@0rbzND%Bu~r($*GIb=9Hn*Z zp#~XK>P2AZN@a3dpvB%UNK~61)x2{v&mHU|sJ{Py64y-@{yzY0OSjkSy+J1|`3SyT zRk4sp+XkMc!xqzom!e=Ge`Z58CUdDFO9ASGC=svPD>Odzoa&v*NyDcR2RoYf{+;|WWGYqy-t;~K2(LDC;W9aY)yhEj_R>!1j z^ZjC`ZF`D_V+#j)K2Tn0j9BnTA~5Bg1xl&R^_k`~&hEm6yBdK^!rqxP3Ds-BagqGp zG%>)_*uz#O*w6XJL&`~9&6#vqOeiHZ@;+uJrl}RA6xC7`*hKoN=;bg_6T@L2unbN9 zNbKyms?fKkpFOFb7pT3b;HCxFn)CrOw|envbKyEJt^WAb?OzU}Z@p3%Fh2F3J<6ui zGpyL5|HxjDt( z`2PAkQ^dQNt>KJt?=!Lq&A8JNX0c)tWa(z3SI0d6ALH2l>rs%*|8hec`@W9YRtYHh z+jy&y1h+TQs5Z_54j!~=bB9HP^d~H;DqL|K)HJtR5eSj|crP>3S92)j|9{9w3d(+% z$=d!!PWj;5O)0vt(Ypk^O)_aN^pMEnsVwc%$45p zQiJ4<95v1i?XOw!9y)Y0d+D3i%lDPD!hJNxUIu1)R`;{9(XP?Ctq2#}#dE{L!1K99$IiT*dnj8IW!JY$Cw{rE4&~RDid!NB0@qFDEn`#FMhiXo)6KR|jOv zRr`DqRi_ajCSv3X9Jzq!#uU~O^QS)%+FL*5%b9$gP62J0faVoB+hq~bmHrQ(?N;p+ ziSwkPfH=js6h51vc>zr=4$3L+pME{>@-@A5osnu^`)asIcjh#;puWT+fKghj(_Dl2 zjhFcg{n2JYQwK?I3JZT8_Pl!3;sLByVhL!Dp~g83VRx&5GGB6ZDud6r_F`qTxc za`kuDKYD3pgKH6$t#c+b7aY88^!5T{#6u2w&!;!xbn&va^A$1tp618fCL^JOUD{5>l(I5rfN5?CHmnDlPh ziSXoSuXWEsadD!Mx7!Q^d#%vk`y+4TMa@9)0C0zCDTh-Y;$$ZWB{)gCX4xbn1^`6N zNzhm#>SBhD2z(NaZ$O<8UcQQ!P(-|ASJD9kEn*8QYqx*2sVvCr)%MgST+=5ke~Bo$ zqWDp?&X6_@nZlMYU?bLSvAXej?7F;+5$|y*92lVor?4)exps*$I-NOqfzD#EbvSvj zb)tB!F|no%GW;2IF|q;{E`(n ziq<8oh_6lyY((tmpData.size()))); - m_dbFile.close(); - - m_dbFileName = QFileInfo(m_dbFile).fileName(); } // Every test starts with opening the temp database void TestGui::init() { + // Write the temp storage to a temp database file for use in our tests + QVERIFY(m_dbFile.open()); + QCOMPARE(m_dbFile.write(m_dbData), static_cast((m_dbData.size()))); + m_dbFile.close(); + + m_dbFileName = QFileInfo(m_dbFile).fileName(); + fileDialog()->setNextFileName(m_dbFile.fileName()); triggerAction("actionDatabaseOpen"); @@ -110,8 +110,8 @@ void TestGui::cleanup() void TestGui::testMergeDatabase() { - // this triggers a warning. Perhaps similar to https://bugreports.qt.io/browse/QTBUG-49623 ? - QSignalSpy dbMergeSpy(m_tabWidget->currentWidget(), SIGNAL(databaseMerged(Database*))); + // It is safe to ignore the warning this line produces + QSignalSpy dbMergeSpy(m_dbWidget, SIGNAL(databaseMerged(Database*))); // set file to merge from fileDialog()->setNextFileName(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx")); @@ -139,6 +139,74 @@ void TestGui::testMergeDatabase() QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 1); } +void TestGui::testAutoreloadDatabase() +{ + config()->set("AutoReloadOnChange", false); + + // Load the MergeDatabase.kdbx file into temporary storage + QByteArray tmpData; + QFile mergeDbFile(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx")); + QVERIFY(mergeDbFile.open(QIODevice::ReadOnly)); + QVERIFY(Tools::readAllFromDevice(&mergeDbFile, tmpData)); + mergeDbFile.close(); + + // Test accepting new file in autoreload + MessageBox::setNextAnswer(QMessageBox::Yes); + // Overwrite the current database with the temp data + QVERIFY(m_dbFile.open()); + QVERIFY(m_dbFile.write(tmpData, static_cast(tmpData.size()))); + m_dbFile.close(); + Tools::wait(1500); + + m_db = m_dbWidget->database(); + + // the General group contains one entry from the new db data + QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 1); + QVERIFY(! m_tabWidget->tabText(m_tabWidget->currentIndex()).endsWith("*")); + + // Reset the state + cleanup(); + init(); + + // Test rejecting new file in autoreload + MessageBox::setNextAnswer(QMessageBox::No); + // Overwrite the current temp database with a new file + m_dbFile.open(); + QVERIFY(m_dbFile.write(tmpData, static_cast(tmpData.size()))); + m_dbFile.close(); + Tools::wait(1500); + + m_db = m_dbWidget->database(); + + // Ensure the merge did not take place + QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 0); + QVERIFY(m_tabWidget->tabText(m_tabWidget->currentIndex()).endsWith("*")); + + // Reset the state + cleanup(); + init(); + + // Test accepting a merge of edits into autoreload + // Turn on autoload so we only get one messagebox (for the merge) + config()->set("AutoReloadOnChange", true); + + // Modify some entries + testEditEntry(); + + // This is saying yes to merging the entries + MessageBox::setNextAnswer(QMessageBox::Yes); + // Overwrite the current database with the temp data + QVERIFY(m_dbFile.open()); + QVERIFY(m_dbFile.write(tmpData, static_cast(tmpData.size()))); + m_dbFile.close(); + Tools::wait(1500); + + m_db = m_dbWidget->database(); + + QCOMPARE(m_db->rootGroup()->findChildByName("General")->entries().size(), 1); + QVERIFY(m_tabWidget->tabText(m_tabWidget->currentIndex()).endsWith("*")); +} + void TestGui::testTabs() { QCOMPARE(m_tabWidget->count(), 1); diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h index 82ffc1850..02e8da1d3 100644 --- a/tests/gui/TestGui.h +++ b/tests/gui/TestGui.h @@ -39,6 +39,7 @@ private Q_SLOTS: void cleanupTestCase(); void testMergeDatabase(); + void testAutoreloadDatabase(); void testTabs(); void testEditEntry(); void testAddEntry(); @@ -64,6 +65,7 @@ private: MainWindow* m_mainWindow; DatabaseTabWidget* m_tabWidget; DatabaseWidget* m_dbWidget; + QByteArray m_dbData; QTemporaryFile m_dbFile; QString m_dbFileName; Database* m_db;