From 3318f05b583a68c58b66211ce2e7e85f67c9ca21 Mon Sep 17 00:00:00 2001 From: seatedscribe Date: Sat, 6 Feb 2016 01:48:27 +0100 Subject: [PATCH] CSV import feature delivered. --- src/CMakeLists.txt | 5 + src/core/CsvParser.cpp | 411 ++++++++++++++++++++ src/core/CsvParser.h | 100 +++++ src/format/KeePass2Writer.cpp | 2 + src/gui/ChangeMasterKeyWidget.cpp | 6 +- src/gui/ChangeMasterKeyWidget.h | 3 +- src/gui/ChangeMasterKeyWidget.ui | 12 +- src/gui/DatabaseTabWidget.cpp | 17 + src/gui/DatabaseTabWidget.h | 1 + src/gui/DatabaseWidget.cpp | 28 +- src/gui/DatabaseWidget.h | 4 + src/gui/MainWindow.cpp | 4 +- src/gui/MainWindow.ui | 24 +- src/gui/csvImport/CsvImportWidget.cpp | 258 +++++++++++++ src/gui/csvImport/CsvImportWidget.h | 78 ++++ src/gui/csvImport/CsvImportWidget.ui | 524 ++++++++++++++++++++++++++ src/gui/csvImport/CsvImportWizard.cpp | 72 ++++ src/gui/csvImport/CsvImportWizard.h | 57 +++ src/gui/csvImport/CsvParserModel.cpp | 139 +++++++ src/gui/csvImport/CsvParserModel.h | 60 +++ src/main.cpp | 2 +- tests/CMakeLists.txt | 11 +- tests/TestCsvParser.cpp | 421 +++++++++++++++++++++ tests/TestCsvParser.h | 71 ++++ 24 files changed, 2282 insertions(+), 28 deletions(-) create mode 100644 src/core/CsvParser.cpp create mode 100644 src/core/CsvParser.h create mode 100644 src/gui/csvImport/CsvImportWidget.cpp create mode 100644 src/gui/csvImport/CsvImportWidget.h create mode 100644 src/gui/csvImport/CsvImportWidget.ui create mode 100644 src/gui/csvImport/CsvImportWizard.cpp create mode 100644 src/gui/csvImport/CsvImportWizard.h create mode 100644 src/gui/csvImport/CsvParserModel.cpp create mode 100644 src/gui/csvImport/CsvParserModel.h create mode 100644 tests/TestCsvParser.cpp create mode 100644 tests/TestCsvParser.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 837a3e7a8..73c6532ba 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -40,6 +40,7 @@ set(keepassx_SOURCES autotype/test/AutoTypeTestInterface.h core/AutoTypeAssociations.cpp core/Config.cpp + core/CsvParser.cpp core/Database.cpp core/DatabaseIcons.cpp core/Endian.cpp @@ -103,6 +104,9 @@ set(keepassx_SOURCES gui/SortFilterHideProxyModel.cpp gui/UnlockDatabaseWidget.cpp gui/WelcomeWidget.cpp + gui/csvImport/CsvImportWidget.cpp + gui/csvImport/CsvImportWizard.cpp + gui/csvImport/CsvParserModel.cpp gui/entry/AutoTypeAssociationsModel.cpp gui/entry/EditEntryWidget.cpp gui/entry/EditEntryWidget_p.h @@ -133,6 +137,7 @@ set(keepassx_SOURCES_MAINEXE set(keepassx_FORMS gui/AboutDialog.ui gui/ChangeMasterKeyWidget.ui + gui/csvImport/CsvImportWidget.ui gui/DatabaseOpenWidget.ui gui/DatabaseSettingsWidget.ui gui/EditWidget.ui diff --git a/src/core/CsvParser.cpp b/src/core/CsvParser.cpp new file mode 100644 index 000000000..11ee102ba --- /dev/null +++ b/src/core/CsvParser.cpp @@ -0,0 +1,411 @@ +/* + * Copyright (C) 2015 Enrico Mariotti + * + * 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 +#include +#include "core/Tools.h" +#include "CsvParser.h" + +CsvParser::CsvParser() + : m_ch(0) + , m_comment('#') + , m_currCol(1) + , m_currRow(1) + , m_isBackslashSyntax(false) + , m_isEof(false) + , m_isFileLoaded(false) + , m_isGood(true) + , m_lastPos(-1) + , m_maxCols(0) + , m_qualifier('"') + , m_separator(',') + , m_statusMsg("") +{ + m_csv.setBuffer(&m_array); + m_ts.setDevice(&m_csv); + m_csv.open(QIODevice::ReadOnly); + m_ts.setCodec("UTF-8"); +} + +CsvParser::~CsvParser() { + m_csv.close(); +} + +bool CsvParser::isFileLoaded() { + return m_isFileLoaded; +} + +bool CsvParser::reparse() { + reset(); + return parseFile(); +} + + +bool CsvParser::parse(QFile *device) { + clear(); + if (nullptr == device) { + m_statusMsg += QObject::tr("NULL device\n"); + return false; + } + if (!readFile(device)) { + return false; + } + return parseFile(); +} + +bool CsvParser::readFile(QFile *device) { + if (device->isOpen()) { + device->close(); + } + + device->open(QIODevice::ReadOnly); + if (!Tools::readAllFromDevice(device, m_array)) { + m_statusMsg += QObject::tr("Error reading from device\n"); + m_isFileLoaded = false; + } + else { + device->close(); + + m_array.replace("\r\n", "\n"); + m_array.replace("\r", "\n"); + if (0 == m_array.size()) { + m_statusMsg += QObject::tr("File empty\n"); + } + m_isFileLoaded = true; + } + return m_isFileLoaded; +} + +void CsvParser::reset() { + m_ch = 0; + m_currCol = 1; + m_currRow = 1; + m_isEof = false; + m_isGood = true; + m_lastPos = -1; + m_maxCols = 0; + m_statusMsg = ""; + m_ts.seek(0); + m_table.clear(); + //the following are users' concern :) + //m_comment = '#'; + //m_backslashSyntax = false; + //m_comment = '#'; + //m_qualifier = '"'; + //m_separator = ','; +} + +void CsvParser::clear() { + reset(); + m_isFileLoaded = false; + m_array.clear(); +} + +bool CsvParser::parseFile() { + parseRecord(); + while (!m_isEof) + { + if (!skipEndline()) { + appendStatusMsg(QObject::tr("malformed string")); + } + m_currRow++; + m_currCol = 1; + parseRecord(); + } + fillColumns(); + return m_isGood; +} + +void CsvParser::parseRecord() { + csvrow row; + if (isComment()) { + skipLine(); + return; + } + else { + do { + parseField(row); + getChar(m_ch); + } while (isSeparator(m_ch) && !m_isEof); + + if (!m_isEof) { + ungetChar(); + } + if (isEmptyRow(row)) { + row.clear(); + return; + } + m_table.push_back(row); + if (m_maxCols < row.size()) { + m_maxCols = row.size(); + } + m_currCol++; + } +} + +void CsvParser::parseField(csvrow& row) { + QString field; + peek(m_ch); + if (!isTerminator(m_ch)) + { + if (isQualifier(m_ch)) { + parseQuoted(field); + } + else { + parseSimple(field); + } + } + row.push_back(field); +} + +void CsvParser::parseSimple(QString &s) { + QChar c; + getChar(c); + while ((isText(c)) && (!m_isEof)) + { + s.append(c); + getChar(c); + } + if (!m_isEof) { + ungetChar(); + } +} + +void CsvParser::parseQuoted(QString &s) { + //read and discard initial qualifier (e.g. quote) + getChar(m_ch); + parseEscaped(s); + //getChar(m_ch); + if (!isQualifier(m_ch)) { + appendStatusMsg(QObject::tr("missing closing quote")); + } +} + +void CsvParser::parseEscaped(QString &s) { + parseEscapedText(s); + while (processEscapeMark(s, m_ch)) { + parseEscapedText(s); + } + if (!m_isEof) { + ungetChar(); + } +} + +void CsvParser::parseEscapedText(QString &s) { + getChar(m_ch); + while ((!isQualifier(m_ch)) && !m_isEof) + { + s.append(m_ch); + getChar(m_ch); + } +} + +bool CsvParser::processEscapeMark(QString &s, QChar c) { + QChar buf; + peek(buf); + QChar c2; + //escape-character syntax, e.g. \" + if (true == m_isBackslashSyntax) + { + if (c != '\\') { + return false; + } + //consume (and append) second qualifier + getChar(c2); + if (m_isEof){ + c2='\\'; + s.append('\\'); + return false; + } + else { + s.append(c2); + return true; + } + } + //double quote syntax, e.g. "" + else + { + if (!isQualifier(c)) { + return false; + } + peek(c2); + if (!m_isEof) { //not EOF, can read one char + if (isQualifier(c2)) { + s.append(c2); + getChar(c2); + return true; + } + } + return false; + } +} + +void CsvParser::fillColumns() { + //fill the rows with lesser columns with empty fields + + for (int i=0; i 0) { + csvrow r = m_table.at(i); + for (int j=0; j> c; + } +} + +void CsvParser::ungetChar() { + if (!m_ts.seek(m_lastPos)) + m_statusMsg += QObject::tr("Internal: unget lower bound exceeded"); +} + +void CsvParser::peek(QChar& c) { + getChar(c); + if (!m_isEof) { + ungetChar(); + } +} + +bool CsvParser::isQualifier(const QChar c) const { + if (true == m_isBackslashSyntax && (c != m_qualifier)) { + return (c == '\\'); + } + else { + return (c == m_qualifier); + } +} + +bool CsvParser::isComment() { + bool result = false; + QChar c2; + qint64 pos = m_ts.pos(); + + do { + getChar(c2); + } while ((isSpace(c2) || isTab(c2)) && (!m_isEof)); + + if (c2 == m_comment) { + result = true; + } + m_ts.seek(pos); + return result; +} + +bool CsvParser::isText(QChar c) const { + return !( (isCRLF(c)) || (isSeparator(c)) ); +} + +bool CsvParser::isEmptyRow(csvrow row) const { + csvrow::const_iterator it = row.constBegin(); + for (; it != row.constEnd(); ++it) { + if ( ((*it) != "\n") && ((*it) != "") ) + return false; + } + return true; +} + +bool CsvParser::isCRLF(const QChar c) const { + return (c == '\n'); +} + +bool CsvParser::isSpace(const QChar c) const { + return (c == 0x20); +} + +bool CsvParser::isTab(const QChar c) const { + return (c == '\t'); +} + +bool CsvParser::isSeparator(const QChar c) const { + return (c == m_separator); +} + +bool CsvParser::isTerminator(const QChar c) const { + return (isSeparator(c) || (c == '\n') || (c == '\r')); +} + +void CsvParser::setBackslashSyntax(bool set) { + m_isBackslashSyntax = set; +} + +void CsvParser::setComment(const QChar c) { + m_comment = c.unicode(); +} + +void CsvParser::setCodec(const QString s) { + m_ts.setCodec(QTextCodec::codecForName(s.toLocal8Bit())); +} + +void CsvParser::setFieldSeparator(const QChar c) { + m_separator = c.unicode(); +} + +void CsvParser::setTextQualifier(const QChar c) { + m_qualifier = c.unicode(); +} + +int CsvParser::getFileSize() const { + return m_csv.size(); +} + +const csvtable CsvParser::getCsvTable() const { + return m_table; +} + +QString CsvParser::getStatus() const { + return m_statusMsg; +} + +int CsvParser::getCsvCols() const { + if ((m_table.size() > 0) && (m_table.at(0).size() > 0)) + return m_table.at(0).size(); + else return 0; +} + +int CsvParser::getCsvRows() const { + return m_table.size(); +} + + +void CsvParser::appendStatusMsg(QString s) { + m_statusMsg += s + .append(" @" + QString::number(m_currRow)) + .append(",") + .append(QString::number(m_currCol)) + .append("\n"); + m_isGood = false; +} diff --git a/src/core/CsvParser.h b/src/core/CsvParser.h new file mode 100644 index 000000000..9eb8149f9 --- /dev/null +++ b/src/core/CsvParser.h @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2015 Enrico Mariotti + * + * 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_CSVPARSER_H +#define KEEPASSX_CSVPARSER_H + +#include +#include +#include +#include +#include + +typedef QStringList csvrow; +typedef QList csvtable; + +class CsvParser { + +public: + CsvParser(); + ~CsvParser(); + //read data from device and parse it + bool parse(QFile *device); + bool isFileLoaded(); + //reparse the same buffer (device is not opened again) + bool reparse(); + void setCodec(const QString s); + void setComment(const QChar c); + void setFieldSeparator(const QChar c); + void setTextQualifier(const QChar c); + void setBackslashSyntax(bool set); + int getFileSize() const; + QString getStatus() const; + const csvtable getCsvTable() const; + int getCsvRows() const; + int getCsvCols() const; + +protected: + csvtable m_table; + +private: + QByteArray m_array; + QChar m_ch, m_comment; + QBuffer m_csv; + unsigned int m_currCol, m_currRow; + bool m_isBackslashSyntax; + bool m_isEof; + bool m_isFileLoaded; + bool m_isGood; + qint64 m_lastPos; + int m_maxCols; + QChar m_qualifier; + QChar m_separator; + QString m_statusMsg; + QTextStream m_ts; + + void getChar(QChar &c); + void ungetChar(); + void peek(QChar &c); + void fillColumns(); + bool isTerminator(const QChar c) const; + bool isSeparator(const QChar c) const; + bool isQualifier(const QChar c) const; + bool processEscapeMark(QString &s, QChar c); + bool isText(QChar c) const; + bool isComment(); + bool isCRLF(const QChar c) const; + bool isSpace(const QChar c) const; + bool isTab(const QChar c) const; + bool isEmptyRow(csvrow row) const; + bool parseFile(); + void parseRecord(); + void parseField(csvrow& row); + void parseSimple(QString& s); + void parseQuoted(QString& s); + void parseEscaped(QString& s); + void parseEscapedText(QString &s); + bool readFile(QFile *device); + void reset(); + void clear(); + bool skipEndline(); + void skipLine(); + void appendStatusMsg(QString s); +}; + +#endif //CSVPARSER_H + diff --git a/src/format/KeePass2Writer.cpp b/src/format/KeePass2Writer.cpp index dfbbf3532..29cf7acf0 100644 --- a/src/format/KeePass2Writer.cpp +++ b/src/format/KeePass2Writer.cpp @@ -21,6 +21,8 @@ #include #include +#include + #include "core/Database.h" #include "core/Endian.h" #include "crypto/CryptoHash.h" diff --git a/src/gui/ChangeMasterKeyWidget.cpp b/src/gui/ChangeMasterKeyWidget.cpp index 3e346bc10..0fefaf4da 100644 --- a/src/gui/ChangeMasterKeyWidget.cpp +++ b/src/gui/ChangeMasterKeyWidget.cpp @@ -102,9 +102,9 @@ void ChangeMasterKeyWidget::generateKey() if (m_ui->enterPasswordEdit->text() == m_ui->repeatPasswordEdit->text()) { if (m_ui->enterPasswordEdit->text().isEmpty()) { if (MessageBox::question(this, tr("Question"), - tr("Do you really want to use an empty string as password?"), - QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { - return; + tr("Do you really want to use an empty string as password?"), + QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { + return; } } m_key.addKey(PasswordKey(m_ui->enterPasswordEdit->text())); diff --git a/src/gui/ChangeMasterKeyWidget.h b/src/gui/ChangeMasterKeyWidget.h index 8985ff7a8..13c59111b 100644 --- a/src/gui/ChangeMasterKeyWidget.h +++ b/src/gui/ChangeMasterKeyWidget.h @@ -49,8 +49,9 @@ private Q_SLOTS: void browseKeyFile(); private: - const QScopedPointer m_ui; CompositeKey m_key; +protected: + const QScopedPointer m_ui; Q_DISABLE_COPY(ChangeMasterKeyWidget) }; diff --git a/src/gui/ChangeMasterKeyWidget.ui b/src/gui/ChangeMasterKeyWidget.ui index d14941ccc..8c4eb3402 100644 --- a/src/gui/ChangeMasterKeyWidget.ui +++ b/src/gui/ChangeMasterKeyWidget.ui @@ -7,12 +7,20 @@ 0 0 438 - 256 + 278 - + + + + 11 + 75 + true + + + diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index 69b4f7e72..73a673143 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -193,6 +193,23 @@ void DatabaseTabWidget::openDatabase(const QString& fileName, const QString& pw, } } +void DatabaseTabWidget::importCsv() +{ + QString fileName = fileDialog()->getOpenFileName(this, tr("Open CSV file"), QString(), + tr("CSV file") + " (*.csv);;" + tr("All files (*)")); + + if (fileName.isEmpty()) { + return; + } + + Database* db = new Database(); + DatabaseManagerStruct dbStruct; + dbStruct.dbWidget = new DatabaseWidget(db, this); + + insertDatabase(db, dbStruct); + dbStruct.dbWidget->switchToImportCsv(fileName); +} + void DatabaseTabWidget::importKeePass1Database() { QString fileName = fileDialog()->getOpenFileName(this, tr("Open KeePass 1 database"), QString(), diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h index de4a9ca12..bf8a3316c 100644 --- a/src/gui/DatabaseTabWidget.h +++ b/src/gui/DatabaseTabWidget.h @@ -63,6 +63,7 @@ class DatabaseTabWidget : public QTabWidget public Q_SLOTS: void newDatabase(); void openDatabase(); + void importCsv(); void importKeePass1Database(); bool saveDatabase(int index = -1); bool saveDatabaseAs(int index = -1); diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 3bca6fa31..113c36969 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -37,6 +37,7 @@ #include "core/Tools.h" #include "gui/ChangeMasterKeyWidget.h" #include "gui/Clipboard.h" +#include "gui/csvImport/CsvImportWizard.h" #include "gui/DatabaseOpenWidget.h" #include "gui/DatabaseSettingsWidget.h" #include "gui/KeePass1OpenWidget.h" @@ -118,10 +119,8 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) m_editGroupWidget->setObjectName("editGroupWidget"); m_changeMasterKeyWidget = new ChangeMasterKeyWidget(); m_changeMasterKeyWidget->headlineLabel()->setText(tr("Change master key")); - QFont headlineLabelFont = m_changeMasterKeyWidget->headlineLabel()->font(); - headlineLabelFont.setBold(true); - headlineLabelFont.setPointSize(headlineLabelFont.pointSize() + 2); - m_changeMasterKeyWidget->headlineLabel()->setFont(headlineLabelFont); + m_csvImportWizard = new CsvImportWizard(); + m_csvImportWizard->setObjectName("csvImportWizard"); m_databaseSettingsWidget = new DatabaseSettingsWidget(); m_databaseSettingsWidget->setObjectName("databaseSettingsWidget"); m_databaseOpenWidget = new DatabaseOpenWidget(); @@ -137,6 +136,7 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) addWidget(m_databaseSettingsWidget); addWidget(m_historyEditEntryWidget); addWidget(m_databaseOpenWidget); + addWidget(m_csvImportWizard); addWidget(m_keepass1OpenWidget); addWidget(m_unlockDatabaseWidget); @@ -156,6 +156,7 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) connect(m_databaseSettingsWidget, SIGNAL(editFinished(bool)), SLOT(switchToView(bool))); connect(m_databaseOpenWidget, SIGNAL(editFinished(bool)), SLOT(openDatabase(bool))); connect(m_keepass1OpenWidget, SIGNAL(editFinished(bool)), SLOT(openDatabase(bool))); + connect(m_csvImportWizard, SIGNAL(importFinished(bool)), SLOT(csvImportFinished(bool))); connect(m_unlockDatabaseWidget, SIGNAL(editFinished(bool)), SLOT(unlockDatabase(bool))); connect(this, SIGNAL(currentChanged(int)), this, SLOT(emitCurrentModeChanged())); connect(m_searchUi->searchEdit, SIGNAL(textChanged(QString)), this, SLOT(startSearchTimer())); @@ -542,6 +543,16 @@ void DatabaseWidget::setCurrentWidget(QWidget* widget) adjustSize(); } +void DatabaseWidget::csvImportFinished(bool accepted) +{ + if (!accepted) { + Q_EMIT closeRequest(); + } + else { + setCurrentWidget(m_mainWidget); + } +} + void DatabaseWidget::switchToView(bool accepted) { if (m_newGroup) { @@ -732,6 +743,15 @@ void DatabaseWidget::switchToOpenDatabase(const QString& fileName, const QString m_databaseOpenWidget->enterKey(password, keyFile); } + +void DatabaseWidget::switchToImportCsv(const QString& fileName) +{ + updateFilename(fileName); + switchToMasterKeyChange(); + m_csvImportWizard->load(fileName, m_db); + setCurrentWidget(m_csvImportWizard); +} + void DatabaseWidget::switchToImportKeepass1(const QString& fileName) { updateFilename(fileName); diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 756c14832..9682c6bc9 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -26,6 +26,7 @@ #include "gui/entry/EntryModel.h" class ChangeMasterKeyWidget; +class CsvImportWizard; class DatabaseOpenWidget; class DatabaseSettingsWidget; class Database; @@ -125,6 +126,8 @@ public Q_SLOTS: void switchToDatabaseSettings(); void switchToOpenDatabase(const QString& fileName); void switchToOpenDatabase(const QString& fileName, const QString& password, const QString& keyFile); + void switchToImportCsv(const QString& fileName); + void csvImportFinished(bool accepted); void switchToImportKeepass1(const QString& fileName); void openSearch(); @@ -162,6 +165,7 @@ private Q_SLOTS: EditEntryWidget* m_historyEditEntryWidget; EditGroupWidget* m_editGroupWidget; ChangeMasterKeyWidget* m_changeMasterKeyWidget; + CsvImportWizard* m_csvImportWizard; DatabaseSettingsWidget* m_databaseSettingsWidget; DatabaseOpenWidget* m_databaseOpenWidget; KeePass1OpenWidget* m_keepass1OpenWidget; diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index be91b7a08..d7c57ec72 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->actionImportCsv, SIGNAL(triggered()), m_ui->tabWidget, + SLOT(importCsv())); connect(m_ui->actionExportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(exportToCsv())); connect(m_ui->actionLockDatabases, SIGNAL(triggered()), m_ui->tabWidget, @@ -366,7 +368,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionDatabaseNew->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); m_ui->actionDatabaseOpen->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); m_ui->menuRecentDatabases->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); - m_ui->actionImportKeePass1->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); + m_ui->menuImport->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); m_ui->actionLockDatabases->setEnabled(m_ui->tabWidget->hasLockableDatabases()); } diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index 92c806347..0c5edc4b9 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -109,6 +109,13 @@ Recent databases + + + Import + + + + @@ -119,7 +126,7 @@ - + @@ -318,11 +325,6 @@ Database settings - - - Import KeePass 1 database - - false @@ -422,6 +424,16 @@ Export to CSV file + + + Import KeePass 1 database + + + + + Import CSV file + + diff --git a/src/gui/csvImport/CsvImportWidget.cpp b/src/gui/csvImport/CsvImportWidget.cpp new file mode 100644 index 000000000..294da4b21 --- /dev/null +++ b/src/gui/csvImport/CsvImportWidget.cpp @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2015 Enrico Mariotti + * + * 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 "CsvImportWidget.h" +#include "ui_CsvImportWidget.h" +#include "gui/MessageBox.h" + +#include +#include +#include + +//I wanted to make the CSV import GUI future-proof, so if one day you need entries +//to have a new field, all you have to do is uncomment a row or two here, and the GUI will follow: +//dynamic generation of comboBoxes, labels, placement and so on. Try it for immense fun! +const QStringList CsvImportWidget::m_columnheader = QStringList() + << QObject::tr("Group") + << QObject::tr("Title") + << QObject::tr("Username") + << QObject::tr("Password") + << QObject::tr("URL") + << QObject::tr("Notes") +// << QObject::tr("Future field1") +// << QObject::tr("Future field2") +// << QObject::tr("Future field3") + ; + +CsvImportWidget::CsvImportWidget(QWidget *parent) + : QWidget(parent) + , m_ui(new Ui::CsvImportWidget()) + , m_parserModel(new CsvParserModel(this)) + , m_comboModel(new QStringListModel(this)) + , m_comboMapper(new QSignalMapper(this)) + , m_lastParseColumns(-1) +{ + m_ui->setupUi(this); + + QFont font = m_ui->labelHeadline->font(); + font.setBold(true); + font.setPointSize(font.pointSize() + 2); + m_ui->labelHeadline->setFont(font); + + m_ui->comboBoxCodec->addItems(QStringList() <<"UTF-8" <<"Windows-1252" <<"UTF-16" <<"UTF-16LE"); + m_ui->comboBoxFieldSeparator->addItems(QStringList() <<";" <<"," <<"-" <<":" <<"."); + m_ui->comboBoxTextQualifier->addItems(QStringList() <<"\"" <<"'" <<":" <<"." <<"|"); + m_ui->comboBoxComment->addItems(QStringList() <<"#" <<";" <<":" <<"@"); + + m_ui->tableViewFields->setSelectionMode(QAbstractItemView::NoSelection); + m_ui->tableViewFields->setFocusPolicy(Qt::NoFocus); + + for (int i=0; isetFixedWidth(label->minimumSizeHint().width()); + font = label->font(); + font.setBold(false); + label->setFont(font); + + QComboBox* combo = new QComboBox(this); + font = combo->font(); + font.setBold(false); + combo->setFont(font); + m_combos.append(combo); + combo->setModel(m_comboModel); + m_comboMapper->setMapping(combo, i); + connect(combo, SIGNAL(currentIndexChanged(int)), m_comboMapper, SLOT(map())); + + //layout labels and combo fields in column-first order + int combo_rows = 1+(m_columnheader.count()-1)/2; + int x=i%combo_rows; + int y= 2*(i/combo_rows); + m_ui->gridLayout_combos->addWidget(label, x, y); + m_ui->gridLayout_combos->addWidget(combo, x, y+1); + } + + m_parserModel->setHeaderLabels(m_columnheader); + m_ui->tableViewFields->setModel(m_parserModel); + + connect(m_ui->spinBoxSkip, SIGNAL(valueChanged(int)), SLOT(skippedChanged(int))); + connect(m_ui->comboBoxCodec, SIGNAL(currentIndexChanged(int)), SLOT(parse())); + connect(m_ui->comboBoxTextQualifier, SIGNAL(currentIndexChanged(int)), SLOT(parse())); + connect(m_ui->comboBoxComment, SIGNAL(currentIndexChanged(int)), SLOT(parse())); + connect(m_ui->comboBoxFieldSeparator, SIGNAL(currentIndexChanged(int)), SLOT(parse())); + connect(m_ui->checkBoxBackslash, SIGNAL(toggled(bool)), SLOT(parse())); + connect(m_ui->pushButtonWarnings, SIGNAL(clicked()), this, SLOT(showReport())); + connect(m_comboMapper, SIGNAL(mapped(int)), this, SLOT(comboChanged(int))); + + connect(m_ui->buttonBox, SIGNAL(accepted()), this, SLOT(writeDatabase())); + connect(m_ui->buttonBox, SIGNAL(rejected()), this, SLOT(reject())); +} + +void CsvImportWidget::comboChanged(int comboId) { + QComboBox* currentSender = qobject_cast(m_comboMapper->mapping(comboId)); + if (currentSender != nullptr) { + qDebug() <<"comboChanged row " <currentIndex() <<" of sender: " <currentIndex() != -1) { + //here is the line that actually updates the GUI table + m_parserModel->mapColumns(currentSender->currentIndex(), comboId); + } + updateTableview(); +} + +void CsvImportWidget::skippedChanged(int rows) { + m_parserModel->setSkippedRows(rows); + updateTableview(); +} + +CsvImportWidget::~CsvImportWidget() {} + +void CsvImportWidget::configParser() { + m_parserModel->setBackslashSyntax(m_ui->checkBoxBackslash->isChecked()); + m_parserModel->setComment(m_ui->comboBoxComment->currentText().at(0)); + m_parserModel->setTextQualifier(m_ui->comboBoxTextQualifier->currentText().at(0)); + m_parserModel->setCodec(m_ui->comboBoxCodec->currentText()); + m_parserModel->setFieldSeparator(m_ui->comboBoxFieldSeparator->currentText().at(0)); +} + +void CsvImportWidget::updateTableview() { + m_ui->tableViewFields->resizeRowsToContents(); + m_ui->tableViewFields->resizeColumnsToContents(); + + for (int c=0; ctableViewFields->horizontalHeader()->count(); ++c) { + m_ui->tableViewFields->horizontalHeader()->setSectionResizeMode( + c, QHeaderView::Stretch); + } +} + +void CsvImportWidget::updatePreview() { + + m_ui->labelSizeRowsCols->setText(m_parserModel->getFileInfo()); + m_ui->spinBoxSkip->setValue(0); + m_ui->spinBoxSkip->setMaximum(m_parserModel->rowCount()-1); + + int i; + QStringList list(tr("Not present in CSV file")); + + for (i=1; igetCsvCols(); i++) { + QString s = QString(tr("Column ")) + QString::number(i); + list << s; + } + m_comboModel->setStringList(list); + + i=1; + Q_FOREACH (QComboBox* b, m_combos) { + if (i < m_parserModel->getCsvCols()) { + b->setCurrentIndex(i); + } + else { + b->setCurrentIndex(0); + } + ++i; + } +} + +void CsvImportWidget::load(const QString& filename, Database* const db) { + m_db = db; + m_parserModel->setFilename(filename); + m_ui->labelFilename->setText(filename); + Group* group = m_db->rootGroup(); + group->setName(tr("Root")); + group->setUuid(Uuid::random()); + group->setNotes(tr("Imported from CSV\nOriginal data: ") + filename); + + parse(); +} + +void CsvImportWidget::parse() { + configParser(); + QApplication::setOverrideCursor(Qt::WaitCursor); + bool good = m_parserModel->parse(); + QApplication::restoreOverrideCursor(); + updatePreview(); + m_ui->pushButtonWarnings->setEnabled(!good); +} + +void CsvImportWidget::showReport() { + MessageBox::warning(this, tr("Syntax error"), tr("While parsing file...\n").append(m_parserModel->getStatus()), QMessageBox::Ok, QMessageBox::Ok); +} + +void CsvImportWidget::writeDatabase() { + for (int r=0; rrowCount(); r++) { + //use the validity of second column as a GO/NOGO hint for all others fields + if (m_parserModel->data(m_parserModel->index(r, 1)).isValid()) { + Entry* entry = new Entry(); + entry->setUuid(Uuid::random()); + entry->setGroup(grp(m_parserModel->data(m_parserModel->index(r, 0)).toString())); + entry->setTitle( m_parserModel->data(m_parserModel->index(r, 1)).toString()); + entry->setUsername( m_parserModel->data(m_parserModel->index(r, 2)).toString()); + entry->setPassword( m_parserModel->data(m_parserModel->index(r, 3)).toString()); + entry->setUrl( m_parserModel->data(m_parserModel->index(r, 4)).toString()); + entry->setNotes( m_parserModel->data(m_parserModel->index(r, 5)).toString()); + } + } + + QBuffer buffer; + buffer.open(QBuffer::ReadWrite); + + KeePass2Writer writer; + writer.writeDatabase(&buffer, m_db); + if (writer.hasError()) { + MessageBox::warning(this, tr("Error"), tr("CSV import: writer has errors:\n").append((writer.errorString())), QMessageBox::Ok, QMessageBox::Ok); + } + Q_EMIT editFinished(true); +} + + +Group* CsvImportWidget::grp(QString label) { + Q_ASSERT(label); + Group* root = m_db->rootGroup(), *current = root; + Group* neu = nullptr; + QStringList grpList = label.split("/", QString::SkipEmptyParts); + + Q_FOREACH (const QString& grpName, grpList) { + Group *children = hasChildren(current, grpName); + if (children == nullptr) { + neu = new Group(); + neu->setParent(current); + neu->setName(grpName); + current = neu; + } + else { + Q_ASSERT(children != nullptr); + current = children; + } + } + return current; +} + +Group* CsvImportWidget::hasChildren(Group* current, QString grpName) { + //returns the group whose name is "grpName" and is child of "current" group + Q_FOREACH(Group* grp, current->children()) { + if (grp->name() == grpName) { + return grp; + } + } + return nullptr; +} + +void CsvImportWidget::reject() { + Q_EMIT editFinished(false); +} diff --git a/src/gui/csvImport/CsvImportWidget.h b/src/gui/csvImport/CsvImportWidget.h new file mode 100644 index 000000000..3f1ae9ac7 --- /dev/null +++ b/src/gui/csvImport/CsvImportWidget.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2015 Enrico Mariotti + * + * 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_CSVIMPORTWIDGET_H +#define KEEPASSX_CSVIMPORTWIDGET_H + +#include +#include +#include +#include +#include +#include +#include + +#include "format/KeePass2Writer.h" +#include "gui/csvImport/CsvParserModel.h" +#include "keys/PasswordKey.h" +#include "core/Metadata.h" + + +namespace Ui { + class CsvImportWidget; +} + +class CsvImportWidget : public QWidget +{ + Q_OBJECT + +public: + explicit CsvImportWidget(QWidget *parent = nullptr); + virtual ~CsvImportWidget(); + void load(const QString& filename, Database* const db); + +Q_SIGNALS: + void editFinished(bool accepted); + +private Q_SLOTS: + void parse(); + void showReport(); + void comboChanged(int comboId); + void skippedChanged(int rows); + void writeDatabase(); + void reject(); + +private: + Q_DISABLE_COPY(CsvImportWidget) + const QScopedPointer m_ui; + CsvParserModel* const m_parserModel; + QStringListModel* const m_comboModel; + QSignalMapper* m_comboMapper; + QList m_combos; + Database *m_db; + int m_lastParseColumns; + + KeePass2Writer m_writer; + static const QStringList m_columnheader; + void configParser(); + void updatePreview(); + void updateTableview(); + Group* grp(QString label); + Group* hasChildren(Group* current, QString grpName); +}; + +#endif // KEEPASSX_CSVIMPORTWIDGET_H diff --git a/src/gui/csvImport/CsvImportWidget.ui b/src/gui/csvImport/CsvImportWidget.ui new file mode 100644 index 000000000..5df2aa1af --- /dev/null +++ b/src/gui/csvImport/CsvImportWidget.ui @@ -0,0 +1,524 @@ + + + CsvImportWidget + + + + 0 + 0 + 779 + 691 + + + + + + + + + + + 0 + 137 + + + + + 75 + true + + + + Encoding + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 114 + 20 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 114 + 20 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 114 + 20 + + + + + + + + + 50 + false + + + + Text is qualified by + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + false + + + + 50 + false + + + + Show parser warnings + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 114 + 20 + + + + + + + + + 0 + 0 + + + + + 50 + false + + + + false + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 114 + 20 + + + + + + + + Qt::Horizontal + + + + 114 + 20 + + + + + + + + + 50 + false + + + + Codec + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + + 114 + 20 + + + + + + + + + 0 + 0 + + + + + 50 + false + + + + false + + + + + + + + 50 + false + + + + Fields are separated by + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 50 + false + + + + Comments start with + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + 50 + false + + + + false + + + + + + + + 0 + 0 + + + + + 50 + false + + + + false + + + + + + + Qt::Horizontal + + + + 114 + 20 + + + + + + + + + 50 + false + + + + Treat '\' as escape character + + + + + + + + + + 50 + false + + + + Skip first + + + + + + + + 50 + false + + + + + + + + + 50 + false + + + + rows + + + + + + + + + + 50 + false + true + + + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + false + + + + + + + + 75 + true + + + + Column layout + + + + + + 0 + + + + + + + + + + + + + 0 + 0 + + + + Import CSV fields + + + + + + + + 0 + 0 + + + + filename + + + + + + + + 0 + 0 + + + + size, rows, columns + + + + + + + + + + 0 + 0 + + + + + 0 + 200 + + + + + 75 + true + + + + Preview + + + false + + + + + + + 0 + 0 + + + + + 50 + false + + + + true + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 27 + + + + + + + + + diff --git a/src/gui/csvImport/CsvImportWizard.cpp b/src/gui/csvImport/CsvImportWizard.cpp new file mode 100644 index 000000000..19d992f42 --- /dev/null +++ b/src/gui/csvImport/CsvImportWizard.cpp @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2015 Enrico Mariotti + * + * 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 "CsvImportWizard.h" +#include +#include +#include "gui/MessageBox.h" + + +CsvImportWizard::CsvImportWizard(QWidget *parent) + : DialogyWidget(parent) +{ + m_layout = new QGridLayout(this); + m_pages = new QStackedWidget(parent); + m_layout->addWidget(m_pages, 0, 0); + + m_pages->addWidget(key = new ChangeMasterKeyWidget(m_pages)); + m_pages->addWidget(parse = new CsvImportWidget(m_pages)); + key->headlineLabel()->setText(tr("Import CSV file")); + + connect(key, SIGNAL(editFinished(bool)), this, SLOT(keyFinished(bool))); + connect(parse, SIGNAL(editFinished(bool)), this, SLOT(parseFinished(bool))); +} + +CsvImportWizard::~CsvImportWizard() +{} + +void CsvImportWizard::load(const QString& filename, Database* database) +{ + m_db = database; + parse->load(filename, database); + key->clearForms(); +} + +void CsvImportWizard::keyFinished(bool accepted) +{ + if (!accepted) { + Q_EMIT(importFinished(false)); + return; + } + + m_pages->setCurrentIndex(m_pages->currentIndex()+1); + + QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); + bool result = m_db->setKey(key->newMasterKey()); + QApplication::restoreOverrideCursor(); + + if (!result) { + MessageBox::critical(this, tr("Error"), tr("Unable to calculate master key")); + Q_EMIT importFinished(false); + return; + } +} + +void CsvImportWizard::parseFinished(bool accepted) +{ + Q_EMIT(importFinished(accepted)); +} diff --git a/src/gui/csvImport/CsvImportWizard.h b/src/gui/csvImport/CsvImportWizard.h new file mode 100644 index 000000000..b5c7e9b76 --- /dev/null +++ b/src/gui/csvImport/CsvImportWizard.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2015 Enrico Mariotti + * + * 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_CSVIMPORTWIZARD_H +#define KEEPASSX_CSVIMPORTWIZARD_H + +#include + +#include +#include + +#include "CsvImportWidget.h" +#include "core/Database.h" +#include "gui/ChangeMasterKeyWidget.h" +#include "gui/DialogyWidget.h" + +class CsvImportWidget; + +class CsvImportWizard : public DialogyWidget +{ + Q_OBJECT + +public: + explicit CsvImportWizard(QWidget *parent = nullptr); + virtual ~CsvImportWizard(); + void load(const QString& filename, Database *database); + +Q_SIGNALS: + void importFinished(bool accepted); + +private Q_SLOTS: + void keyFinished(bool accepted); + void parseFinished(bool accepted); + +private: + Database* m_db; + CsvImportWidget* parse; + ChangeMasterKeyWidget* key; + QStackedWidget *m_pages; + QGridLayout *m_layout; +}; + +#endif //KEEPASSX_CSVIMPORTWIZARD_H diff --git a/src/gui/csvImport/CsvParserModel.cpp b/src/gui/csvImport/CsvParserModel.cpp new file mode 100644 index 000000000..43cbcd644 --- /dev/null +++ b/src/gui/csvImport/CsvParserModel.cpp @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2015 Enrico Mariotti + * + * 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 "CsvParserModel.h" + +CsvParserModel::CsvParserModel(QObject *parent) + : QAbstractTableModel(parent) + , m_skipped(0) +{} + +CsvParserModel::~CsvParserModel() +{} + +void CsvParserModel::setFilename(const QString& filename) { + m_filename = filename; +} + +QString CsvParserModel::getFileInfo(){ + QString a(QString::number(getFileSize()).append(tr(" byte, "))); + a.append(QString::number(getCsvRows())).append(tr(" rows, ")); + a.append(QString::number((getCsvCols()-1))).append(tr(" columns")); + return a; +} + +bool CsvParserModel::parse() { + bool r; + beginResetModel(); + m_columnMap.clear(); + if (CsvParser::isFileLoaded()) { + r = CsvParser::reparse(); + } + else { + QFile csv(m_filename); + r = CsvParser::parse(&csv); + } + for (int i=0; i= getCsvCols()) { + m_columnMap[dbColumn] = 0; //map to the empty column + } + else { + m_columnMap[dbColumn] = csvColumn; + } + endResetModel(); +} + +void CsvParserModel::setSkippedRows(int skipped) { + m_skipped = skipped; + QModelIndex topLeft = createIndex(skipped,0); + QModelIndex bottomRight = createIndex(m_skipped+rowCount(), columnCount()); + Q_EMIT dataChanged(topLeft, bottomRight); + Q_EMIT layoutChanged(); +} + +void CsvParserModel::setHeaderLabels(QStringList l) { + m_columnHeader = l; +} + +int CsvParserModel::rowCount(const QModelIndex &parent) const { + if (parent.isValid()) { + return 0; + } + return getCsvRows(); +} + +int CsvParserModel::columnCount(const QModelIndex &parent) const { + if (parent.isValid()) { + return 0; + } + return m_columnHeader.size(); +} + +QVariant CsvParserModel::data(const QModelIndex &index, int role) const { + if ( (index.column() >= m_columnHeader.size()) + || (index.row()+m_skipped >= rowCount()) + || !index.isValid() ) + { + return QVariant(); + } + + if (role == Qt::DisplayRole) { + return m_table.at(index.row()+m_skipped).at(m_columnMap[index.column()]); + } + return QVariant(); +} + +QVariant CsvParserModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (role == Qt::DisplayRole) { + if (orientation == Qt::Horizontal) { + if ( (section < 0) || (section >= m_columnHeader.size())) { + return QVariant(); + } + return m_columnHeader.at(section); + } + else if (orientation == Qt::Vertical) { + if (section+m_skipped >= rowCount()) { + return QVariant(); + } + return QString::number(section+1); + } + } + return QVariant(); +} + + diff --git a/src/gui/csvImport/CsvParserModel.h b/src/gui/csvImport/CsvParserModel.h new file mode 100644 index 000000000..9a3d970e8 --- /dev/null +++ b/src/gui/csvImport/CsvParserModel.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2015 Enrico Mariotti + * + * 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_CSVPARSERMODEL_H +#define KEEPASSX_CSVPARSERMODEL_H + +#include +#include +#include +#include "core/Group.h" +#include "core/CsvParser.h" + +class CsvParserModel : public QAbstractTableModel, public CsvParser +{ + Q_OBJECT + +public: + explicit CsvParserModel(QObject *parent = nullptr); + virtual ~CsvParserModel(); + void setFilename(const QString& filename); + QString getFileInfo(); + bool parse(); + + void setHeaderLabels(QStringList l); + void mapColumns(int csvColumn, int dbColumn); + + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + virtual int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const Q_DECL_OVERRIDE; + +public Q_SLOTS: + void setSkippedRows(int skipped); + +private: + int m_skipped; + QString m_filename; + QStringList m_columnHeader; + //first column of model must be empty (aka combobox row "Not present in CSV file") + void addEmptyColumn(); + //mapping CSV columns to keepassx columns + QMap m_columnMap; +}; + +#endif //KEEPASSX_CSVPARSERMODEL_H + diff --git a/src/main.cpp b/src/main.cpp index 04b70a465..22c058bac 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -25,6 +25,7 @@ #include "crypto/Crypto.h" #include "gui/Application.h" #include "gui/MainWindow.h" +#include "gui/csvImport/CsvImportWizard.h" #include "gui/MessageBox.h" int main(int argc, char** argv) @@ -102,6 +103,5 @@ int main(int argc, char** argv) } } } - return app.exec(); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7ea399fed..afc0a0bb0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -149,16 +149,7 @@ set_target_properties(testautotype PROPERTIES ENABLE_EXPORTS ON) add_unit_test(NAME testentry SOURCES TestEntry.cpp LIBS ${TEST_LIBRARIES}) -add_unit_test(NAME testrandom SOURCES TestRandom.cpp - LIBS ${TEST_LIBRARIES}) - -add_unit_test(NAME testentrysearcher SOURCES TestEntrySearcher.cpp - LIBS ${TEST_LIBRARIES}) - -add_unit_test(NAME testexporter SOURCES TestExporter.cpp - LIBS ${TEST_LIBRARIES}) - -add_unit_test(NAME testcsvexporter SOURCES TestCsvExporter.cpp +add_unit_test(NAME testcsvparser SOURCES TestCsvParser.cpp LIBS ${TEST_LIBRARIES}) if(WITH_GUI_TESTS) diff --git a/tests/TestCsvParser.cpp b/tests/TestCsvParser.cpp new file mode 100644 index 000000000..2eb99ea61 --- /dev/null +++ b/tests/TestCsvParser.cpp @@ -0,0 +1,421 @@ +/* + * Copyright (C) 2015 Enrico Mariotti + * + * 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 "TestCsvParser.h" +#include + +//TODO: http://stackoverflow.com/questions/31001398/qt-run-unit-tests-from-multiple-test-classes-and-summarize-the-output-from-all +//useful to show CR/LF cat -v /tmp/keepassXn94do1x.csv + +QTEST_GUILESS_MAIN(TestCsvParser) + +void TestCsvParser::initTestCase() +{ + parser = new CsvParser(); +} + +void TestCsvParser::cleanupTestCase() +{ + delete parser; +} + +void TestCsvParser::init() +{ + file.setFileName("/tmp/keepassXn94do1x.csv"); + if (!file.open(QIODevice::ReadWrite | QIODevice::Truncate)) + QFAIL("Cannot open file!"); + parser->setBackslashSyntax(false); + parser->setComment('#'); + parser->setFieldSeparator(','); + parser->setTextQualifier(QChar('"')); +} + +void TestCsvParser::cleanup() +{ + file.close(); +} + +/****************** TEST CASES ******************/ +void TestCsvParser::testMissingQuote() { + parser->setTextQualifier(':'); + QTextStream out(&file); + out << "A,B\n:BM,1"; + QEXPECT_FAIL("", "Bad format", Continue); + QVERIFY(parser->parse(&file)); + t = parser->getCsvTable(); + QWARN(parser->getStatus().toLatin1()); +} + +void TestCsvParser::testMalformed() { + parser->setTextQualifier(':'); + QTextStream out(&file); + out << "A,B,C\n:BM::,1,:2:"; + QEXPECT_FAIL("", "Bad format", Continue); + QVERIFY(parser->parse(&file)); + t = parser->getCsvTable(); + QWARN(parser->getStatus().toLatin1()); +} + +void TestCsvParser::testBackslashSyntax() { + parser->setBackslashSyntax(true); + parser->setTextQualifier(QChar('X')); + QTextStream out(&file); + //attended result: one"\t\"wo + out << "Xone\\\"\\\\t\\\\\\\"w\noX\n" + << "X13X,X2\\X,X,\"\"3\"X\r" + << "3,X\"4\"X,,\n" + << "XX\n" + << "\\"; + QVERIFY(parser->parse(&file)); + t = parser->getCsvTable(); + QVERIFY(t.at(0).at(0) == "one\"\\t\\\"w\no"); + QVERIFY(t.at(1).at(0) == "13"); + QVERIFY(t.at(1).at(1) == "2X,"); + QVERIFY(t.at(1).at(2) == "\"\"3\"X"); + QVERIFY(t.at(2).at(0) == "3"); + QVERIFY(t.at(2).at(1) == "\"4\""); + QVERIFY(t.at(2).at(2) == ""); + QVERIFY(t.at(2).at(3) == ""); + QVERIFY(t.at(3).at(0) == "\\"); + QVERIFY(t.size() == 4); +} + +void TestCsvParser::testQuoted() { + QTextStream out(&file); + out << "ro,w,\"end, of \"\"\"\"\"\"row\"\"\"\"\"\n" + << "2\n"; + QVERIFY(parser->parse(&file)); + t = parser->getCsvTable(); + QVERIFY(t.at(0).at(0) == "ro"); + QVERIFY(t.at(0).at(1) == "w"); + QVERIFY(t.at(0).at(2) == "end, of \"\"\"row\"\""); + QVERIFY(t.at(1).at(0) == "2"); + QVERIFY(t.size() == 2); +} + +void TestCsvParser::testEmptySimple() { + QTextStream out(&file); + out <<""; + QVERIFY(parser->parse(&file)); + t = parser->getCsvTable(); + QVERIFY(t.size() == 0); +} + +void TestCsvParser::testEmptyQuoted() { + QTextStream out(&file); + out <<"\"\""; + QVERIFY(parser->parse(&file)); + t = parser->getCsvTable(); + QVERIFY(t.size() == 0); +} + +void TestCsvParser::testEmptyNewline() { + QTextStream out(&file); + out <<"\"\n\""; + QVERIFY(parser->parse(&file)); + t = parser->getCsvTable(); + QVERIFY(t.size() == 0); +} + +void TestCsvParser::testEmptyFile() +{ + QVERIFY(parser->parse(&file)); + t = parser->getCsvTable(); + QVERIFY(t.size() == 0); +} + +void TestCsvParser::testNewline() +{ + QTextStream out(&file); + out << "1,2\n\n\n"; + QVERIFY(parser->parse(&file)); + t = parser->getCsvTable(); + QVERIFY(t.size() == 1); + QVERIFY(t.at(0).at(0) == "1"); + QVERIFY(t.at(0).at(1) == "2"); +} + +void TestCsvParser::testCR() +{ + QTextStream out(&file); + out << "1,2\r3,4"; + QVERIFY(parser->parse(&file)); + t = parser->getCsvTable(); + QVERIFY(t.size() == 2); + QVERIFY(t.at(0).at(0) == "1"); + QVERIFY(t.at(0).at(1) == "2"); + QVERIFY(t.at(1).at(0) == "3"); + QVERIFY(t.at(1).at(1) == "4"); +} + +void TestCsvParser::testLF() +{ + QTextStream out(&file); + out << "1,2\n3,4"; + QVERIFY(parser->parse(&file)); + t = parser->getCsvTable(); + QVERIFY(t.size() == 2); + QVERIFY(t.at(0).at(0) == "1"); + QVERIFY(t.at(0).at(1) == "2"); + QVERIFY(t.at(1).at(0) == "3"); + QVERIFY(t.at(1).at(1) == "4"); +} + +void TestCsvParser::testCRLF() +{ + QTextStream out(&file); + out << "1,2\r\n3,4"; + QVERIFY(parser->parse(&file)); + t = parser->getCsvTable(); + QVERIFY(t.size() == 2); + QVERIFY(t.at(0).at(0) == "1"); + QVERIFY(t.at(0).at(1) == "2"); + QVERIFY(t.at(1).at(0) == "3"); + QVERIFY(t.at(1).at(1) == "4"); +} + +void TestCsvParser::testComments() +{ + QTextStream out(&file); + out << " #one\n" + << " \t # two, three \r\n" + << " #, sing\t with\r" + << " #\t me!\n" + << "useful,text #1!"; + QVERIFY(parser->parse(&file)); + t = parser->getCsvTable(); + QVERIFY(t.size() == 1); + QVERIFY(t.at(0).at(0) == "useful"); + QVERIFY(t.at(0).at(1) == "text #1!"); +} + +void TestCsvParser::testColumns() { + QTextStream out(&file); + out << "1,2\n" + << ",,,,,,,,,a\n" + << "a,b,c,d\n"; + QVERIFY(parser->parse(&file)); + t = parser->getCsvTable(); + QVERIFY(parser->getCsvCols() == 10); +} + +void TestCsvParser::testSimple() { + QTextStream out(&file); + out << ",,2\r,2,3\n" + << "A,,B\"\n" + << " ,,\n"; + QVERIFY(parser->parse(&file)); + t = parser->getCsvTable(); + QVERIFY(t.size() == 4); + QVERIFY(t.at(0).at(0) == ""); + QVERIFY(t.at(0).at(1) == ""); + QVERIFY(t.at(0).at(2) == "2"); + QVERIFY(t.at(1).at(0) == ""); + QVERIFY(t.at(1).at(1) == "2"); + QVERIFY(t.at(1).at(2) == "3"); + QVERIFY(t.at(2).at(0) == "A"); + QVERIFY(t.at(2).at(1) == ""); + QVERIFY(t.at(2).at(2) == "B\""); + QVERIFY(t.at(3).at(0) == " "); + QVERIFY(t.at(3).at(1) == ""); + QVERIFY(t.at(3).at(2) == ""); +} + +void TestCsvParser::testSeparator() { + parser->setFieldSeparator('\t'); + QTextStream out(&file); + out << "\t\t2\r\t2\t3\n" + << "A\t\tB\"\n" + << " \t\t\n"; + QVERIFY(parser->parse(&file)); + t = parser->getCsvTable(); + QVERIFY(t.size() == 4); + QVERIFY(t.at(0).at(0) == ""); + QVERIFY(t.at(0).at(1) == ""); + QVERIFY(t.at(0).at(2) == "2"); + QVERIFY(t.at(1).at(0) == ""); + QVERIFY(t.at(1).at(1) == "2"); + QVERIFY(t.at(1).at(2) == "3"); + QVERIFY(t.at(2).at(0) == "A"); + QVERIFY(t.at(2).at(1) == ""); + QVERIFY(t.at(2).at(2) == "B\""); + QVERIFY(t.at(3).at(0) == " "); + QVERIFY(t.at(3).at(1) == ""); + QVERIFY(t.at(3).at(2) == ""); +} + +void TestCsvParser::testMultiline() +{ + parser->setTextQualifier(QChar(':')); + QTextStream out(&file); + out << ":1\r\n2a::b:,:3\r4:\n" + << "2\n"; + QVERIFY(parser->parse(&file)); + t = parser->getCsvTable(); + QVERIFY(t.at(0).at(0) == "1\n2a:b"); + QVERIFY(t.at(0).at(1) == "3\n4"); + QVERIFY(t.at(1).at(0) == "2"); + QVERIFY(t.size() == 2); +} + +void TestCsvParser::testEmptyReparsing() +{ + parser->parse(nullptr); + QVERIFY(parser->reparse()); + t = parser->getCsvTable(); + QVERIFY(t.size() == 0); +} + +void TestCsvParser::testReparsing() +{ + QTextStream out(&file); + out << ":te\r\nxt1:,:te\rxt2:,:end of \"this\n string\":\n" + << "2\n"; + QVERIFY(parser->parse(&file)); + t = parser->getCsvTable(); + + QEXPECT_FAIL("", "Wrong qualifier", Continue); + QVERIFY(t.at(0).at(0) == "te\nxt1"); + + parser->setTextQualifier(QChar(':')); + + QVERIFY(parser->reparse()); + t = parser->getCsvTable(); + QVERIFY(t.at(0).at(0) == "te\nxt1"); + QVERIFY(t.at(0).at(1) == "te\nxt2"); + QVERIFY(t.at(0).at(2) == "end of \"this\n string\""); + QVERIFY(t.at(1).at(0) == "2"); + QVERIFY(t.size() == 2); +} + +void TestCsvParser::testQualifier() { + parser->setTextQualifier(QChar('X')); + QTextStream out(&file); + out << "X1X,X2XX,X,\"\"3\"\"\"X\r" + << "3,X\"4\"X,,\n"; + QVERIFY(parser->parse(&file)); + t = parser->getCsvTable(); + QVERIFY(t.size() == 2); + QVERIFY(t.at(0).at(0) == "1"); + QVERIFY(t.at(0).at(1) == "2X,"); + QVERIFY(t.at(0).at(2) == "\"\"3\"\"\"X"); + QVERIFY(t.at(1).at(0) == "3"); + QVERIFY(t.at(1).at(1) == "\"4\""); + QVERIFY(t.at(1).at(2) == ""); + QVERIFY(t.at(1).at(3) == ""); +} + +void TestCsvParser::testUnicode() { + //QString m("Texte en fran\u00e7ais"); + //CORRECT QString g("\u20AC"); + //CORRECT QChar g(0x20AC); + //ERROR QChar g("\u20AC"); + parser->setFieldSeparator(QChar('A')); + QTextStream out(&file); + out << QString("€1A2śA\"3śAż\"Ażac"); + + QVERIFY(parser->parse(&file)); + t = parser->getCsvTable(); + QVERIFY(t.size() == 1); + QVERIFY(t.at(0).at(0) == "€1"); + QVERIFY(t.at(0).at(1) == "2ś"); + QVERIFY(t.at(0).at(2) == "3śAż"); + QVERIFY(t.at(0).at(3) == "żac"); +} + +void TestCsvParser::testKeepass() { + file.close(); + file.setFileName("../../res/keepass.csv"); + if (!file.open(QIODevice::ReadWrite)) + QFAIL("Cannot open file!"); + parser->setBackslashSyntax(true); + QVERIFY(parser->parse(&file)); + t = parser->getCsvTable(); + QVERIFY(t.size() == 3); + QVERIFY(t.at(1).at(1) == "2\""); + QVERIFY(t.at(1).at(4) == "some notes...\n\nksjdkj@jdjd.com\n"); + QVERIFY(t.at(2).at(1) == "€èéç"); +} + +void TestCsvParser::dumpRow(csvtable, int) {} +/* +void TestCsvParser::dumpRow(csvtable table, int row) { + if ( (row < 0) || (row >= table.size())) { + qDebug() << QString("Error, nonexistent row %1").arg(row); + return; + } + csvrow::const_iterator it = table.at(row).constBegin(); + qDebug() <<"@row" <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 TestCsvParser::testEmptyDatabase() +{ + QBuffer buffer; + QVERIFY(buffer.open(QIODevice::ReadWrite)); + m_csvExporter->exportDatabase(&buffer, m_db); + + QCOMPARE(QString::fromUtf8(buffer.buffer().constData()), ExpectedHeaderLine); +} + +void TestCsvParser::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/TestCsvParser.h b/tests/TestCsvParser.h new file mode 100644 index 000000000..d095448fd --- /dev/null +++ b/tests/TestCsvParser.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2015 Enrico Mariotti + * + * 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_TESTCSVPARSER_H +#define KEEPASSX_TESTCSVPARSER_H + +#include +#include + +#include "core/CsvParser.h" + +class CsvParser; + +class TestCsvParser : public QObject +{ + Q_OBJECT + +public: + +private Q_SLOTS: + void init(); + void cleanup(); + void initTestCase(); + void cleanupTestCase(); + + void testUnicode(); + void testLF(); + void testEmptyReparsing(); + void testSimple(); + void testEmptyQuoted(); + void testEmptyNewline(); + void testSeparator(); + void testCR(); + void testCRLF(); + void testMalformed(); + void testQualifier(); + void testNewline(); + void testEmptySimple(); + void testMissingQuote(); + void testComments(); + void testBackslashSyntax(); + void testReparsing(); + void testEmptyFile(); + void testQuoted(); + void testMultiline(); + void testColumns(); + void testKeepass(); +// void testBigFile(); + +private: + QFile file; + CsvParser* parser; + csvtable t; + void dumpRow(csvtable table, int row); +}; + +#endif // KEEPASSX_TESTCSVPARSER_H