diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ee83fac32..a56559933 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -71,6 +71,7 @@ set(core_SOURCES format/BitwardenReader.cpp format/CsvExporter.cpp format/CsvParser.cpp + format/HtmlExporter.cpp format/KeePass1Reader.cpp format/KeePass2.cpp format/KeePass2RandomStream.cpp @@ -127,7 +128,7 @@ set(gui_SOURCES gui/FileDialog.cpp gui/Font.cpp gui/GuiTools.cpp - gui/HtmlExporter.cpp + gui/HtmlGuiExporter.cpp gui/IconModels.cpp gui/KMessageWidget.cpp gui/MainWindow.cpp diff --git a/src/cli/Export.cpp b/src/cli/Export.cpp index a7f454776..36b38b1de 100644 --- a/src/cli/Export.cpp +++ b/src/cli/Export.cpp @@ -21,13 +21,14 @@ #include "Utils.h" #include "core/Global.h" #include "format/CsvExporter.h" +#include "format/HtmlExporter.h" #include const QCommandLineOption Export::FormatOption = QCommandLineOption( QStringList() << "f" << "format", - QObject::tr("Format to use when exporting. Available choices are 'xml' or 'csv'. Defaults to 'xml'."), - QStringLiteral("xml|csv")); + QObject::tr("Format to use when exporting. Available choices are 'xml', 'csv' or 'html'. Defaults to 'xml'."), + QStringLiteral("xml|csv|html")); Export::Export() { @@ -53,6 +54,9 @@ int Export::executeWithDatabase(QSharedPointer database, QSharedPointe } else if (format.startsWith(QStringLiteral("csv"), Qt::CaseInsensitive)) { CsvExporter csvExporter; out << csvExporter.exportDatabase(database); + } else if (format.startsWith(QStringLiteral("html"), Qt::CaseInsensitive)) { + HtmlExporter htmlExporter; + out << htmlExporter.exportDatabase(database); } else { err << QObject::tr("Unsupported format %1").arg(format) << Qt::endl; return EXIT_FAILURE; diff --git a/src/gui/HtmlExporter.cpp b/src/format/HtmlExporter.cpp similarity index 82% rename from src/gui/HtmlExporter.cpp rename to src/format/HtmlExporter.cpp index 0c45259ac..eb57d0377 100644 --- a/src/gui/HtmlExporter.cpp +++ b/src/format/HtmlExporter.cpp @@ -17,28 +17,13 @@ #include "HtmlExporter.h" -#include #include #include "core/Group.h" #include "core/Metadata.h" -#include "gui/Icons.h" namespace { - QString PixmapToHTML(const QPixmap& pixmap) - { - if (pixmap.isNull()) { - return ""; - } - - // Based on https://stackoverflow.com/a/6621278 - QByteArray a; - QBuffer buffer(&a); - pixmap.save(&buffer, "PNG"); - return QString(""; - } - QString formatEntry(const Entry& entry) { // Here we collect the table rows with this entry's data fields @@ -127,15 +112,62 @@ QString HtmlExporter::errorString() const return m_error; } +QString HtmlExporter::groupIconToHtml(const Group* /* group */) { + return ""; +} + +QString HtmlExporter::entryIconToHtml(const Entry* /* entry */) { + return ""; +} + bool HtmlExporter::exportDatabase(QIODevice* device, const QSharedPointer& db, bool sorted, bool ascending) +{ + if (device->write(exportHeader(db).toUtf8()) == -1) { + m_error = device->errorString(); + return false; + } + + if (db->rootGroup()) { + if (device->write(exportGroup(*db->rootGroup(), QString(), sorted, ascending).toUtf8()) == -1) { + m_error = device->errorString(); + return false; + } + } + + if (device->write(exportFooter().toUtf8()) == -1) { + m_error = device->errorString(); + return false; + } + + return true; +} + +QString HtmlExporter::exportDatabase(const QSharedPointer& db, + bool sorted, + bool ascending) +{ + QString response; + + response = exportHeader(db); + if (!response.isEmpty()) { + if (db->rootGroup()) { + response.append(exportGroup(*db->rootGroup(), QString(), sorted, ascending)); + } + response.append(exportFooter()); + } + + return response; +} + +QString HtmlExporter::exportHeader(const QSharedPointer& db) { const auto meta = db->metadata(); if (!meta) { m_error = "Internal error: metadata is NULL"; - return false; + return ""; } const auto header = QString("" @@ -171,33 +203,23 @@ bool HtmlExporter::exportDatabase(QIODevice* device, + "

" "

" + db->filePath().toHtmlEscaped() + "

"); - const auto footer = QString("" - ""); - - if (device->write(header.toUtf8()) == -1) { - m_error = device->errorString(); - return false; - } - - if (db->rootGroup()) { - if (!writeGroup(*device, *db->rootGroup(), QString(), sorted, ascending)) { - return false; - } - } - - if (device->write(footer.toUtf8()) == -1) { - m_error = device->errorString(); - return false; - } - - return true; + return header; } -bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString path, bool sorted, bool ascending) +QString HtmlExporter::exportFooter() { + const auto footer = QString("" + ""); + return footer; +} + +QString HtmlExporter::exportGroup(const Group& group, QString path, bool sorted, bool ascending) +{ + QString response = ""; + // Don't output the recycle bin if (&group == group.database()->metadata()->recycleBin()) { - return true; + return response; } if (!path.isEmpty()) { @@ -212,8 +234,11 @@ bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString pat if (!group.entries().empty() || !notes.isEmpty()) { // Header line auto header = QString("

"); - header.append(PixmapToHTML(Icons::groupIconPixmap(&group, IconSize::Medium))); - header.append(" "); + auto group_icon = this->groupIconToHtml(&group); + if (!group_icon.isEmpty()) { + header.append(group_icon); + header.append(" "); + } header.append(path); header.append("

\n"); @@ -224,11 +249,8 @@ bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString pat header.append("

"); } - // Output it - if (device.write(header.toUtf8()) == -1) { - m_error = device.errorString(); - return false; - } + // Append it to the output + response.append(header); } // Begin the table for the entries in this group @@ -252,7 +274,10 @@ bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString pat // Output it into our table. First the left side with // icon and entry title ... table += ""; - table += "" + PixmapToHTML(Icons::entryIconPixmap(entry, IconSize::Medium)) + ""; + auto entry_icon = this->entryIconToHtml(entry); + if (!entry_icon.isEmpty()) { + table += "" + entry_icon + ""; + } auto caption = "" + entry->title().toHtmlEscaped() + ""; // ... then the right side with the data fields @@ -261,12 +286,9 @@ bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString pat table += ""; } - // Output the complete table of this group + // Append the complete table of this group to the output table.append("\n"); - if (device.write(table.toUtf8()) == -1) { - m_error = device.errorString(); - return false; - } + response.append(table); auto children = group.children(); if (sorted) { @@ -278,10 +300,10 @@ bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString pat // Recursively output the child groups for (const auto* child : children) { - if (child && !writeGroup(device, *child, path, sorted, ascending)) { - return false; + if (child) { + response.append(exportGroup(*child, path, sorted, ascending)); } } - return true; + return response; } diff --git a/src/gui/HtmlExporter.h b/src/format/HtmlExporter.h similarity index 68% rename from src/gui/HtmlExporter.h rename to src/format/HtmlExporter.h index 1ee9b4448..1774b059c 100644 --- a/src/gui/HtmlExporter.h +++ b/src/format/HtmlExporter.h @@ -21,6 +21,8 @@ #include #include +#include "core/Group.h" + class Database; class Group; class QIODevice; @@ -32,18 +34,28 @@ public: const QSharedPointer& db, bool sorted = true, bool ascending = true); - QString errorString() const; - -private: bool exportDatabase(QIODevice* device, const QSharedPointer& db, bool sorted = true, bool ascending = true); - bool writeGroup(QIODevice& device, - const Group& group, - QString path = QString(), - bool sorted = true, - bool ascending = true); + QString exportDatabase(const QSharedPointer& db, + bool sorted = true, + bool ascending = true); + QString errorString() const; + + virtual ~HtmlExporter() = default; + +protected: + virtual QString groupIconToHtml(const Group* group); + virtual QString entryIconToHtml(const Entry* entry); + +private: + QString exportGroup(const Group& group, + QString path = QString(), + bool sorted = true, + bool ascending = true); + QString exportHeader(const QSharedPointer& db); + QString exportFooter(); QString m_error; }; diff --git a/src/gui/HtmlGuiExporter.cpp b/src/gui/HtmlGuiExporter.cpp new file mode 100644 index 000000000..34343eef6 --- /dev/null +++ b/src/gui/HtmlGuiExporter.cpp @@ -0,0 +1,46 @@ +/* + * 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 "HtmlGuiExporter.h" + +#include + +#include "gui/Icons.h" + +namespace +{ + QString PixmapToHTML(const QPixmap& pixmap) + { + if (pixmap.isNull()) { + return ""; + } + + // Based on https://stackoverflow.com/a/6621278 + QByteArray a; + QBuffer buffer(&a); + pixmap.save(&buffer, "PNG"); + return QString(""; + } +} // namespace + +QString HtmlGuiExporter::groupIconToHtml(const Group* group) { + return PixmapToHTML(Icons::groupIconPixmap(group, IconSize::Medium)); +} + +QString HtmlGuiExporter::entryIconToHtml(const Entry* entry) { + return PixmapToHTML(Icons::entryIconPixmap(entry, IconSize::Medium)); +} diff --git a/src/gui/HtmlGuiExporter.h b/src/gui/HtmlGuiExporter.h new file mode 100644 index 000000000..9979996a1 --- /dev/null +++ b/src/gui/HtmlGuiExporter.h @@ -0,0 +1,30 @@ +/* + * 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 . + */ + +#ifndef KEEPASSX_HTMLGUIEXPORTER_H +#define KEEPASSX_HTMLGUIEXPORTER_H + +#include "format/HtmlExporter.h" + +class HtmlGuiExporter : public HtmlExporter +{ +protected: + QString groupIconToHtml(const Group* group) override; + QString entryIconToHtml(const Entry* entry) override; +}; + +#endif // KEEPASSX_HTMLGUIEXPORTER_H diff --git a/src/gui/export/ExportDialog.cpp b/src/gui/export/ExportDialog.cpp index 6985546fa..4c2c2e6b5 100644 --- a/src/gui/export/ExportDialog.cpp +++ b/src/gui/export/ExportDialog.cpp @@ -19,7 +19,7 @@ #include "ui_ExportDialog.h" #include "gui/FileDialog.h" -#include "gui/HtmlExporter.h" +#include "gui/HtmlGuiExporter.h" ExportDialog::ExportDialog(QSharedPointer db, DatabaseTabWidget* parent) : QDialog(parent) @@ -72,7 +72,7 @@ void ExportDialog::exportDatabase() FileDialog::saveLastDir("html", fileName, true); - HtmlExporter htmlExporter; + HtmlGuiExporter htmlExporter; if (!htmlExporter.exportDatabase( fileName, m_db, sortBy != ExportSortingStrategy::BY_DATABASE_ORDER, ascendingOrder)) { emit exportFailed(htmlExporter.errorString()); diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index 7200d21fa..c1e628fbf 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -1311,6 +1311,18 @@ void TestCli::testExport() QVERIFY(csvData.contains(QByteArray( "\"NewDatabase\",\"Sample Entry\",\"User Name\",\"Password\",\"http://www.somesite.com/\",\"Notes\""))); + // HTML exporting + setInput("a"); + execCmd(exportCmd, {"export", "-f", "html", m_dbFile->fileName()}); + QByteArray htmlHeader = m_stdout->readLine(); + QVERIFY(htmlHeader.contains(QByteArray(""))); + QByteArray htmlBody = m_stdout->readAll(); + QVERIFY(htmlBody.contains(QByteArray("

NewDatabase

"))); + QVERIFY(htmlBody.contains(QByteArray( + "Sample Entry" + "User nameUser Name" + "PasswordPassword" + "URLhttp://www.somesite.com/"))); // test invalid format setInput("a"); execCmd(exportCmd, {"export", "-f", "yaml", m_dbFile->fileName()});