Skip to content

Commit ab6b6f3

Browse files
Feature: HTML export from CLI tool (#11590)
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 <[email protected]>
1 parent 5a3289e commit ab6b6f3

File tree

9 files changed

+195
-71
lines changed

9 files changed

+195
-71
lines changed

share/translations/keepassxc_en.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8080,10 +8080,6 @@ Do you want to overwrite it?</source>
80808080
<source>Exit interactive mode.</source>
80818081
<translation type="unfinished"></translation>
80828082
</message>
8083-
<message>
8084-
<source>Format to use when exporting. Available choices are &apos;xml&apos; or &apos;csv&apos;. Defaults to &apos;xml&apos;.</source>
8085-
<translation type="unfinished"></translation>
8086-
</message>
80878083
<message>
80888084
<source>Exports the content of a database to standard output in the specified format.</source>
80898085
<translation type="unfinished"></translation>
@@ -9228,6 +9224,10 @@ This option is deprecated, use --set-key-file instead.</source>
92289224
<source>Passkey</source>
92299225
<translation type="unfinished"></translation>
92309226
</message>
9227+
<message>
9228+
<source>Format to use when exporting. Available choices are &apos;xml&apos;, &apos;csv&apos; or &apos;html&apos;. Defaults to &apos;xml&apos;.</source>
9229+
<translation type="unfinished"></translation>
9230+
</message>
92319231
<message>
92329232
<source>start minimized to the system tray</source>
92339233
<translation type="unfinished"></translation>

src/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ set(core_SOURCES
7171
format/BitwardenReader.cpp
7272
format/CsvExporter.cpp
7373
format/CsvParser.cpp
74+
format/HtmlExporter.cpp
7475
format/KeePass1Reader.cpp
7576
format/KeePass2.cpp
7677
format/KeePass2RandomStream.cpp
@@ -129,7 +130,7 @@ set(gui_SOURCES
129130
gui/FileDialog.cpp
130131
gui/Font.cpp
131132
gui/GuiTools.cpp
132-
gui/HtmlExporter.cpp
133+
gui/HtmlGuiExporter.cpp
133134
gui/IconModels.cpp
134135
gui/KMessageWidget.cpp
135136
gui/MainWindow.cpp

src/cli/Export.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@
2121
#include "Utils.h"
2222
#include "core/Global.h"
2323
#include "format/CsvExporter.h"
24+
#include "format/HtmlExporter.h"
2425

2526
#include <QCommandLineParser>
2627

2728
const QCommandLineOption Export::FormatOption = QCommandLineOption(
2829
QStringList() << "f" << "format",
29-
QObject::tr("Format to use when exporting. Available choices are 'xml' or 'csv'. Defaults to 'xml'."),
30-
QStringLiteral("xml|csv"));
30+
QObject::tr("Format to use when exporting. Available choices are 'xml', 'csv' or 'html'. Defaults to 'xml'."),
31+
QStringLiteral("xml|csv|html"));
3132

3233
Export::Export()
3334
{
@@ -53,6 +54,9 @@ int Export::executeWithDatabase(QSharedPointer<Database> database, QSharedPointe
5354
} else if (format.startsWith(QStringLiteral("csv"), Qt::CaseInsensitive)) {
5455
CsvExporter csvExporter;
5556
out << csvExporter.exportDatabase(database);
57+
} else if (format.startsWith(QStringLiteral("html"), Qt::CaseInsensitive)) {
58+
HtmlExporter htmlExporter;
59+
out << htmlExporter.exportDatabase(database);
5660
} else {
5761
err << QObject::tr("Unsupported format %1").arg(format) << Qt::endl;
5862
return EXIT_FAILURE;

src/gui/HtmlExporter.cpp renamed to src/format/HtmlExporter.cpp

Lines changed: 76 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,13 @@
1717

1818
#include "HtmlExporter.h"
1919

20-
#include <QBuffer>
2120
#include <QFile>
2221

2322
#include "core/Group.h"
2423
#include "core/Metadata.h"
25-
#include "gui/Icons.h"
2624

2725
namespace
2826
{
29-
QString PixmapToHTML(const QPixmap& pixmap)
30-
{
31-
if (pixmap.isNull()) {
32-
return "";
33-
}
34-
35-
// Based on https://stackoverflow.com/a/6621278
36-
QByteArray a;
37-
QBuffer buffer(&a);
38-
pixmap.save(&buffer, "PNG");
39-
return QString("<img src=\"data:image/png;base64,") + a.toBase64() + "\"/>";
40-
}
41-
4227
QString formatEntry(const Entry& entry)
4328
{
4429
// Here we collect the table rows with this entry's data fields
@@ -127,15 +112,62 @@ QString HtmlExporter::errorString() const
127112
return m_error;
128113
}
129114

115+
QString HtmlExporter::groupIconToHtml(const Group* /* group */)
116+
{
117+
return "";
118+
}
119+
120+
QString HtmlExporter::entryIconToHtml(const Entry* /* entry */)
121+
{
122+
return "";
123+
}
124+
130125
bool HtmlExporter::exportDatabase(QIODevice* device,
131126
const QSharedPointer<const Database>& db,
132127
bool sorted,
133128
bool ascending)
129+
{
130+
if (device->write(exportHeader(db).toUtf8()) == -1) {
131+
m_error = device->errorString();
132+
return false;
133+
}
134+
135+
if (db->rootGroup()) {
136+
if (device->write(exportGroup(*db->rootGroup(), QString(), sorted, ascending).toUtf8()) == -1) {
137+
m_error = device->errorString();
138+
return false;
139+
}
140+
}
141+
142+
if (device->write(exportFooter().toUtf8()) == -1) {
143+
m_error = device->errorString();
144+
return false;
145+
}
146+
147+
return true;
148+
}
149+
150+
QString HtmlExporter::exportDatabase(const QSharedPointer<const Database>& db, bool sorted, bool ascending)
151+
{
152+
QString response;
153+
154+
response = exportHeader(db);
155+
if (!response.isEmpty()) {
156+
if (db->rootGroup()) {
157+
response.append(exportGroup(*db->rootGroup(), QString(), sorted, ascending));
158+
}
159+
response.append(exportFooter());
160+
}
161+
162+
return response;
163+
}
164+
165+
QString HtmlExporter::exportHeader(const QSharedPointer<const Database>& db)
134166
{
135167
const auto meta = db->metadata();
136168
if (!meta) {
137169
m_error = "Internal error: metadata is NULL";
138-
return false;
170+
return "";
139171
}
140172

141173
const auto header = QString("<html>"
@@ -171,33 +203,23 @@ bool HtmlExporter::exportDatabase(QIODevice* device,
171203
+ "</p>"
172204
"<p><code>"
173205
+ db->filePath().toHtmlEscaped() + "</code></p>");
206+
return header;
207+
}
208+
209+
QString HtmlExporter::exportFooter()
210+
{
174211
const auto footer = QString("</body>"
175212
"</html>");
176-
177-
if (device->write(header.toUtf8()) == -1) {
178-
m_error = device->errorString();
179-
return false;
180-
}
181-
182-
if (db->rootGroup()) {
183-
if (!writeGroup(*device, *db->rootGroup(), QString(), sorted, ascending)) {
184-
return false;
185-
}
186-
}
187-
188-
if (device->write(footer.toUtf8()) == -1) {
189-
m_error = device->errorString();
190-
return false;
191-
}
192-
193-
return true;
213+
return footer;
194214
}
195215

196-
bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString path, bool sorted, bool ascending)
216+
QString HtmlExporter::exportGroup(const Group& group, QString path, bool sorted, bool ascending)
197217
{
218+
QString response = "";
219+
198220
// Don't output the recycle bin
199221
if (&group == group.database()->metadata()->recycleBin()) {
200-
return true;
222+
return response;
201223
}
202224

203225
if (!path.isEmpty()) {
@@ -212,8 +234,11 @@ bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString pat
212234
if (!group.entries().empty() || !notes.isEmpty()) {
213235
// Header line
214236
auto header = QString("<hr><h2>");
215-
header.append(PixmapToHTML(Icons::groupIconPixmap(&group, IconSize::Medium)));
216-
header.append("&nbsp;");
237+
auto groupIcon = this->groupIconToHtml(&group);
238+
if (!groupIcon.isEmpty()) {
239+
header.append(groupIcon);
240+
header.append("&nbsp;");
241+
}
217242
header.append(path);
218243
header.append("</h2>\n");
219244

@@ -224,11 +249,8 @@ bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString pat
224249
header.append("</p>");
225250
}
226251

227-
// Output it
228-
if (device.write(header.toUtf8()) == -1) {
229-
m_error = device.errorString();
230-
return false;
231-
}
252+
// Append it to the output
253+
response.append(header);
232254
}
233255

234256
// Begin the table for the entries in this group
@@ -242,7 +264,7 @@ bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString pat
242264
});
243265
}
244266

245-
// Output the entries in this group
267+
// Append to the output the entries in this group
246268
for (const auto* entry : entries) {
247269
auto formatted_entry = formatEntry(*entry);
248270

@@ -252,7 +274,10 @@ bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString pat
252274
// Output it into our table. First the left side with
253275
// icon and entry title ...
254276
table += "<tr>";
255-
table += "<td width=\"1%\">" + PixmapToHTML(Icons::entryIconPixmap(entry, IconSize::Medium)) + "</td>";
277+
auto entryIcon = this->entryIconToHtml(entry);
278+
if (!entryIcon.isEmpty()) {
279+
table += "<td width=\"1%\">" + entryIcon + "</td>";
280+
}
256281
auto caption = "<caption>" + entry->title().toHtmlEscaped() + "</caption>";
257282

258283
// ... then the right side with the data fields
@@ -261,12 +286,9 @@ bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString pat
261286
table += "</tr>";
262287
}
263288

264-
// Output the complete table of this group
289+
// Append the complete table of this group to the output
265290
table.append("</table>\n");
266-
if (device.write(table.toUtf8()) == -1) {
267-
m_error = device.errorString();
268-
return false;
269-
}
291+
response.append(table);
270292

271293
auto children = group.children();
272294
if (sorted) {
@@ -276,12 +298,12 @@ bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString pat
276298
});
277299
}
278300

279-
// Recursively output the child groups
301+
// Recursively append to the output the child groups
280302
for (const auto* child : children) {
281-
if (child && !writeGroup(device, *child, path, sorted, ascending)) {
282-
return false;
303+
if (child) {
304+
response.append(exportGroup(*child, path, sorted, ascending));
283305
}
284306
}
285307

286-
return true;
308+
return response;
287309
}

src/gui/HtmlExporter.h renamed to src/format/HtmlExporter.h

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
#include <QSharedPointer>
2222
#include <QString>
2323

24+
#include "core/Group.h"
25+
2426
class Database;
2527
class Group;
2628
class QIODevice;
@@ -32,18 +34,23 @@ class HtmlExporter
3234
const QSharedPointer<const Database>& db,
3335
bool sorted = true,
3436
bool ascending = true);
35-
QString errorString() const;
36-
37-
private:
3837
bool exportDatabase(QIODevice* device,
3938
const QSharedPointer<const Database>& db,
4039
bool sorted = true,
4140
bool ascending = true);
42-
bool writeGroup(QIODevice& device,
43-
const Group& group,
44-
QString path = QString(),
45-
bool sorted = true,
46-
bool ascending = true);
41+
QString exportDatabase(const QSharedPointer<const Database>& db, bool sorted = true, bool ascending = true);
42+
QString errorString() const;
43+
44+
virtual ~HtmlExporter() = default;
45+
46+
protected:
47+
virtual QString groupIconToHtml(const Group* group);
48+
virtual QString entryIconToHtml(const Entry* entry);
49+
50+
private:
51+
QString exportGroup(const Group& group, QString path = QString(), bool sorted = true, bool ascending = true);
52+
QString exportHeader(const QSharedPointer<const Database>& db);
53+
QString exportFooter();
4754

4855
QString m_error;
4956
};

src/gui/HtmlGuiExporter.cpp

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright (C) 2019 KeePassXC Team <[email protected]>
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 2 or (at your option)
7+
* version 3 of the License.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
#include "HtmlGuiExporter.h"
19+
20+
#include <QBuffer>
21+
22+
#include "gui/Icons.h"
23+
24+
namespace
25+
{
26+
QString PixmapToHTML(const QPixmap& pixmap)
27+
{
28+
if (pixmap.isNull()) {
29+
return "";
30+
}
31+
32+
// Based on https://stackoverflow.com/a/6621278
33+
QByteArray a;
34+
QBuffer buffer(&a);
35+
pixmap.save(&buffer, "PNG");
36+
return QString("<img src=\"data:image/png;base64,") + a.toBase64() + "\"/>";
37+
}
38+
} // namespace
39+
40+
QString HtmlGuiExporter::groupIconToHtml(const Group* group)
41+
{
42+
return PixmapToHTML(Icons::groupIconPixmap(group, IconSize::Medium));
43+
}
44+
45+
QString HtmlGuiExporter::entryIconToHtml(const Entry* entry)
46+
{
47+
return PixmapToHTML(Icons::entryIconPixmap(entry, IconSize::Medium));
48+
}

0 commit comments

Comments
 (0)