Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI: Export database as CSV #3278

Merged
merged 1 commit into from Sep 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGELOG.md
Expand Up @@ -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]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be #3278 ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're right 😱

- 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)
Expand Down
2 changes: 1 addition & 1 deletion src/cli/CMakeLists.txt
Expand Up @@ -23,7 +23,7 @@ set(cli_SOURCES
Diceware.cpp
Edit.cpp
Estimate.cpp
Extract.cpp
Export.cpp
Generate.cpp
List.cpp
Locate.cpp
Expand Down
4 changes: 2 additions & 2 deletions src/cli/Command.cpp
Expand Up @@ -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"
Expand Down Expand Up @@ -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());
Expand Down
65 changes: 65 additions & 0 deletions src/cli/Export.cpp
@@ -0,0 +1,65 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 <cstdlib>
#include <stdio.h>

#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> database, QSharedPointer<QCommandLineParser> 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;
}
12 changes: 7 additions & 5 deletions src/cli/Extract.h → src/cli/Export.h
Expand Up @@ -15,17 +15,19 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#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<Database> db, QSharedPointer<QCommandLineParser> parser) override;

static const QCommandLineOption FormatOption;
};

#endif // KEEPASSXC_EXTRACT_H
#endif // KEEPASSXC_EXPORT_H
46 changes: 0 additions & 46 deletions src/cli/Extract.cpp

This file was deleted.

10 changes: 8 additions & 2 deletions src/cli/keepassxc-cli.1
Expand Up @@ -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] <database>"
Extracts and prints the contents of a database to standard output in XML format.
.IP "export [options] <database>"
Exports the content of a database to standard output in the specified format (defaults to XML).

.IP "generate [options]"
Generate a random password.
Expand Down Expand Up @@ -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"
Expand Down
48 changes: 28 additions & 20 deletions src/format/CsvExporter.cpp
Expand Up @@ -35,30 +35,44 @@ bool CsvExporter::exportDatabase(const QString& filename, const QSharedPointer<c

bool CsvExporter::exportDatabase(QIODevice* device, const QSharedPointer<const Database>& 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<const Database>& db)
droidmonkey marked this conversation as resolved.
Show resolved Hide resolved
{
return exportHeader() + exportGroup(db->rootGroup());
}

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("/");
}
Expand All @@ -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<Group*>& 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)
Expand Down
4 changes: 3 additions & 1 deletion src/format/CsvExporter.h
Expand Up @@ -31,10 +31,12 @@ class CsvExporter
public:
bool exportDatabase(const QString& filename, const QSharedPointer<const Database>& db);
bool exportDatabase(QIODevice* device, const QSharedPointer<const Database>& db);
QString exportDatabase(const QSharedPointer<const Database>& 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;
Expand Down
45 changes: 37 additions & 8 deletions tests/TestCli.cpp
Expand Up @@ -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"
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -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
Expand All @@ -768,12 +768,41 @@ void TestCli::testExtract()
// Quiet option
QScopedPointer<Database> 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()
Expand Down
2 changes: 1 addition & 1 deletion tests/TestCli.h
Expand Up @@ -52,7 +52,7 @@ private slots:
void testEdit();
void testEstimate_data();
void testEstimate();
void testExtract();
void testExport();
void testGenerate_data();
void testGenerate();
void testKeyFileOption();
Expand Down