0
0
mirror of https://github.com/keepassxreboot/keepassxc.git synced 2024-09-20 20:23:05 +02:00

Merge pull request #146 from seatedscribe/feature/import-csv-format

Implement import of databases in CSV (Comma Separated Values) format
This commit is contained in:
Janek Bevendorff 2017-03-17 00:09:45 +01:00 committed by GitHub
commit 80fc1e5309
23 changed files with 2252 additions and 12 deletions

View File

@ -31,6 +31,7 @@ configure_file(version.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/version.h @ONLY)
set(keepassx_SOURCES
core/AutoTypeAssociations.cpp
core/Config.cpp
core/CsvParser.cpp
core/Database.cpp
core/DatabaseIcons.cpp
core/Endian.cpp
@ -102,6 +103,9 @@ set(keepassx_SOURCES
gui/UnlockDatabaseWidget.cpp
gui/UnlockDatabaseDialog.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
@ -135,6 +139,7 @@ set(keepassx_FORMS
gui/AboutDialog.ui
gui/ChangeMasterKeyWidget.ui
gui/CloneDialog.ui
gui/csvImport/CsvImportWidget.ui
gui/DatabaseOpenWidget.ui
gui/DatabaseSettingsWidget.ui
gui/CategoryListWidget.ui

383
src/core/CsvParser.cpp Normal file
View File

@ -0,0 +1,383 @@
/*
* Copyright (C) 2016 Enrico Mariotti <enricomariotti@yahoo.it>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "CsvParser.h"
#include <QTextCodec>
#include <QObject>
#include "core/Tools.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) {
appendStatusMsg(QObject::tr("NULL device"), true);
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)) {
appendStatusMsg(QObject::tr("error reading from device"), true);
m_isFileLoaded = false;
}
else {
device->close();
m_array.replace("\r\n", "\n");
m_array.replace("\r", "\n");
if (0 == m_array.size())
appendStatusMsg(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"), true);
m_currRow++;
m_currCol = 1;
parseRecord();
}
fillColumns();
return m_isGood;
}
void CsvParser::parseRecord() {
CsvRow row;
if (isComment()) {
skipLine();
return;
}
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"), true);
}
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;
if (true == m_isBackslashSyntax) {
//escape-character syntax, e.g. \"
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;
}
} else {
//double quote syntax, e.g. ""
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 shorter rows with empty placeholder columns
for (int i = 0; i < m_table.size(); ++i) {
int gap = m_maxCols-m_table.at(i).size();
if (gap > 0) {
CsvRow r = m_table.at(i);
for (int j = 0; j < gap; ++j) {
r.append(QString(""));
}
m_table.replace(i, r);
}
}
}
void CsvParser::skipLine() {
m_ts.readLine();
m_ts.seek(m_ts.pos() - 1);
}
bool CsvParser::skipEndline() {
getChar(m_ch);
return (m_ch == '\n');
}
void CsvParser::getChar(QChar& c) {
m_isEof = m_ts.atEnd();
if (!m_isEof) {
m_lastPos = m_ts.pos();
m_ts >> c;
}
}
void CsvParser::ungetChar() {
if (!m_ts.seek(m_lastPos))
appendStatusMsg(QObject::tr("INTERNAL - unget lower bound exceeded"), true);
}
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 == ' ');
}
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, bool isCritical) {
m_statusMsg += s
.append(": (row,col) " + QString::number(m_currRow))
.append(",")
.append(QString::number(m_currCol))
.append("\n");
m_isGood = not isCritical;
}

101
src/core/CsvParser.h Normal file
View File

@ -0,0 +1,101 @@
/*
* Copyright (C) 2016 Enrico Mariotti <enricomariotti@yahoo.it>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_CSVPARSER_H
#define KEEPASSX_CSVPARSER_H
#include <QFile>
#include <QBuffer>
#include <QQueue>
#include <QTextStream>
typedef QStringList CsvRow;
typedef QList<CsvRow> 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;
int getCsvRows() const;
int getCsvCols() const;
QString getStatus() const;
const CsvTable getCsvTable() const;
protected:
CsvTable m_table;
private:
QByteArray m_array;
QBuffer m_csv;
QChar m_ch;
QChar m_comment;
unsigned int m_currCol;
unsigned int 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, bool isCritical = false);
};
#endif //CSVPARSER_H

View File

@ -160,7 +160,7 @@
&lt;li&gt;debfx (KeePassX)&lt;/li&gt;
&lt;li&gt;droidmonkey&lt;/li&gt;
&lt;li&gt;louib&lt;/li&gt;
&lt;li&gt;phoerious&lt;li&gt;
&lt;li&gt;phoerious&lt;/li&gt;
&lt;li&gt;thezero&lt;/li&gt;
&lt;/ul&gt;</string>
</property>

View File

@ -212,6 +212,23 @@ void DatabaseTabWidget::openDatabase(const QString& fileName, const QString& pw,
emit messageDismissGlobal();
}
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::mergeDatabase()
{
QString filter = QString("%1 (*.kdbx);;%2 (*)").arg(tr("KeePass 2 Database"), tr("All files"));

View File

@ -66,6 +66,7 @@ public:
public slots:
void newDatabase();
void openDatabase();
void importCsv();
void mergeDatabase();
void importKeePass1Database();
bool saveDatabase(int index = -1);

View File

@ -1,4 +1,4 @@
/*
/*
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
*
* This program is free software: you can redistribute it and/or modify
@ -122,6 +122,8 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent)
m_editGroupWidget->setObjectName("editGroupWidget");
m_changeMasterKeyWidget = new ChangeMasterKeyWidget();
m_changeMasterKeyWidget->headlineLabel()->setText(tr("Change master key"));
m_csvImportWizard = new CsvImportWizard();
m_csvImportWizard->setObjectName("csvImportWizard");
QFont headlineLabelFont = m_changeMasterKeyWidget->headlineLabel()->font();
headlineLabelFont.setBold(true);
headlineLabelFont.setPointSize(headlineLabelFont.pointSize() + 2);
@ -145,6 +147,7 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent)
addWidget(m_databaseSettingsWidget);
addWidget(m_historyEditEntryWidget);
addWidget(m_databaseOpenWidget);
addWidget(m_csvImportWizard);
addWidget(m_databaseOpenMergeWidget);
addWidget(m_keepass1OpenWidget);
addWidget(m_unlockDatabaseWidget);
@ -165,6 +168,7 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent)
connect(m_databaseOpenWidget, SIGNAL(editFinished(bool)), SLOT(openDatabase(bool)));
connect(m_databaseOpenMergeWidget, SIGNAL(editFinished(bool)), SLOT(mergeDatabase(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(m_unlockDatabaseDialog, SIGNAL(unlockDone(bool)), SLOT(unlockDatabase(bool)));
connect(&m_fileWatcher, SIGNAL(fileChanged(QString)), this, SLOT(onWatchedFileChanged()));
@ -192,6 +196,9 @@ DatabaseWidget::Mode DatabaseWidget::currentMode() const
if (currentWidget() == nullptr) {
return DatabaseWidget::None;
}
else if (currentWidget() == m_csvImportWizard) {
return DatabaseWidget::ImportMode;
}
else if (currentWidget() == m_mainWidget) {
return DatabaseWidget::ViewMode;
}
@ -624,6 +631,16 @@ void DatabaseWidget::setCurrentWidget(QWidget* widget)
adjustSize();
}
void DatabaseWidget::csvImportFinished(bool accepted)
{
if (!accepted) {
emit closeRequest();
}
else {
setCurrentWidget(m_mainWidget);
}
}
void DatabaseWidget::switchToView(bool accepted)
{
if (m_newGroup) {
@ -844,12 +861,21 @@ 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::switchToOpenMergeDatabase(const QString& fileName)
{
m_databaseOpenMergeWidget->load(fileName);
setCurrentWidget(m_databaseOpenMergeWidget);
}
void DatabaseWidget::switchToOpenMergeDatabase(const QString& fileName, const QString& password,
const QString& keyFile)
{

View File

@ -1,4 +1,4 @@
/*
/*
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
*
* This program is free software: you can redistribute it and/or modify
@ -27,6 +27,7 @@
#include "gui/entry/EntryModel.h"
#include "gui/MessageWidget.h"
#include "gui/csvImport/CsvImportWizard.h"
class ChangeMasterKeyWidget;
class DatabaseOpenWidget;
@ -60,6 +61,7 @@ public:
enum Mode
{
None,
ImportMode,
ViewMode,
EditMode,
LockedMode
@ -143,6 +145,8 @@ public 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 switchToOpenMergeDatabase(const QString& fileName);
void switchToOpenMergeDatabase(const QString& fileName, const QString& password, const QString& keyFile);
void switchToImportKeepass1(const QString& fileName);
@ -188,6 +192,7 @@ private:
EditEntryWidget* m_historyEditEntryWidget;
EditGroupWidget* m_editGroupWidget;
ChangeMasterKeyWidget* m_changeMasterKeyWidget;
CsvImportWizard* m_csvImportWizard;
DatabaseSettingsWidget* m_databaseSettingsWidget;
DatabaseOpenWidget* m_databaseOpenWidget;
DatabaseOpenWidget* m_databaseOpenMergeWidget;

View File

@ -250,6 +250,8 @@ MainWindow::MainWindow()
SLOT(changeMasterKey()));
connect(m_ui->actionChangeDatabaseSettings, SIGNAL(triggered()), m_ui->tabWidget,
SLOT(changeDatabaseSettings()));
connect(m_ui->actionImportCsv, SIGNAL(triggered()), m_ui->tabWidget,
SLOT(importCsv()));
connect(m_ui->actionImportKeePass1, SIGNAL(triggered()), m_ui->tabWidget,
SLOT(importKeePass1Database()));
connect(m_ui->actionRepairDatabase, SIGNAL(triggered()), this,
@ -423,6 +425,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
break;
}
case DatabaseWidget::EditMode:
case DatabaseWidget::ImportMode:
case DatabaseWidget::LockedMode: {
const QList<QAction*> entryActions = m_ui->menuEntries->actions();
for (QAction* action : entryActions) {
@ -487,7 +490,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->actionDatabaseMerge->setEnabled(inDatabaseTabWidget);
m_ui->actionRepairDatabase->setEnabled(inDatabaseTabWidgetOrWelcomeWidget);

View File

@ -174,6 +174,13 @@
<string>&amp;Recent databases</string>
</property>
</widget>
<widget class="QMenu" name="menuImport">
<property name="title">
<string>Import</string>
</property>
<addaction name="actionImportKeePass1"/>
<addaction name="actionImportCsv"/>
</widget>
<addaction name="actionDatabaseNew"/>
<addaction name="actionDatabaseOpen"/>
<addaction name="menuRecentDatabases"/>
@ -185,7 +192,7 @@
<addaction name="actionChangeDatabaseSettings"/>
<addaction name="separator"/>
<addaction name="actionDatabaseMerge"/>
<addaction name="actionImportKeePass1"/>
<addaction name="menuImport"/>
<addaction name="actionExportCsv"/>
<addaction name="actionRepairDatabase"/>
<addaction name="separator"/>
@ -392,11 +399,6 @@
<string>Database settings</string>
</property>
</action>
<action name="actionImportKeePass1">
<property name="text">
<string>&amp;Import KeePass 1 database</string>
</property>
</action>
<action name="actionEntryClone">
<property name="enabled">
<bool>false</bool>
@ -504,6 +506,16 @@
<string>&amp;Export to CSV file</string>
</property>
</action>
<action name="actionImportKeePass1">
<property name="text">
<string>Import KeePass 1 database</string>
</property>
</action>
<action name="actionImportCsv">
<property name="text">
<string>Import CSV file</string>
</property>
</action>
<action name="actionRepairDatabase">
<property name="text">
<string>Re&amp;pair database</string>

View File

@ -0,0 +1,297 @@
/*
* Copyright (C) 2016 Enrico Mariotti <enricomariotti@yahoo.it>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "CsvImportWidget.h"
#include "ui_CsvImportWidget.h"
#include <QFile>
#include <QFileInfo>
#include <QSpacerItem>
#include "gui/MessageBox.h"
#include "gui/MessageWidget.h"
//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_ui->setupUi(this);
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);
m_ui->messageWidget->setHidden(true);
for (int i = 0; i < m_columnHeader.count(); ++i) {
QLabel* label = new QLabel(m_columnHeader.at(i), this);
label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
QFont 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);
QSpacerItem *item = new QSpacerItem(1,1, QSizePolicy::Expanding, QSizePolicy::Fixed);
m_ui->gridLayout_combos->addItem(item, x, y+2);
}
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_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<QComboBox*>(m_comboMapper->mapping(comboId));
if (currentSender->currentIndex() != -1)
//this line is the one that actually updates 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; c < m_ui->tableViewFields->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(qMax(0, m_parserModel->rowCount() - 1));
int i;
QStringList list(tr("Not present in CSV file"));
for (i = 1; i < m_parserModel->getCsvCols(); ++i) {
QString s = QString(tr("Column ")) + QString::number(i);
list << s;
}
m_comboModel->setStringList(list);
i=1;
for (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) {
//QApplication::processEvents();
m_db = db;
m_parserModel->setFilename(filename);
m_ui->labelFilename->setText(filename);
Group* group = m_db->rootGroup();
group->setUuid(Uuid::random());
group->setNotes(tr("Imported from CSV file").append("\n").append(tr("Original data: ")) + filename);
parse();
}
void CsvImportWidget::parse() {
configParser();
QApplication::setOverrideCursor(Qt::WaitCursor);
//QApplication::processEvents();
bool good = m_parserModel->parse();
updatePreview();
QApplication::restoreOverrideCursor();
if (!good)
m_ui->messageWidget->showMessage(tr("Error(s) detected in CSV file !").append("\n")
.append(formatStatusText()), MessageWidget::Warning);
else
m_ui->messageWidget->setHidden(true);
QWidget::adjustSize();
}
QString CsvImportWidget::formatStatusText() const {
QString text = m_parserModel->getStatus();
int items = text.count('\n');
if (items > 2) {
return text.section('\n', 0, 1)
.append("\n[").append(QString::number(items - 2))
.append(tr(" more messages skipped]"));
}
if (items == 1) {
text.append(QString("\n"));
}
return text;
}
void CsvImportWidget::writeDatabase() {
setRootGroup();
for (int r = 0; r < m_parserModel->rowCount(); ++r) {
//use validity of second column as a GO/NOGO for all others fields
if (not m_parserModel->data(m_parserModel->index(r, 1)).isValid())
continue;
Entry* entry = new Entry();
entry->setUuid(Uuid::random());
entry->setGroup(splitGroups(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);
emit editFinished(true);
}
void CsvImportWidget::setRootGroup() {
QString groupLabel;
QStringList groupList;
bool is_root = false;
bool is_empty = false;
bool is_label = false;
for (int r = 0; r < m_parserModel->rowCount(); ++r) {
//use validity of second column as a GO/NOGO for all others fields
if (not m_parserModel->data(m_parserModel->index(r, 1)).isValid())
continue;
groupLabel = m_parserModel->data(m_parserModel->index(r, 0)).toString();
//check if group name is either "root", "" (empty) or some other label
groupList = groupLabel.split("/", QString::SkipEmptyParts);
if (groupList.isEmpty())
is_empty = true;
else
if (not groupList.first().compare("Root", Qt::CaseSensitive))
is_root = true;
else if (not groupLabel.compare(""))
is_empty = true;
else
is_label = true;
groupList.clear();
}
if ((is_empty and is_root) or (is_label and not is_empty and is_root))
m_db->rootGroup()->setName("CSV IMPORTED");
else
m_db->rootGroup()->setName("Root");
}
Group *CsvImportWidget::splitGroups(QString label) {
//extract group names from nested path provided in "label"
Group *current = m_db->rootGroup();
if (label.isEmpty())
return current;
QStringList groupList = label.split("/", QString::SkipEmptyParts);
//avoid the creation of a subgroup with the same name as Root
if (m_db->rootGroup()->name() == "Root" && groupList.first() == "Root")
groupList.removeFirst();
for (const QString& groupName : groupList) {
Group *children = hasChildren(current, groupName);
if (children == nullptr) {
Group *brandNew = new Group();
brandNew->setParent(current);
brandNew->setName(groupName);
brandNew->setUuid(Uuid::random());
current = brandNew;
} else {
Q_ASSERT(children != nullptr);
current = children;
}
}
return current;
}
Group* CsvImportWidget::hasChildren(Group* current, QString groupName) {
//returns the group whose name is "groupName" and is child of "current" group
for (Group * group : current->children()) {
if (group->name() == groupName)
return group;
}
return nullptr;
}
void CsvImportWidget::reject() {
emit editFinished(false);
}

View File

@ -0,0 +1,78 @@
/*
* Copyright (C) 2016 Enrico Mariotti <enricomariotti@yahoo.it>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_CSVIMPORTWIDGET_H
#define KEEPASSX_CSVIMPORTWIDGET_H
#include <QScopedPointer>
#include <QPushButton>
#include <QStringListModel>
#include <QSignalMapper>
#include <QList>
#include <QComboBox>
#include <QStackedWidget>
#include "core/Metadata.h"
#include "format/KeePass2Writer.h"
#include "gui/csvImport/CsvParserModel.h"
#include "keys/PasswordKey.h"
namespace Ui {
class CsvImportWidget;
}
class CsvImportWidget : public QWidget
{
Q_OBJECT
public:
explicit CsvImportWidget(QWidget *parent = nullptr);
~CsvImportWidget();
void load(const QString& filename, Database* const db);
signals:
void editFinished(bool accepted);
private slots:
void parse();
void comboChanged(int comboId);
void skippedChanged(int rows);
void writeDatabase();
void setRootGroup();
void reject();
private:
Q_DISABLE_COPY(CsvImportWidget)
const QScopedPointer<Ui::CsvImportWidget> m_ui;
CsvParserModel* const m_parserModel;
QStringListModel* const m_comboModel;
QSignalMapper* m_comboMapper;
QList<QComboBox*> m_combos;
Database *m_db;
KeePass2Writer m_writer;
static const QStringList m_columnHeader;
void configParser();
void updatePreview();
void updateTableview();
Group* splitGroups(QString label);
Group* hasChildren(Group* current, QString groupName);
QString formatStatusText() const;
};
#endif // KEEPASSX_CSVIMPORTWIDGET_H

View File

@ -0,0 +1,584 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CsvImportWidget</class>
<widget class="QWidget" name="CsvImportWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>779</width>
<height>691</height>
</rect>
</property>
<property name="windowTitle">
<string/>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0" rowspan="2">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="MessageWidget" name="messageWidget" native="true"/>
</item>
<item>
<widget class="QLabel" name="labelHeadline">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Import CSV fields</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelFilename">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>filename</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelSizeRowsCols">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>size, rows, columns</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0" colspan="2">
<widget class="QGroupBox" name="Encoding">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="title">
<string>Encoding</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="4">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Preferred</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>114</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="4">
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>114</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="3">
<widget class="QCheckBox" name="checkBoxBackslash">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Consider '\' an escape character</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="labelCodec">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Codec</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QLabel" name="labelWarnings">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelFieldSeparator">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Fields are separated by</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="comboBoxFieldSeparator">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="labelComments">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Comments start with</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="2">
<spacer name="horizontalSpacer_10">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>114</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="comboBoxTextQualifier">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="2">
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>114</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="2">
<spacer name="horizontalSpacer_9">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>114</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelTextQualifier">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Text is qualified by</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBoxCodec">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="2">
<spacer name="horizontalSpacer_7">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>114</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="3">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="labelSkipRows">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Skip first</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBoxSkip">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelSkipRows_2">
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>rows</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="comboBoxComment">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="4">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>114</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="4">
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>114</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="QGroupBox" name="groupBoxColumnAssociations">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="title">
<string>Column layout</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<layout class="QGridLayout" name="gridLayout_combos">
<property name="leftMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
</layout>
</item>
</layout>
</widget>
</item>
<item row="2" column="0" colspan="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>758</width>
<height>24</height>
</size>
</property>
</spacer>
</item>
<item row="6" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>false</bool>
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<widget class="QGroupBox" name="groupBoxPreview">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>200</height>
</size>
</property>
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="title">
<string>Preview</string>
</property>
<property name="checkable">
<bool>false</bool>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTableView" name="tableViewFields">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<weight>50</weight>
<bold>false</bold>
</font>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>MessageWidget</class>
<extends>QWidget</extends>
<header>gui/MessageWidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,77 @@
/*
* Copyright (C) 2016 Enrico Mariotti <enricomariotti@yahoo.it>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "CsvImportWizard.h"
#include <QApplication>
#include <QLabel>
#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"));
QFont headLineFont = key->headlineLabel()->font();
headLineFont.setBold(true);
headLineFont.setPointSize(headLineFont.pointSize() + 2);
key->headlineLabel()->setFont(headLineFont);
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) {
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"));
emit(importFinished(false));
}
}
void CsvImportWizard::parseFinished(bool accepted)
{
emit(importFinished(accepted));
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (C) 2016 Enrico Mariotti <enricomariotti@yahoo.it>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_CSVIMPORTWIZARD_H
#define KEEPASSX_CSVIMPORTWIZARD_H
#include "CsvImportWidget.h"
#include <QStackedWidget>
#include <QGridLayout>
#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);
~CsvImportWizard();
void load(const QString& filename, Database *database);
signals:
void importFinished(bool accepted);
private 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

View File

@ -0,0 +1,125 @@
/*
* Copyright (C) 2016 Enrico Mariotti <enricomariotti@yahoo.it>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "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(qMax(0, 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 < columnCount(); ++i)
m_columnMap.insert(i,0);
addEmptyColumn();
endResetModel();
return r;
}
void CsvParserModel::addEmptyColumn() {
for (int i = 0; i < m_table.size(); ++i) {
CsvRow r = m_table.at(i);
r.prepend(QString(""));
m_table.replace(i, r);
}
}
void CsvParserModel::mapColumns(int csvColumn, int dbColumn) {
if ((csvColumn < 0) || (dbColumn < 0))
return;
beginResetModel();
if (csvColumn >= 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());
emit dataChanged(topLeft, bottomRight);
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();
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (C) 2016 Enrico Mariotti <enricomariotti@yahoo.it>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_CSVPARSERMODEL_H
#define KEEPASSX_CSVPARSERMODEL_H
#include <QAbstractTableModel>
#include <QMap>
#include "core/CsvParser.h"
#include "core/Group.h"
class CsvParserModel : public QAbstractTableModel, public CsvParser
{
Q_OBJECT
public:
explicit CsvParserModel(QObject *parent = nullptr);
~CsvParserModel();
void setFilename(const QString& filename);
QString getFileInfo();
bool parse();
void setHeaderLabels(QStringList l);
void mapColumns(int csvColumn, int dbColumn);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
public 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<int, int> m_columnMap;
};
#endif //KEEPASSX_CSVPARSERMODEL_H

View File

@ -77,7 +77,7 @@ public:
*/
void detect();
Q_SIGNALS:
signals:
/** Emitted in response to detect() when a device is found
*
* @slot is the slot number detected

View File

@ -26,6 +26,7 @@
#include "crypto/Crypto.h"
#include "gui/Application.h"
#include "gui/MainWindow.h"
#include "gui/csvImport/CsvImportWizard.h"
#include "gui/MessageBox.h"
#if defined(WITH_ASAN) && defined(WITH_LSAN)

View File

@ -158,6 +158,9 @@ endif()
add_unit_test(NAME testentry SOURCES TestEntry.cpp
LIBS ${TEST_LIBRARIES})
add_unit_test(NAME testcsvparser SOURCES TestCsvParser.cpp
LIBS ${TEST_LIBRARIES})
add_unit_test(NAME testrandom SOURCES TestRandom.cpp
LIBS ${TEST_LIBRARIES})

336
tests/TestCsvParser.cpp Normal file
View File

@ -0,0 +1,336 @@
/*
* Copyright (C) 2015 Enrico Mariotti <enricomariotti@yahoo.it>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "TestCsvParser.h"
#include <QTest>
QTEST_GUILESS_MAIN(TestCsvParser)
void TestCsvParser::initTestCase()
{
parser = new CsvParser();
}
void TestCsvParser::cleanupTestCase()
{
delete parser;
}
void TestCsvParser::init()
{
file = new QTemporaryFile();
if (not file->open())
QFAIL("Cannot open file!");
parser->setBackslashSyntax(false);
parser->setComment('#');
parser->setFieldSeparator(',');
parser->setTextQualifier(QChar('"'));
}
void TestCsvParser::cleanup()
{
file->remove();
}
/****************** 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) == "");
QVERIFY(t.at(0).at(2) == "3śAż");
QVERIFY(t.at(0).at(3) == "żac");
}

70
tests/TestCsvParser.h Normal file
View File

@ -0,0 +1,70 @@
/*
* Copyright (C) 2015 Enrico Mariotti <enricomariotti@yahoo.it>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_TESTCSVPARSER_H
#define KEEPASSX_TESTCSVPARSER_H
#include <QObject>
#include <QFile>
#include <QTemporaryFile>
#include "core/CsvParser.h"
class CsvParser;
class TestCsvParser : public QObject
{
Q_OBJECT
public:
private 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();
private:
QTemporaryFile* file;
CsvParser* parser;
CsvTable t;
void dumpRow(CsvTable table, int row);
};
#endif // KEEPASSX_TESTCSVPARSER_H

View File

@ -26,7 +26,7 @@ class TestYubiKeyChalResp: public QObject
{
Q_OBJECT
private Q_SLOTS:
private slots:
void initTestCase();
void cleanupTestCase();