execNewDatabaseWizard();
diff --git a/src/gui/HtmlExporter.cpp b/src/gui/HtmlExporter.cpp
index c7968163..70249ed2 100644
--- a/src/gui/HtmlExporter.cpp
+++ b/src/gui/HtmlExporter.cpp
@@ -39,82 +39,87 @@ namespace
return QString("
";
}
- QString formatHTML(const QString& value)
- {
- return value.toHtmlEscaped().replace(" ", " ").replace('\n', "
");
- }
-
- QString formatAttribute(const QString& key,
- const QString& value,
- const QString& classname,
- const QString& templt = QString("| %1 | %3 |
"))
- {
- const auto& formatted_attribute = templt;
- if (!value.isEmpty()) {
- // Format key as well -> Translations into other languages may have non-standard chars
- return formatted_attribute.arg(formatHTML(key), classname, formatHTML(value));
- }
- return {};
- }
-
- QString formatAttribute(const Entry& entry,
- const QString& key,
- const QString& value,
- const QString& classname,
- const QString& templt = QString("| %1 | %3 |
"))
- {
- if (value.isEmpty())
- return {};
- return formatAttribute(key, entry.resolveMultiplePlaceholders(value), classname, templt);
- }
-
QString formatEntry(const Entry& entry)
{
// Here we collect the table rows with this entry's data fields
QString item;
// Output the fixed fields
- item.append(formatAttribute(entry, QObject::tr("User name"), entry.username(), "username"));
-
- item.append(formatAttribute(entry, QObject::tr("Password"), entry.password(), "password"));
-
- if (!entry.url().isEmpty()) {
- constexpr auto maxlen = 100;
- QString displayedURL(formatHTML(entry.url()).mid(0, maxlen));
-
- if (displayedURL.size() == maxlen) {
- displayedURL.append("…");
- }
-
- item.append(formatAttribute(entry,
- QObject::tr("URL"),
- entry.url(),
- "url",
- R"(| %1 | %4 |
)")
- .arg(entry.resolveMultiplePlaceholders(displayedURL)));
+ const auto& u = entry.username();
+ if (!u.isEmpty()) {
+ item.append("| ");
+ item.append(QObject::tr("User name"));
+ item.append(" | ");
+ item.append(entry.username().toHtmlEscaped());
+ item.append(" |
");
}
- item.append(formatAttribute(entry, QObject::tr("Notes"), entry.notes(), "notes"));
+ const auto& p = entry.password();
+ if (!p.isEmpty()) {
+ item.append("| ");
+ item.append(QObject::tr("Password"));
+ item.append(" | ");
+ item.append(entry.password().toHtmlEscaped());
+ item.append(" |
");
+ }
+
+ const auto& r = entry.url();
+ if (!r.isEmpty()) {
+ item.append("| ");
+ item.append(QObject::tr("URL"));
+ item.append(" | ");
+
+ // Restrict the length of what we display of the URL -
+ // even from a paper backup, nobody will every type in
+ // more than 100 characters of a URL
+ constexpr auto maxlen = 100;
+ if (r.size() <= maxlen) {
+ item.append(r.toHtmlEscaped());
+ } else {
+ item.append(r.mid(0, maxlen).toHtmlEscaped());
+ item.append("…");
+ }
+
+ item.append(" |
");
+ }
+
+ const auto& n = entry.notes();
+ if (!n.isEmpty()) {
+ item.append("| ");
+ item.append(QObject::tr("Notes"));
+ item.append(" | ");
+ item.append(entry.notes().toHtmlEscaped().replace("\n", " "));
+ item.append(" |
");
+ }
// Now add the attributes (if there are any)
const auto* const attr = entry.attributes();
if (attr && !attr->customKeys().isEmpty()) {
for (const auto& key : attr->customKeys()) {
- item.append(formatAttribute(entry, key, attr->value(key), "attr"));
+ item.append("| ");
+ item.append(key.toHtmlEscaped());
+ item.append(" | ");
+ item.append(attr->value(key).toHtmlEscaped().replace(" ", " ").replace("\n", " "));
+ item.append(" |
");
}
}
return item;
}
} // namespace
-bool HtmlExporter::exportDatabase(const QString& filename, const QSharedPointer& db)
+bool HtmlExporter::exportDatabase(const QString& filename,
+ const QSharedPointer& db,
+ bool sorted,
+ bool ascending)
{
QFile file(filename);
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
m_error = file.errorString();
return false;
}
- return exportDatabase(&file, db);
+ return exportDatabase(&file, db, sorted, ascending);
}
QString HtmlExporter::errorString() const
@@ -122,7 +127,10 @@ QString HtmlExporter::errorString() const
return m_error;
}
-bool HtmlExporter::exportDatabase(QIODevice* device, const QSharedPointer& db)
+bool HtmlExporter::exportDatabase(QIODevice* device,
+ const QSharedPointer& db,
+ bool sorted,
+ bool ascending)
{
const auto meta = db->metadata();
if (!meta) {
@@ -171,7 +179,7 @@ bool HtmlExporter::exportDatabase(QIODevice* device, const QSharedPointerrootGroup()) {
- if (!writeGroup(*device, *db->rootGroup())) {
+ if (!writeGroup(*device, *db->rootGroup(), QString(), sorted, ascending)) {
return false;
}
}
@@ -184,7 +192,7 @@ bool HtmlExporter::exportDatabase(QIODevice* device, const QSharedPointermetadata()->recycleBin()) {
@@ -199,10 +207,8 @@ bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString pat
// Output the header for this group (but only if there are
// any notes or entries in this group, otherwise we'd get
// a header with nothing after it, which looks stupid)
- const auto& entries = group.entries();
const auto notes = group.notes();
- if (!entries.empty() || !notes.isEmpty()) {
-
+ if (!group.entries().empty() || !notes.isEmpty()) {
// Header line
auto header = QString("
");
header.append(PixmapToHTML(Icons::groupIconPixmap(&group, IconSize::Medium)));
@@ -227,8 +233,16 @@ bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString pat
// Begin the table for the entries in this group
auto table = QString("
");
+ auto entries = group.entries();
+ if (sorted) {
+ std::sort(entries.begin(), entries.end(), [&](Entry* lhs, Entry* rhs) {
+ int cmp = lhs->title().compare(rhs->title(), Qt::CaseInsensitive);
+ return ascending ? cmp < 0 : cmp > 0;
+ });
+ }
+
// Output the entries in this group
- for (const auto entry : entries) {
+ for (const auto* entry : entries) {
auto formatted_entry = formatEntry(*entry);
if (formatted_entry.isEmpty())
@@ -252,10 +266,17 @@ bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString pat
return false;
}
+ auto children = group.children();
+ if (sorted) {
+ std::sort(children.begin(), children.end(), [&](Group* lhs, Group* rhs) {
+ int cmp = lhs->name().compare(rhs->name(), Qt::CaseInsensitive);
+ return ascending ? cmp < 0 : cmp > 0;
+ });
+ }
+
// Recursively output the child groups
- const auto& children = group.children();
- for (const auto child : children) {
- if (child && !writeGroup(device, *child, path)) {
+ for (const auto* child : children) {
+ if (child && !writeGroup(device, *child, path, sorted, ascending)) {
return false;
}
}
diff --git a/src/gui/HtmlExporter.h b/src/gui/HtmlExporter.h
index 3a592e54..1ee9b444 100644
--- a/src/gui/HtmlExporter.h
+++ b/src/gui/HtmlExporter.h
@@ -28,12 +28,22 @@ class QIODevice;
class HtmlExporter
{
public:
- bool exportDatabase(const QString& filename, const QSharedPointer& db);
+ bool exportDatabase(const QString& filename,
+ const QSharedPointer& db,
+ bool sorted = true,
+ bool ascending = true);
QString errorString() const;
private:
- bool exportDatabase(QIODevice* device, const QSharedPointer& db);
- bool writeGroup(QIODevice& device, const Group& group, QString path = QString());
+ 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 m_error;
};
diff --git a/src/gui/export/ExportDialog.cpp b/src/gui/export/ExportDialog.cpp
new file mode 100644
index 00000000..3537505c
--- /dev/null
+++ b/src/gui/export/ExportDialog.cpp
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2021 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 "ExportDialog.h"
+#include "ui_ExportDialog.h"
+
+#include "gui/FileDialog.h"
+#include "gui/HtmlExporter.h"
+
+ExportDialog::ExportDialog(QSharedPointer db, DatabaseTabWidget* parent)
+ : QDialog(parent)
+ , m_ui(new Ui::ExportDialog())
+ , m_db(std::move(db))
+{
+ m_ui->setupUi(this);
+
+ setAttribute(Qt::WA_DeleteOnClose);
+
+ connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(close()));
+ connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(exportDatabase()));
+
+ m_ui->sortingStrategy->addItem(getStrategyName(BY_NAME_ASC), BY_NAME_ASC);
+ m_ui->sortingStrategy->addItem(getStrategyName(BY_NAME_DESC), BY_NAME_DESC);
+ m_ui->sortingStrategy->addItem(getStrategyName(BY_DATABASE_ORDER), BY_DATABASE_ORDER);
+
+ m_ui->messageWidget->setCloseButtonVisible(false);
+ m_ui->messageWidget->setAutoHideTimeout(-1);
+ m_ui->messageWidget->showMessage(tr("You are about to export your database to an unencrypted file.\n"
+ "This will leave your passwords and sensitive information vulnerable!\n"),
+ MessageWidget::Warning);
+}
+
+ExportDialog::~ExportDialog()
+{
+}
+
+QString ExportDialog::getStrategyName(ExportSortingStrategy strategy)
+{
+ switch (strategy) {
+ case ExportSortingStrategy::BY_DATABASE_ORDER:
+ return tr("database order");
+ case ExportSortingStrategy::BY_NAME_ASC:
+ return tr("name (ascending)");
+ case ExportSortingStrategy::BY_NAME_DESC:
+ return tr("name (descending)");
+ }
+ return tr("unknown");
+}
+
+void ExportDialog::exportDatabase()
+{
+ auto sortBy = m_ui->sortingStrategy->currentData().toInt();
+ bool ascendingOrder = sortBy == ExportSortingStrategy::BY_NAME_ASC;
+
+ const QString fileName = fileDialog()->getSaveFileName(
+ this, tr("Export database to HTML file"), FileDialog::getLastDir("html"), tr("HTML file").append(" (*.html)"));
+ if (fileName.isEmpty()) {
+ return;
+ }
+
+ FileDialog::saveLastDir("html", fileName, true);
+
+ HtmlExporter htmlExporter;
+ if (!htmlExporter.exportDatabase(
+ fileName, m_db, sortBy != ExportSortingStrategy::BY_DATABASE_ORDER, ascendingOrder)) {
+ emit exportFailed(htmlExporter.errorString());
+ reject();
+ }
+
+ accept();
+}
diff --git a/src/gui/export/ExportDialog.h b/src/gui/export/ExportDialog.h
new file mode 100644
index 00000000..7e598686
--- /dev/null
+++ b/src/gui/export/ExportDialog.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2021 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 KEEPASSXC_EXPORTDIALOG_H
+#define KEEPASSXC_EXPORTDIALOG_H
+
+#include "core/Database.h"
+#include "gui/DatabaseTabWidget.h"
+#include
+
+namespace Ui
+{
+ class ExportDialog;
+}
+
+class ExportDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit ExportDialog(QSharedPointer db, DatabaseTabWidget* parent = nullptr);
+ ~ExportDialog() override;
+
+ enum ExportSortingStrategy
+ {
+ BY_DATABASE_ORDER = 0,
+ BY_NAME_ASC = 1,
+ BY_NAME_DESC = 2
+ };
+
+signals:
+ void exportFailed(QString reason);
+
+private slots:
+ void exportDatabase();
+
+private:
+ QString getStrategyName(ExportSortingStrategy strategy);
+
+ QScopedPointer m_ui;
+ QSharedPointer m_db;
+};
+
+#endif // KEEPASSXC_EXPORTDIALOG_H
diff --git a/src/gui/export/ExportDialog.ui b/src/gui/export/ExportDialog.ui
new file mode 100644
index 00000000..16500b13
--- /dev/null
+++ b/src/gui/export/ExportDialog.ui
@@ -0,0 +1,79 @@
+
+
+ ExportDialog
+
+
+
+ 0
+ 0
+ 186
+ 164
+
+
+
+ Export options
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+ Sort entries by...
+
+
+ sortingStrategy
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+ MessageWidget
+ QWidget
+
+ 1
+
+
+
+
+