mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-12-24 23:09:44 -05:00
Feature: HTML export from CLI tool
This commit introduces support for exporting a KeePassXC database in HTML format via the CLI tool. The key changes include: - Refactoring HtmlExporter: - Moved HtmlExporter to the format directory and made its API compatible with CsvExporter. - Since the original HtmlExporter had a direct dependency on the gui/Icons functions and indirect dependencies on the gui/DatabaseIcons class, only the non-GUI parts were moved to format/HtmlExporter. - All icon-related functionality was encapsulated in a new child class, gui/HtmlGuiExporter. - The gui/HtmlGuiExporter retains the original functionality of the HtmlExporter class. - The format/HtmlExporter now generates HTML export without icons. Adding icon support to format/HtmlExporter would require moving icon management logic to the core, which could have broader implications. - CLI integration: - Updated cli/Export to use format/HtmlExporter. - GUI Integration: - Updated gui/export/ExportDialog to use gui/HtmlGuiExporter. - Build System Updates: - Updated CMakeLists.txt to build HtmlExporter as part of core_SOURCES and HtmlGuiExporter as part of gui_SOURCES. - Testing: - Updated TestCli to automatically verify the output of the HTML export. Signed-off-by: AdriandMartin <adriandmartin@protonmail.com>
This commit is contained in:
parent
0cb0373f85
commit
c040dd0371
@ -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
|
||||
|
@ -21,13 +21,14 @@
|
||||
#include "Utils.h"
|
||||
#include "core/Global.h"
|
||||
#include "format/CsvExporter.h"
|
||||
#include "format/HtmlExporter.h"
|
||||
|
||||
#include <QCommandLineParser>
|
||||
|
||||
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> 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;
|
||||
|
@ -17,28 +17,13 @@
|
||||
|
||||
#include "HtmlExporter.h"
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QFile>
|
||||
|
||||
#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("<img src=\"data:image/png;base64,") + a.toBase64() + "\"/>";
|
||||
}
|
||||
|
||||
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<const Database>& 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<const Database>& 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<const Database>& db)
|
||||
{
|
||||
const auto meta = db->metadata();
|
||||
if (!meta) {
|
||||
m_error = "Internal error: metadata is NULL";
|
||||
return false;
|
||||
return "";
|
||||
}
|
||||
|
||||
const auto header = QString("<html>"
|
||||
@ -171,33 +203,23 @@ bool HtmlExporter::exportDatabase(QIODevice* device,
|
||||
+ "</p>"
|
||||
"<p><code>"
|
||||
+ db->filePath().toHtmlEscaped() + "</code></p>");
|
||||
return header;
|
||||
}
|
||||
|
||||
QString HtmlExporter::exportFooter()
|
||||
{
|
||||
const auto footer = QString("</body>"
|
||||
"</html>");
|
||||
|
||||
if (device->write(header.toUtf8()) == -1) {
|
||||
m_error = device->errorString();
|
||||
return false;
|
||||
return footer;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString path, bool sorted, bool ascending)
|
||||
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("<hr><h2>");
|
||||
header.append(PixmapToHTML(Icons::groupIconPixmap(&group, IconSize::Medium)));
|
||||
auto group_icon = this->groupIconToHtml(&group);
|
||||
if (!group_icon.isEmpty()) {
|
||||
header.append(group_icon);
|
||||
header.append(" ");
|
||||
}
|
||||
header.append(path);
|
||||
header.append("</h2>\n");
|
||||
|
||||
@ -224,11 +249,8 @@ bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString pat
|
||||
header.append("</p>");
|
||||
}
|
||||
|
||||
// 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 += "<tr>";
|
||||
table += "<td width=\"1%\">" + PixmapToHTML(Icons::entryIconPixmap(entry, IconSize::Medium)) + "</td>";
|
||||
auto entry_icon = this->entryIconToHtml(entry);
|
||||
if (!entry_icon.isEmpty()) {
|
||||
table += "<td width=\"1%\">" + entry_icon + "</td>";
|
||||
}
|
||||
auto caption = "<caption>" + entry->title().toHtmlEscaped() + "</caption>";
|
||||
|
||||
// ... then the right side with the data fields
|
||||
@ -261,12 +286,9 @@ bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString pat
|
||||
table += "</tr>";
|
||||
}
|
||||
|
||||
// Output the complete table of this group
|
||||
// Append the complete table of this group to the output
|
||||
table.append("</table>\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;
|
||||
}
|
@ -21,6 +21,8 @@
|
||||
#include <QSharedPointer>
|
||||
#include <QString>
|
||||
|
||||
#include "core/Group.h"
|
||||
|
||||
class Database;
|
||||
class Group;
|
||||
class QIODevice;
|
||||
@ -32,18 +34,28 @@ public:
|
||||
const QSharedPointer<const Database>& db,
|
||||
bool sorted = true,
|
||||
bool ascending = true);
|
||||
QString errorString() const;
|
||||
|
||||
private:
|
||||
bool exportDatabase(QIODevice* device,
|
||||
const QSharedPointer<const Database>& db,
|
||||
bool sorted = true,
|
||||
bool ascending = true);
|
||||
bool writeGroup(QIODevice& device,
|
||||
const Group& group,
|
||||
QString exportDatabase(const QSharedPointer<const Database>& 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<const Database>& db);
|
||||
QString exportFooter();
|
||||
|
||||
QString m_error;
|
||||
};
|
46
src/gui/HtmlGuiExporter.cpp
Normal file
46
src/gui/HtmlGuiExporter.cpp
Normal file
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 "HtmlGuiExporter.h"
|
||||
|
||||
#include <QBuffer>
|
||||
|
||||
#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("<img src=\"data:image/png;base64,") + a.toBase64() + "\"/>";
|
||||
}
|
||||
} // 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));
|
||||
}
|
30
src/gui/HtmlGuiExporter.h
Normal file
30
src/gui/HtmlGuiExporter.h
Normal file
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#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
|
@ -19,7 +19,7 @@
|
||||
#include "ui_ExportDialog.h"
|
||||
|
||||
#include "gui/FileDialog.h"
|
||||
#include "gui/HtmlExporter.h"
|
||||
#include "gui/HtmlGuiExporter.h"
|
||||
|
||||
ExportDialog::ExportDialog(QSharedPointer<const Database> 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());
|
||||
|
@ -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("<meta charset=\"UTF-8\"><title></title>")));
|
||||
QByteArray htmlBody = m_stdout->readAll();
|
||||
QVERIFY(htmlBody.contains(QByteArray("<h2>NewDatabase</h2>")));
|
||||
QVERIFY(htmlBody.contains(QByteArray(
|
||||
"<caption>Sample Entry</caption>"
|
||||
"<tr><th>User name</th><td class=\"username\">User Name</td></tr>"
|
||||
"<tr><th>Password</th><td class=\"password\">Password</td></tr>"
|
||||
"<tr><th>URL</th><td class=\"url\"><a href=\"http://www.somesite.com/\">http://www.somesite.com/</a></td></tr>")));
|
||||
// test invalid format
|
||||
setInput("a");
|
||||
execCmd(exportCmd, {"export", "-f", "yaml", m_dbFile->fileName()});
|
||||
|
Loading…
Reference in New Issue
Block a user