From ceb6a0383e4f4960584eed41d0fc4abc946f0b79 Mon Sep 17 00:00:00 2001 From: Felix Geyer Date: Tue, 14 Jul 2015 22:14:34 +0200 Subject: [PATCH] Add ability to export databases to CSV files. Based on implementation by Florian Geyer Closes #57 --- src/CMakeLists.txt | 1 + src/format/CsvExporter.cpp | 103 ++++++++++++++++++++++++++++++++++ src/format/CsvExporter.h | 42 ++++++++++++++ src/gui/DatabaseTabWidget.cpp | 22 ++++++++ src/gui/DatabaseTabWidget.h | 1 + src/gui/MainWindow.cpp | 5 ++ src/gui/MainWindow.ui | 9 +++ tests/CMakeLists.txt | 3 + tests/TestCsvExporter.cpp | 103 ++++++++++++++++++++++++++++++++++ tests/TestCsvExporter.h | 48 ++++++++++++++++ 10 files changed, 337 insertions(+) create mode 100644 src/format/CsvExporter.cpp create mode 100644 src/format/CsvExporter.h create mode 100644 tests/TestCsvExporter.cpp create mode 100644 tests/TestCsvExporter.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 26d194cf7..05f394dc4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -61,6 +61,7 @@ set(keepassx_SOURCES crypto/SymmetricCipher.cpp crypto/SymmetricCipherBackend.h crypto/SymmetricCipherGcrypt.cpp + format/CsvExporter.cpp format/KeePass1.h format/KeePass1Reader.cpp format/KeePass2.h diff --git a/src/format/CsvExporter.cpp b/src/format/CsvExporter.cpp new file mode 100644 index 000000000..11378642f --- /dev/null +++ b/src/format/CsvExporter.cpp @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2015 Florian Geyer + * Copyright (C) 2015 Felix Geyer + * + * 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 . + */ + +#include "CsvExporter.h" + +#include + +#include "core/Database.h" +#include "core/Group.h" + +bool CsvExporter::exportDatabase(const QString& filename, const Database* db) +{ + QFile file(filename); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + m_error = file.errorString(); + return false; + } + return exportDatabase(&file, db); +} + +bool CsvExporter::exportDatabase(QIODevice* device, const Database* db) +{ + QString header; + addColumn(header, "Group"); + addColumn(header, "Title"); + addColumn(header, "Username"); + addColumn(header, "Password"); + addColumn(header, "URL"); + addColumn(header, "Notes"); + header.append("\n"); + + if (device->write(header.toUtf8()) == -1) { + m_error = device->errorString(); + return false; + } + + return writeGroup(device, db->rootGroup()); +} + +QString CsvExporter::errorString() const +{ + return m_error; +} + +bool CsvExporter::writeGroup(QIODevice* device, const Group* group, QString groupPath) +{ + if (!groupPath.isEmpty()) { + groupPath.append("/"); + } + groupPath.append(group->name()); + + Q_FOREACH (const Entry* entry, group->entries()) { + QString line; + + addColumn(line, groupPath); + addColumn(line, entry->title()); + addColumn(line, entry->username()); + addColumn(line, entry->password()); + addColumn(line, entry->url()); + addColumn(line, entry->notes()); + + line.append("\n"); + + if (device->write(line.toUtf8()) == -1) { + m_error = device->errorString(); + return false; + } + } + + Q_FOREACH (const Group* child, group->children()) { + if (!writeGroup(device, child, groupPath)) { + return false; + } + } + + return true; +} + +void CsvExporter::addColumn(QString& str, const QString& column) +{ + if (!str.isEmpty()) { + str.append(","); + } + + str.append("\""); + str.append(QString(column).replace("\"", "\"\"")); + str.append("\""); +} diff --git a/src/format/CsvExporter.h b/src/format/CsvExporter.h new file mode 100644 index 000000000..4040a3505 --- /dev/null +++ b/src/format/CsvExporter.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2015 Florian Geyer + * Copyright (C) 2015 Felix Geyer + * + * 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 . + */ + +#ifndef KEEPASSX_CSVEXPORTER_H +#define KEEPASSX_CSVEXPORTER_H + +#include + +class Database; +class Group; +class QIODevice; + +class CsvExporter +{ +public: + bool exportDatabase(const QString& filename, const Database* db); + bool exportDatabase(QIODevice* device, const Database* db); + QString errorString() const; + +private: + bool writeGroup(QIODevice* device, const Group* group, QString groupPath = QString()); + void addColumn(QString& str, const QString& column); + + QString m_error; +}; + +#endif // KEEPASSX_CSVEXPORTER_H diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index b99a7d629..e36edbee6 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -26,6 +26,7 @@ #include "core/Group.h" #include "core/Metadata.h" #include "core/qsavefile.h" +#include "format/CsvExporter.h" #include "gui/DatabaseWidget.h" #include "gui/DatabaseWidgetStateSync.h" #include "gui/DragTabBar.h" @@ -399,6 +400,27 @@ bool DatabaseTabWidget::saveDatabaseAs(int index) return saveDatabaseAs(indexDatabase(index)); } +void DatabaseTabWidget::exportToCsv() +{ + Database* db = indexDatabase(currentIndex()); + if (!db) { + Q_ASSERT(false); + return; + } + + QString fileName = fileDialog()->getSaveFileName(this, tr("Export database to CSV file"), + QString(), tr("CSV file").append(" (*.csv)")); + if (fileName.isEmpty()) { + return; + } + + CsvExporter csvExporter; + if (!csvExporter.exportDatabase(fileName, db)) { + MessageBox::critical(this, tr("Error"), tr("Writing the CSV file failed.") + "\n\n" + + csvExporter.errorString()); + } +} + void DatabaseTabWidget::changeMasterKey() { currentDatabaseWidget()->switchToMasterKeyChange(); diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h index 2ad3010df..c15408d3d 100644 --- a/src/gui/DatabaseTabWidget.h +++ b/src/gui/DatabaseTabWidget.h @@ -66,6 +66,7 @@ public Q_SLOTS: void importKeePass1Database(); bool saveDatabase(int index = -1); bool saveDatabaseAs(int index = -1); + void exportToCsv(); bool closeDatabase(int index = -1); void closeDatabaseFromSender(); bool closeAllDatabases(); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index d050529f8..1b1292799 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -163,6 +163,8 @@ MainWindow::MainWindow() SLOT(changeDatabaseSettings())); connect(m_ui->actionImportKeePass1, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importKeePass1Database())); + connect(m_ui->actionExportCsv, SIGNAL(triggered()), m_ui->tabWidget, + SLOT(exportToCsv())); connect(m_ui->actionLockDatabases, SIGNAL(triggered()), m_ui->tabWidget, SLOT(lockDatabases())); connect(m_ui->actionQuit, SIGNAL(triggered()), SLOT(close())); @@ -303,6 +305,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionChangeDatabaseSettings->setEnabled(true); m_ui->actionDatabaseSave->setEnabled(true); m_ui->actionDatabaseSaveAs->setEnabled(true); + m_ui->actionExportCsv->setEnabled(true); break; } case DatabaseWidget::EditMode: @@ -326,6 +329,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionChangeDatabaseSettings->setEnabled(false); m_ui->actionDatabaseSave->setEnabled(false); m_ui->actionDatabaseSaveAs->setEnabled(false); + m_ui->actionExportCsv->setEnabled(false); break; default: Q_ASSERT(false); @@ -354,6 +358,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionDatabaseSaveAs->setEnabled(false); m_ui->actionDatabaseClose->setEnabled(false); + m_ui->actionExportCsv->setEnabled(false); } bool inDatabaseTabWidgetOrWelcomeWidget = inDatabaseTabWidget || inWelcomeWidget; diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index eb7001cdf..d55c06b3d 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -120,6 +120,7 @@ + @@ -413,6 +414,14 @@ Notes + + + false + + + Export to CSV file + + diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9b4b7b3c7..5e0a41fa5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -168,6 +168,9 @@ add_unit_test(NAME testentrysearcher SOURCES TestEntrySearcher.cpp MOCS TestEntr add_unit_test(NAME testexporter SOURCES TestExporter.cpp MOCS TestExporter.h LIBS ${TEST_LIBRARIES}) +add_unit_test(NAME testcsvexporter SOURCES TestCsvExporter.cpp MOCS TestCsvExporter.h + LIBS ${TEST_LIBRARIES}) + if(WITH_GUI_TESTS) add_subdirectory(gui) endif(WITH_GUI_TESTS) diff --git a/tests/TestCsvExporter.cpp b/tests/TestCsvExporter.cpp new file mode 100644 index 000000000..5965e605f --- /dev/null +++ b/tests/TestCsvExporter.cpp @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2015 Florian Geyer + * Copyright (C) 2015 Felix Geyer + * + * 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 . + */ + +#include "TestCsvExporter.h" + +#include +#include + +#include "tests.h" + +#include "core/Database.h" +#include "core/Entry.h" +#include "core/Group.h" +#include "crypto/Crypto.h" +#include "format/CsvExporter.h" + +QTEST_GUILESS_MAIN(TestCsvExporter) + +const QString TestCsvExporter::ExpectedHeaderLine = QString("\"Group\",\"Title\",\"Username\",\"Password\",\"URL\",\"Notes\"\n"); + +void TestCsvExporter::init() +{ + m_db = new Database(); + m_csvExporter = new CsvExporter(); +} + +void TestCsvExporter::initTestCase() +{ + Crypto::init(); +} + +void TestCsvExporter::cleanUp() +{ + delete m_db; + delete m_csvExporter; +} + +void TestCsvExporter::testExport() +{ + Group* groupRoot = m_db->rootGroup(); + Group* group= new Group(); + group->setName("Test Group Name"); + group->setParent(groupRoot); + Entry* entry = new Entry(); + entry->setGroup(group); + entry->setTitle("Test Entry Title"); + entry->setUsername("Test Username"); + entry->setPassword("Test Password"); + entry->setUrl("http://test.url"); + entry->setNotes("Test Notes"); + + QBuffer buffer; + QVERIFY(buffer.open(QIODevice::ReadWrite)); + m_csvExporter->exportDatabase(&buffer, m_db); + + QString expectedResult = QString().append(ExpectedHeaderLine).append("\"Test Group Name\",\"Test Entry Title\",\"Test Username\",\"Test Password\",\"http://test.url\",\"Test Notes\"\n"); + + QCOMPARE(QString::fromUtf8(buffer.buffer().constData()), expectedResult); +} + +void TestCsvExporter::testEmptyDatabase() +{ + QBuffer buffer; + QVERIFY(buffer.open(QIODevice::ReadWrite)); + m_csvExporter->exportDatabase(&buffer, m_db); + + QCOMPARE(QString::fromUtf8(buffer.buffer().constData()), ExpectedHeaderLine); +} + +void TestCsvExporter::testNestedGroups() +{ + Group* groupRoot = m_db->rootGroup(); + Group* group= new Group(); + group->setName("Test Group Name"); + group->setParent(groupRoot); + Group* childGroup= new Group(); + childGroup->setName("Test Sub Group Name"); + childGroup->setParent(group); + Entry* entry = new Entry(); + entry->setGroup(childGroup); + entry->setTitle("Test Entry Title"); + + QBuffer buffer; + QVERIFY(buffer.open(QIODevice::ReadWrite)); + m_csvExporter->exportDatabase(&buffer, m_db); + + QCOMPARE(QString::fromUtf8(buffer.buffer().constData()), QString().append(ExpectedHeaderLine).append("\"Test Group Name/Test Sub Group Name\",\"Test Entry Title\",\"\",\"\",\"\",\"\"\n")); +} diff --git a/tests/TestCsvExporter.h b/tests/TestCsvExporter.h new file mode 100644 index 000000000..a71c93655 --- /dev/null +++ b/tests/TestCsvExporter.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2015 Florian Geyer + * Copyright (C) 2015 Felix Geyer + * + * 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 . + */ + +#ifndef KEEPASSX_TESTCSVEXPORTER_H +#define KEEPASSX_TESTCSVEXPORTER_H + +#include + +class Database; +class CsvExporter; + +class TestCsvExporter : public QObject +{ + Q_OBJECT + +public: + static const QString ExpectedHeaderLine; + +private Q_SLOTS: + void init(); + void initTestCase(); + void cleanUp(); + void testExport(); + void testEmptyDatabase(); + void testNestedGroups(); + +private: + Database* m_db; + CsvExporter* m_csvExporter; + +}; + +#endif // KEEPASSX_TESTCSVEXPORTER_H