diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 81f856ff5..82c9e7bc3 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -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()); diff --git a/src/core/Entry.h b/src/core/Entry.h index 27df86596..a0dbbf7d4 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -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; diff --git a/src/format/CsvExporter.cpp b/src/format/CsvExporter.cpp index 98fc6fdc8..281b94761 100644 --- a/src/format/CsvExporter.cpp +++ b/src/format/CsvExporter.cpp @@ -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); diff --git a/src/gui/csvImport/CsvImportWidget.cpp b/src/gui/csvImport/CsvImportWidget.cpp index 01fd5fc89..e78e9f94a 100644 --- a/src/gui/csvImport/CsvImportWidget.cpp +++ b/src/gui/csvImport/CsvImportWidget.cpp @@ -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); diff --git a/src/gui/csvImport/CsvImportWidget.ui b/src/gui/csvImport/CsvImportWidget.ui index 1c268fd9d..d3364cb4c 100644 --- a/src/gui/csvImport/CsvImportWidget.ui +++ b/src/gui/csvImport/CsvImportWidget.ui @@ -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> diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index 348afb670..80af58bad 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -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"); diff --git a/tests/TestCsvExporter.cpp b/tests/TestCsvExporter.cpp index 63ba11488..8e7e6021d 100644 --- a/tests/TestCsvExporter.cpp +++ b/tests/TestCsvExporter.cpp @@ -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\",\"\",\"\",\"\",\"\""))); }