mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-03-22 22:26:39 -04:00
Improve CSV export and import capability
* Fixes #3541 * CSV export now includes TOTP settings, Entry Icon (database icon number only), Modified Time, and Created Time. * CSV import properly understands time in ISO 8601 format and Unix Timestamp. * CSV import will set the TOTP settings and entry icon based on the chosen column.
This commit is contained in:
parent
3f7e79cdf3
commit
7ac651763c
@ -465,7 +465,8 @@ void Entry::setTotp(QSharedPointer<Totp::Settings> settings)
|
||||
m_data.totpSettings.reset();
|
||||
} else {
|
||||
m_data.totpSettings = std::move(settings);
|
||||
auto text = Totp::writeSettings(m_data.totpSettings, title(), username());
|
||||
auto text = Totp::writeSettings(
|
||||
m_data.totpSettings, resolveMultiplePlaceholders(title()), resolveMultiplePlaceholders(username()));
|
||||
if (m_data.totpSettings->format != Totp::StorageFormat::LEGACY) {
|
||||
m_attributes->set(Totp::ATTRIBUTE_OTP, text, true);
|
||||
} else {
|
||||
@ -493,6 +494,15 @@ QSharedPointer<Totp::Settings> Entry::totpSettings() const
|
||||
return m_data.totpSettings;
|
||||
}
|
||||
|
||||
QString Entry::totpSettingsString() const
|
||||
{
|
||||
if (m_data.totpSettings) {
|
||||
return Totp::writeSettings(
|
||||
m_data.totpSettings, resolveMultiplePlaceholders(title()), resolveMultiplePlaceholders(username()), true);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void Entry::setUuid(const QUuid& uuid)
|
||||
{
|
||||
Q_ASSERT(!uuid.isNull());
|
||||
|
@ -106,6 +106,7 @@ public:
|
||||
QString notes() const;
|
||||
QString attribute(const QString& key) const;
|
||||
QString totp() const;
|
||||
QString totpSettingsString() const;
|
||||
QSharedPointer<Totp::Settings> totpSettings() const;
|
||||
int size() const;
|
||||
|
||||
|
@ -67,6 +67,10 @@ QString CsvExporter::exportHeader()
|
||||
addColumn(header, "Password");
|
||||
addColumn(header, "URL");
|
||||
addColumn(header, "Notes");
|
||||
addColumn(header, "TOTP");
|
||||
addColumn(header, "Icon");
|
||||
addColumn(header, "Last Modified");
|
||||
addColumn(header, "Created");
|
||||
return header + QString("\n");
|
||||
}
|
||||
|
||||
@ -88,6 +92,10 @@ QString CsvExporter::exportGroup(const Group* group, QString groupPath)
|
||||
addColumn(line, entry->password());
|
||||
addColumn(line, entry->url());
|
||||
addColumn(line, entry->notes());
|
||||
addColumn(line, entry->totpSettingsString());
|
||||
addColumn(line, QString::number(entry->iconNumber()));
|
||||
addColumn(line, entry->timeInfo().lastModificationTime().toString(Qt::ISODate));
|
||||
addColumn(line, entry->timeInfo().creationTime().toString(Qt::ISODate));
|
||||
|
||||
line.append("\n");
|
||||
response.append(line);
|
||||
|
@ -27,6 +27,7 @@
|
||||
#include "format/KeePass2Writer.h"
|
||||
#include "gui/MessageBox.h"
|
||||
#include "gui/MessageWidget.h"
|
||||
#include "totp/totp.h"
|
||||
|
||||
// I wanted to make the CSV import GUI future-proof, so if one day you need a new field,
|
||||
// all you have to do is add a field to m_columnHeader, and the GUI will follow:
|
||||
@ -39,7 +40,8 @@ CsvImportWidget::CsvImportWidget(QWidget* parent)
|
||||
, m_comboModel(new QStringListModel(this))
|
||||
, m_columnHeader(QStringList() << QObject::tr("Group") << QObject::tr("Title") << QObject::tr("Username")
|
||||
<< QObject::tr("Password") << QObject::tr("URL") << QObject::tr("Notes")
|
||||
<< QObject::tr("Last Modified") << QObject::tr("Created"))
|
||||
<< QObject::tr("TOTP") << QObject::tr("Icon") << QObject::tr("Last Modified")
|
||||
<< QObject::tr("Created"))
|
||||
, m_fieldSeparatorList(QStringList() << ","
|
||||
<< ";"
|
||||
<< "-"
|
||||
@ -54,7 +56,7 @@ CsvImportWidget::CsvImportWidget(QWidget* parent)
|
||||
m_ui->messageWidget->setHidden(true);
|
||||
|
||||
m_combos << m_ui->groupCombo << m_ui->titleCombo << m_ui->usernameCombo << m_ui->passwordCombo << m_ui->urlCombo
|
||||
<< m_ui->notesCombo << m_ui->lastModifiedCombo << m_ui->createdCombo;
|
||||
<< m_ui->notesCombo << m_ui->totpCombo << m_ui->iconCombo << m_ui->lastModifiedCombo << m_ui->createdCombo;
|
||||
|
||||
for (auto combo : m_combos) {
|
||||
combo->setModel(m_comboModel);
|
||||
@ -206,17 +208,38 @@ void CsvImportWidget::writeDatabase()
|
||||
entry->setUrl(m_parserModel->data(m_parserModel->index(r, 4)).toString());
|
||||
entry->setNotes(m_parserModel->data(m_parserModel->index(r, 5)).toString());
|
||||
|
||||
TimeInfo timeInfo;
|
||||
if (m_parserModel->data(m_parserModel->index(r, 6)).isValid()) {
|
||||
qint64 lastModified = m_parserModel->data(m_parserModel->index(r, 6)).toString().toLongLong();
|
||||
if (lastModified) {
|
||||
timeInfo.setLastModificationTime(Clock::datetimeUtc(lastModified * 1000));
|
||||
auto totp = Totp::parseSettings(m_parserModel->data(m_parserModel->index(r, 6)).toString());
|
||||
entry->setTotp(totp);
|
||||
}
|
||||
|
||||
bool ok;
|
||||
int icon = m_parserModel->data(m_parserModel->index(r, 7)).toInt(&ok);
|
||||
if (ok) {
|
||||
entry->setIcon(icon);
|
||||
}
|
||||
|
||||
TimeInfo timeInfo;
|
||||
if (m_parserModel->data(m_parserModel->index(r, 8)).isValid()) {
|
||||
auto datetime = m_parserModel->data(m_parserModel->index(r, 8)).toString();
|
||||
if (datetime.contains(QRegularExpression("^\\d+$"))) {
|
||||
timeInfo.setLastModificationTime(Clock::datetimeUtc(datetime.toLongLong() * 1000));
|
||||
} else {
|
||||
auto lastModified = QDateTime::fromString(datetime, Qt::ISODate);
|
||||
if (lastModified.isValid()) {
|
||||
timeInfo.setLastModificationTime(lastModified);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m_parserModel->data(m_parserModel->index(r, 7)).isValid()) {
|
||||
qint64 created = m_parserModel->data(m_parserModel->index(r, 7)).toString().toLongLong();
|
||||
if (created) {
|
||||
timeInfo.setCreationTime(Clock::datetimeUtc(created * 1000));
|
||||
if (m_parserModel->data(m_parserModel->index(r, 9)).isValid()) {
|
||||
auto datetime = m_parserModel->data(m_parserModel->index(r, 9)).toString();
|
||||
if (datetime.contains(QRegularExpression("^\\d+$"))) {
|
||||
timeInfo.setCreationTime(Clock::datetimeUtc(datetime.toLongLong() * 1000));
|
||||
} else {
|
||||
auto created = QDateTime::fromString(datetime, Qt::ISODate);
|
||||
if (created.isValid()) {
|
||||
timeInfo.setCreationTime(created);
|
||||
}
|
||||
}
|
||||
}
|
||||
entry->setTimeInfo(timeInfo);
|
||||
|
@ -96,21 +96,8 @@
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout_combos">
|
||||
<item row="2" column="2">
|
||||
<widget class="QLabel" name="lastModifiedLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>50</weight>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Last Modified</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
<item row="3" column="1">
|
||||
<widget class="QComboBox" name="passwordCombo"/>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="passwordLabel">
|
||||
@ -126,13 +113,13 @@
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="indent">
|
||||
<number>2</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QComboBox" name="passwordCombo"/>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="QLabel" name="createdLabel">
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="usernameLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>50</weight>
|
||||
@ -140,28 +127,21 @@
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Created</string>
|
||||
<string>Username</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="indent">
|
||||
<number>2</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QLabel" name="notesLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>50</weight>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Notes</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="usernameCombo"/>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="titleCombo"/>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="titleLabel">
|
||||
@ -177,6 +157,9 @@
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="indent">
|
||||
<number>2</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
@ -196,9 +179,12 @@
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="indent">
|
||||
<number>2</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="urlLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
@ -212,10 +198,16 @@
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="indent">
|
||||
<number>2</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="usernameLabel">
|
||||
<item row="4" column="1">
|
||||
<widget class="QComboBox" name="urlCombo"/>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="notesLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>50</weight>
|
||||
@ -223,30 +215,106 @@
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Username</string>
|
||||
<string>Notes</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="indent">
|
||||
<number>2</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="usernameCombo"/>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="titleCombo"/>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<widget class="QComboBox" name="urlCombo"/>
|
||||
</item>
|
||||
<item row="1" column="3">
|
||||
<widget class="QComboBox" name="notesCombo"/>
|
||||
</item>
|
||||
<item row="2" column="3">
|
||||
<widget class="QComboBox" name="lastModifiedCombo"/>
|
||||
<item row="1" column="2">
|
||||
<widget class="QLabel" name="totpLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>50</weight>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TOTP</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="indent">
|
||||
<number>2</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="3">
|
||||
<widget class="QComboBox" name="totpCombo"/>
|
||||
</item>
|
||||
<item row="4" column="3">
|
||||
<widget class="QComboBox" name="createdCombo"/>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<widget class="QLabel" name="createdLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>50</weight>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Created</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="indent">
|
||||
<number>2</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="QLabel" name="lastModifiedLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>50</weight>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Last Modified</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="indent">
|
||||
<number>2</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="3">
|
||||
<widget class="QComboBox" name="createdCombo"/>
|
||||
<widget class="QComboBox" name="lastModifiedCombo"/>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QLabel" name="iconLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>50</weight>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Icon</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="indent">
|
||||
<number>2</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="3">
|
||||
<widget class="QComboBox" name="iconCombo"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
@ -682,10 +750,6 @@
|
||||
<tabstop>titleCombo</tabstop>
|
||||
<tabstop>usernameCombo</tabstop>
|
||||
<tabstop>passwordCombo</tabstop>
|
||||
<tabstop>urlCombo</tabstop>
|
||||
<tabstop>notesCombo</tabstop>
|
||||
<tabstop>lastModifiedCombo</tabstop>
|
||||
<tabstop>createdCombo</tabstop>
|
||||
<tabstop>comboBoxCodec</tabstop>
|
||||
<tabstop>comboBoxTextQualifier</tabstop>
|
||||
<tabstop>comboBoxFieldSeparator</tabstop>
|
||||
|
@ -928,10 +928,10 @@ void TestCli::testExport()
|
||||
setInput("a");
|
||||
execCmd(exportCmd, {"export", "-f", "csv", m_dbFile->fileName()});
|
||||
QByteArray csvHeader = m_stdout->readLine();
|
||||
QCOMPARE(csvHeader, QByteArray("\"Group\",\"Title\",\"Username\",\"Password\",\"URL\",\"Notes\"\n"));
|
||||
QVERIFY(csvHeader.contains(QByteArray("\"Group\",\"Title\",\"Username\",\"Password\",\"URL\",\"Notes\"")));
|
||||
QByteArray csvData = m_stdout->readAll();
|
||||
QVERIFY(csvData.contains(QByteArray(
|
||||
"\"NewDatabase\",\"Sample Entry\",\"User Name\",\"Password\",\"http://www.somesite.com/\",\"Notes\"\n")));
|
||||
"\"NewDatabase\",\"Sample Entry\",\"User Name\",\"Password\",\"http://www.somesite.com/\",\"Notes\"")));
|
||||
|
||||
// test invalid format
|
||||
setInput("a");
|
||||
|
@ -23,11 +23,13 @@
|
||||
|
||||
#include "crypto/Crypto.h"
|
||||
#include "format/CsvExporter.h"
|
||||
#include "totp/totp.h"
|
||||
|
||||
QTEST_GUILESS_MAIN(TestCsvExporter)
|
||||
|
||||
const QString TestCsvExporter::ExpectedHeaderLine =
|
||||
QString("\"Group\",\"Title\",\"Username\",\"Password\",\"URL\",\"Notes\"\n");
|
||||
QString("\"Group\",\"Title\",\"Username\",\"Password\",\"URL\",\"Notes\",\"TOTP\",\"Icon\",\"Last "
|
||||
"Modified\",\"Created\"\n");
|
||||
|
||||
void TestCsvExporter::init()
|
||||
{
|
||||
@ -57,17 +59,23 @@ void TestCsvExporter::testExport()
|
||||
entry->setPassword("Test Password");
|
||||
entry->setUrl("http://test.url");
|
||||
entry->setNotes("Test Notes");
|
||||
entry->setTotp(Totp::createSettings("DFDF", Totp::DEFAULT_DIGITS, Totp::DEFAULT_STEP));
|
||||
entry->setIcon(5);
|
||||
|
||||
QBuffer buffer;
|
||||
QVERIFY(buffer.open(QIODevice::ReadWrite));
|
||||
m_csvExporter->exportDatabase(&buffer, m_db);
|
||||
auto exported = QString::fromUtf8(buffer.buffer());
|
||||
|
||||
QString expectedResult = QString()
|
||||
.append(ExpectedHeaderLine)
|
||||
.append("\"Passwords/Test Group Name\",\"Test Entry Title\",\"Test Username\",\"Test "
|
||||
"Password\",\"http://test.url\",\"Test Notes\"\n");
|
||||
"Password\",\"http://test.url\",\"Test Notes\"");
|
||||
|
||||
QCOMPARE(QString::fromUtf8(buffer.buffer().constData()), expectedResult);
|
||||
QVERIFY(exported.startsWith(expectedResult));
|
||||
exported.remove(expectedResult);
|
||||
QVERIFY(exported.contains("otpauth://"));
|
||||
QVERIFY(exported.contains(",\"5\","));
|
||||
}
|
||||
|
||||
void TestCsvExporter::testEmptyDatabase()
|
||||
@ -95,10 +103,9 @@ void TestCsvExporter::testNestedGroups()
|
||||
QBuffer buffer;
|
||||
QVERIFY(buffer.open(QIODevice::ReadWrite));
|
||||
m_csvExporter->exportDatabase(&buffer, m_db);
|
||||
|
||||
QCOMPARE(
|
||||
QString::fromUtf8(buffer.buffer().constData()),
|
||||
auto exported = QString::fromUtf8(buffer.buffer());
|
||||
QVERIFY(exported.startsWith(
|
||||
QString()
|
||||
.append(ExpectedHeaderLine)
|
||||
.append("\"Passwords/Test Group Name/Test Sub Group Name\",\"Test Entry Title\",\"\",\"\",\"\",\"\"\n"));
|
||||
.append("\"Passwords/Test Group Name/Test Sub Group Name\",\"Test Entry Title\",\"\",\"\",\"\",\"\"")));
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user