From 667370e54eb02eacc986d78ff287b351a7d6840f Mon Sep 17 00:00:00 2001 From: louib Date: Sun, 16 Jun 2019 21:33:13 -0400 Subject: [PATCH] CLI: Export database as CSV * Changed `Extract` to `Export` to support additional formats * Allow database expot as CSV. Added a `--format` option to the `Export` command for that, which defaults to xml, so the current behavior is unchanged. *The `CsvExporter` had to be refactored a bit, but nothing major. It can now print to a file or return a string. --- CHANGELOG.md | 6 ++- src/cli/CMakeLists.txt | 2 +- src/cli/Command.cpp | 4 +- src/cli/Export.cpp | 65 +++++++++++++++++++++++++++++++++ src/cli/{Extract.h => Export.h} | 12 +++--- src/cli/Extract.cpp | 46 ----------------------- src/cli/keepassxc-cli.1 | 10 ++++- src/format/CsvExporter.cpp | 48 ++++++++++++++---------- src/format/CsvExporter.h | 4 +- tests/TestCli.cpp | 45 +++++++++++++++++++---- tests/TestCli.h | 2 +- 11 files changed, 156 insertions(+), 88 deletions(-) create mode 100644 src/cli/Export.cpp rename src/cli/{Extract.h => Export.h} (82%) delete mode 100644 src/cli/Extract.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f870ded8c..f9e1b006c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,15 @@ - Group sorting feature [#3282](https://github.com/keepassxreboot/keepassxc/issues/3282) - CLI: Add 'flatten' option to the 'ls' command [#3276](https://github.com/keepassxreboot/keepassxc/issues/3276) - CLI: Add password generation options to `Add` and `Edit` commands [#3275](https://github.com/keepassxreboot/keepassxc/issues/3275) +- CLI: Add CSV export to the 'export' command [#3277] - Add 'Monospaced font' option to the Notes field [#3321](https://github.com/keepassxreboot/keepassxc/issues/3321) ### Changed -- 💥💥 CLI: The password length option `-l` for the CLI commands +- CLI: The password length option `-l` for the CLI commands `Add` and `Edit` is now `-L` [#3275](https://github.com/keepassxreboot/keepassxc/issues/3275) -- 💥💥 CLI: the `-u` shorthand for the `--upper` password generation option has been renamed `-U` [#3275](https://github.com/keepassxreboot/keepassxc/issues/3275) +- CLI: the `-u` shorthand for the `--upper` password generation option has been renamed `-U` [#3275](https://github.com/keepassxreboot/keepassxc/issues/3275) +- CLI: Renamed command `extract` -> `export`. [#3277] - Rework the Entry Preview panel [#3306](https://github.com/keepassxreboot/keepassxc/issues/3306) - Move notes to General tab on Group Preview Panel [#3336](https://github.com/keepassxreboot/keepassxc/issues/3336) - Drop to background when copy feature [#3253](https://github.com/keepassxreboot/keepassxc/issues/3253) diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt index f75d6c6f2d..4a8b28c3b7 100644 --- a/src/cli/CMakeLists.txt +++ b/src/cli/CMakeLists.txt @@ -23,7 +23,7 @@ set(cli_SOURCES Diceware.cpp Edit.cpp Estimate.cpp - Extract.cpp + Export.cpp Generate.cpp List.cpp Locate.cpp diff --git a/src/cli/Command.cpp b/src/cli/Command.cpp index b1d5881a05..fdea26e65f 100644 --- a/src/cli/Command.cpp +++ b/src/cli/Command.cpp @@ -29,7 +29,7 @@ #include "Diceware.h" #include "Edit.h" #include "Estimate.h" -#include "Extract.h" +#include "Export.h" #include "Generate.h" #include "List.h" #include "Locate.h" @@ -114,7 +114,7 @@ void populateCommands() commands.insert(QString("diceware"), new Diceware()); commands.insert(QString("edit"), new Edit()); commands.insert(QString("estimate"), new Estimate()); - commands.insert(QString("extract"), new Extract()); + commands.insert(QString("export"), new Export()); commands.insert(QString("generate"), new Generate()); commands.insert(QString("locate"), new Locate()); commands.insert(QString("ls"), new List()); diff --git a/src/cli/Export.cpp b/src/cli/Export.cpp new file mode 100644 index 0000000000..77acaf8067 --- /dev/null +++ b/src/cli/Export.cpp @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include "Export.h" + +#include "cli/TextStream.h" +#include "cli/Utils.h" +#include "core/Database.h" +#include "format/CsvExporter.h" + +const QCommandLineOption Export::FormatOption = + QCommandLineOption(QStringList() << "f" + << "format", + QObject::tr("Format to use when exporting. Available choices are xml or csv. Defaults to xml."), + QObject::tr("xml|csv")); + +Export::Export() +{ + name = QString("export"); + options.append(Export::FormatOption); + description = QObject::tr("Exports the content of a database to standard output in the specified format."); +} + + +int Export::executeWithDatabase(QSharedPointer database, QSharedPointer parser) +{ + TextStream outputTextStream(Utils::STDOUT, QIODevice::WriteOnly); + TextStream errorTextStream(Utils::STDERR, QIODevice::WriteOnly); + + QString format = parser->value(Export::FormatOption); + if (format.isEmpty() || format == QString("xml")) { + QByteArray xmlData; + QString errorMessage; + if (!database->extract(xmlData, &errorMessage)) { + errorTextStream << QObject::tr("Unable to export database to XML: %1").arg(errorMessage) << endl; + return EXIT_FAILURE; + } + outputTextStream << xmlData.constData() << endl; + } else if (format == QString("csv")) { + CsvExporter csvExporter; + outputTextStream << csvExporter.exportDatabase(database); + } else { + errorTextStream << QObject::tr("Unsupported format %1").arg(format) << endl; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/src/cli/Extract.h b/src/cli/Export.h similarity index 82% rename from src/cli/Extract.h rename to src/cli/Export.h index 929cbd7227..f7f5b86821 100644 --- a/src/cli/Extract.h +++ b/src/cli/Export.h @@ -15,17 +15,19 @@ * along with this program. If not, see . */ -#ifndef KEEPASSXC_EXTRACT_H -#define KEEPASSXC_EXTRACT_H +#ifndef KEEPASSXC_EXPORT_H +#define KEEPASSXC_EXPORT_H #include "DatabaseCommand.h" -class Extract : public DatabaseCommand +class Export : public DatabaseCommand { public: - Extract(); + Export(); int executeWithDatabase(QSharedPointer db, QSharedPointer parser) override; + + static const QCommandLineOption FormatOption; }; -#endif // KEEPASSXC_EXTRACT_H +#endif // KEEPASSXC_EXPORT_H diff --git a/src/cli/Extract.cpp b/src/cli/Extract.cpp deleted file mode 100644 index 9b863f0151..0000000000 --- a/src/cli/Extract.cpp +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2019 KeePassXC Team - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 or (at your option) - * version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include - -#include "Extract.h" - -#include "cli/TextStream.h" -#include "cli/Utils.h" -#include "core/Database.h" - -Extract::Extract() -{ - name = QString("extract"); - description = QObject::tr("Extract and print the content of a database."); -} - -int Extract::executeWithDatabase(QSharedPointer database, QSharedPointer) -{ - TextStream outputTextStream(Utils::STDOUT, QIODevice::WriteOnly); - TextStream errorTextStream(Utils::STDERR, QIODevice::WriteOnly); - - QByteArray xmlData; - QString errorMessage; - if (!database->extract(xmlData, &errorMessage)) { - errorTextStream << QObject::tr("Unable to extract database %1").arg(errorMessage) << endl; - return EXIT_FAILURE; - } - outputTextStream << xmlData.constData() << endl; - return EXIT_SUCCESS; -} diff --git a/src/cli/keepassxc-cli.1 b/src/cli/keepassxc-cli.1 index 18d249170d..24b9b0f764 100644 --- a/src/cli/keepassxc-cli.1 +++ b/src/cli/keepassxc-cli.1 @@ -36,8 +36,8 @@ The same password generation options as documented for the generate command can .IP "estimate [options] [password]" Estimates the entropy of a password. The password to estimate can be provided as a positional argument, or using the standard input. -.IP "extract [options] " -Extracts and prints the contents of a database to standard output in XML format. +.IP "export [options] " +Exports the content of a database to standard output in the specified format (defaults to XML). .IP "generate [options]" Generate a random password. @@ -164,6 +164,12 @@ otherwise the program will fail. If the wordlist has < 4000 words a warning will be printed to STDERR. +.SS "Export options" + +.IP "-f, --format" +Format to use when exporting. Available choices are xml or csv. Defaults to xml. + + .SS "List options" .IP "-R, --recursive" diff --git a/src/format/CsvExporter.cpp b/src/format/CsvExporter.cpp index 03d5a576f0..98fc6fdc83 100644 --- a/src/format/CsvExporter.cpp +++ b/src/format/CsvExporter.cpp @@ -35,21 +35,22 @@ bool CsvExporter::exportDatabase(const QString& filename, const QSharedPointer& db) { - QString header; - addColumn(header, "Group"); - addColumn(header, "Title"); - addColumn(header, "Username"); - addColumn(header, "Password"); - addColumn(header, "URL"); - addColumn(header, "Notes"); - header.append("\n"); + if (device->write(exportHeader().toUtf8()) == -1) { + m_error = device->errorString(); + return false; + } - if (device->write(header.toUtf8()) == -1) { + if (device->write(exportGroup(db->rootGroup()).toUtf8()) == -1) { m_error = device->errorString(); return false; } - return writeGroup(device, db->rootGroup()); + return true; +} + +QString CsvExporter::exportDatabase(const QSharedPointer& db) +{ + return exportHeader() + exportGroup(db->rootGroup()); } QString CsvExporter::errorString() const @@ -57,8 +58,21 @@ QString CsvExporter::errorString() const return m_error; } -bool CsvExporter::writeGroup(QIODevice* device, const Group* group, QString groupPath) +QString CsvExporter::exportHeader() { + QString header; + addColumn(header, "Group"); + addColumn(header, "Title"); + addColumn(header, "Username"); + addColumn(header, "Password"); + addColumn(header, "URL"); + addColumn(header, "Notes"); + return header + QString("\n"); +} + +QString CsvExporter::exportGroup(const Group* group, QString groupPath) +{ + QString response; if (!groupPath.isEmpty()) { groupPath.append("/"); } @@ -76,21 +90,15 @@ bool CsvExporter::writeGroup(QIODevice* device, const Group* group, QString grou addColumn(line, entry->notes()); line.append("\n"); - - if (device->write(line.toUtf8()) == -1) { - m_error = device->errorString(); - return false; - } + response.append(line); } const QList& children = group->children(); for (const Group* child : children) { - if (!writeGroup(device, child, groupPath)) { - return false; - } + response.append(exportGroup(child, groupPath)); } - return true; + return response; } void CsvExporter::addColumn(QString& str, const QString& column) diff --git a/src/format/CsvExporter.h b/src/format/CsvExporter.h index e71cf7fa9a..a982ed1095 100644 --- a/src/format/CsvExporter.h +++ b/src/format/CsvExporter.h @@ -31,10 +31,12 @@ class CsvExporter public: bool exportDatabase(const QString& filename, const QSharedPointer& db); bool exportDatabase(QIODevice* device, const QSharedPointer& db); + QString exportDatabase(const QSharedPointer& db); QString errorString() const; private: - bool writeGroup(QIODevice* device, const Group* group, QString groupPath = QString()); + QString exportGroup(const Group* group, QString groupPath = QString()); + QString exportHeader(); void addColumn(QString& str, const QString& column); QString m_error; diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index 0b7de14e7c..d1ae3992d9 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -39,7 +39,7 @@ #include "cli/Diceware.h" #include "cli/Edit.h" #include "cli/Estimate.h" -#include "cli/Extract.h" +#include "cli/Export.h" #include "cli/Generate.h" #include "cli/List.h" #include "cli/Locate.h" @@ -170,7 +170,7 @@ void TestCli::testCommand() QVERIFY(Command::getCommand("diceware")); QVERIFY(Command::getCommand("edit")); QVERIFY(Command::getCommand("estimate")); - QVERIFY(Command::getCommand("extract")); + QVERIFY(Command::getCommand("export")); QVERIFY(Command::getCommand("generate")); QVERIFY(Command::getCommand("locate")); QVERIFY(Command::getCommand("ls")); @@ -742,14 +742,14 @@ void TestCli::testEstimate() } } -void TestCli::testExtract() +void TestCli::testExport() { - Extract extractCmd; - QVERIFY(!extractCmd.name.isEmpty()); - QVERIFY(extractCmd.getDescriptionLine().contains(extractCmd.name)); + Export exportCmd; + QVERIFY(!exportCmd.name.isEmpty()); + QVERIFY(exportCmd.getDescriptionLine().contains(exportCmd.name)); Utils::Test::setNextPassword("a"); - extractCmd.execute({"extract", m_dbFile->fileName()}); + exportCmd.execute({"export", m_dbFile->fileName()}); m_stdoutFile->seek(0); m_stdoutFile->readLine(); // skip prompt line @@ -768,12 +768,41 @@ void TestCli::testExtract() // Quiet option QScopedPointer dbQuiet(new Database()); qint64 pos = m_stdoutFile->pos(); + qint64 posErr = m_stderrFile->pos(); Utils::Test::setNextPassword("a"); - extractCmd.execute({"extract", "-q", m_dbFile->fileName()}); + exportCmd.execute({"export", "-f", "xml", "-q", m_dbFile->fileName()}); m_stdoutFile->seek(pos); + m_stderrFile->seek(posErr); reader.readDatabase(m_stdoutFile.data(), dbQuiet.data()); QVERIFY(!reader.hasError()); QVERIFY(db.data()); + QCOMPARE(m_stderrFile->readAll(), QByteArray("")); + + // CSV exporting + pos = m_stdoutFile->pos(); + posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + exportCmd.execute({"export", "-f", "csv", m_dbFile->fileName()}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip prompt line + m_stderrFile->seek(posErr); + QByteArray csvHeader = m_stdoutFile->readLine(); + QCOMPARE(csvHeader, QByteArray("\"Group\",\"Title\",\"Username\",\"Password\",\"URL\",\"Notes\"\n")); + QByteArray csvData = m_stdoutFile->readAll(); + QVERIFY(csvData.contains(QByteArray( + "\"NewDatabase\",\"Sample Entry\",\"User Name\",\"Password\",\"http://www.somesite.com/\",\"Notes\"\n"))); + QCOMPARE(m_stderrFile->readAll(), QByteArray("")); + + // test invalid format + pos = m_stdoutFile->pos(); + posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + exportCmd.execute({"export", "-f", "yaml", m_dbFile->fileName()}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip prompt line + m_stderrFile->seek(posErr); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("")); + QCOMPARE(m_stderrFile->readLine(), QByteArray("Unsupported format yaml\n")); } void TestCli::testGenerate_data() diff --git a/tests/TestCli.h b/tests/TestCli.h index a313fe224f..09c55e0edb 100644 --- a/tests/TestCli.h +++ b/tests/TestCli.h @@ -52,7 +52,7 @@ private slots: void testEdit(); void testEstimate_data(); void testEstimate(); - void testExtract(); + void testExport(); void testGenerate_data(); void testGenerate(); void testKeyFileOption();