Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/keepassxreboot/keepassxc.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/src/gui
diff options
context:
space:
mode:
authorPatrick Sean Klein <patrick@libklein.com>2021-10-10 17:36:19 +0300
committerJonathan White <support@dmapps.us>2021-11-24 08:12:13 +0300
commit296cbf0df77020336060795a30fad87cd4994c90 (patch)
treeef8b89bed8b05e881fed9495d7cbb613d26b2339 /src/gui
parentd3b28f86515df73194d1102253b739b51b1909f5 (diff)
Add sorting of HTML export
- Closes #6164 - Implement sorting support in HtmlExporter - Add ExportDialog class and UI, which allows to configure export options.
Diffstat (limited to 'src/gui')
-rw-r--r--src/gui/DatabaseTabWidget.cpp26
-rw-r--r--src/gui/DatabaseTabWidget.h1
-rw-r--r--src/gui/HtmlExporter.cpp133
-rw-r--r--src/gui/HtmlExporter.h16
-rw-r--r--src/gui/export/ExportDialog.cpp85
-rw-r--r--src/gui/export/ExportDialog.h58
-rw-r--r--src/gui/export/ExportDialog.ui79
7 files changed, 322 insertions, 76 deletions
diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp
index 40d42f4e1..bcdf8f10d 100644
--- a/src/gui/DatabaseTabWidget.cpp
+++ b/src/gui/DatabaseTabWidget.cpp
@@ -30,6 +30,7 @@
#include "gui/FileDialog.h"
#include "gui/HtmlExporter.h"
#include "gui/MessageBox.h"
+#include "gui/export/ExportDialog.h"
#ifdef Q_OS_MACOS
#include "gui/osutils/macutils/MacUtils.h"
#endif
@@ -440,6 +441,11 @@ void DatabaseTabWidget::exportToCsv()
}
}
+void DatabaseTabWidget::handleExportError(const QString& reason)
+{
+ emit messageGlobal(tr("Writing the HTML file failed.").append("\n").append(reason), MessageWidget::Error);
+}
+
void DatabaseTabWidget::exportToHtml()
{
auto db = databaseWidgetFromIndex(currentIndex())->database();
@@ -448,23 +454,9 @@ void DatabaseTabWidget::exportToHtml()
return;
}
- if (!warnOnExport()) {
- return;
- }
-
- 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, db)) {
- emit messageGlobal(tr("Writing the HTML file failed.").append("\n").append(htmlExporter.errorString()),
- MessageWidget::Error);
- }
+ auto exportDialog = new ExportDialog(db, this);
+ connect(exportDialog, SIGNAL(exportFailed(QString)), SLOT(handleExportError(const QString&)));
+ exportDialog->exec();
}
bool DatabaseTabWidget::warnOnExport()
diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h
index 4e539339b..faad74552 100644
--- a/src/gui/DatabaseTabWidget.h
+++ b/src/gui/DatabaseTabWidget.h
@@ -100,6 +100,7 @@ private slots:
void emitActiveDatabaseChanged();
void emitDatabaseLockChanged();
void handleDatabaseUnlockDialogFinished(bool accepted, DatabaseWidget* dbWidget);
+ void handleExportError(const QString& reason);
private:
QSharedPointer<Database> execNewDatabaseWizard();
diff --git a/src/gui/HtmlExporter.cpp b/src/gui/HtmlExporter.cpp
index c79681637..70249ed26 100644
--- a/src/gui/HtmlExporter.cpp
+++ b/src/gui/HtmlExporter.cpp
@@ -39,82 +39,87 @@ namespace
return QString("<img src=\"data:image/png;base64,") + a.toBase64() + "\"/>";
}
- QString formatHTML(const QString& value)
- {
- return value.toHtmlEscaped().replace(" ", "&nbsp;").replace('\n', "<br>");
- }
-
- QString formatAttribute(const QString& key,
- const QString& value,
- const QString& classname,
- const QString& templt = QString("<tr><th>%1</th><td class=\"%2\">%3</td></tr>"))
- {
- 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("<tr><th>%1</th><td class=\"%2\">%3</td></tr>"))
- {
- 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"));
+ const auto& u = entry.username();
+ if (!u.isEmpty()) {
+ item.append("<tr><th>");
+ item.append(QObject::tr("User name"));
+ item.append("</th><td class=\"username\">");
+ item.append(entry.username().toHtmlEscaped());
+ item.append("</td></tr>");
+ }
- item.append(formatAttribute(entry, QObject::tr("Password"), entry.password(), "password"));
+ const auto& p = entry.password();
+ if (!p.isEmpty()) {
+ item.append("<tr><th>");
+ item.append(QObject::tr("Password"));
+ item.append("</th><td class=\"password\">");
+ item.append(entry.password().toHtmlEscaped());
+ item.append("</td></tr>");
+ }
- if (!entry.url().isEmpty()) {
+ const auto& r = entry.url();
+ if (!r.isEmpty()) {
+ item.append("<tr><th>");
+ item.append(QObject::tr("URL"));
+ item.append("</th><td class=\"url\"><a href=\"");
+ item.append(r.toHtmlEscaped());
+ 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;
- QString displayedURL(formatHTML(entry.url()).mid(0, maxlen));
-
- if (displayedURL.size() == maxlen) {
- displayedURL.append("&hellip;");
+ if (r.size() <= maxlen) {
+ item.append(r.toHtmlEscaped());
+ } else {
+ item.append(r.mid(0, maxlen).toHtmlEscaped());
+ item.append("&hellip;");
}
- item.append(formatAttribute(entry,
- QObject::tr("URL"),
- entry.url(),
- "url",
- R"(<tr><th>%1</th><td class="%2"><a href="%3">%4</a></td></tr>)")
- .arg(entry.resolveMultiplePlaceholders(displayedURL)));
+ item.append("</a></td></tr>");
}
- item.append(formatAttribute(entry, QObject::tr("Notes"), entry.notes(), "notes"));
+ const auto& n = entry.notes();
+ if (!n.isEmpty()) {
+ item.append("<tr><th>");
+ item.append(QObject::tr("Notes"));
+ item.append("</th><td class=\"notes\">");
+ item.append(entry.notes().toHtmlEscaped().replace("\n", "<br>"));
+ item.append("</td></tr>");
+ }
// 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("<tr><th>");
+ item.append(key.toHtmlEscaped());
+ item.append("</th><td class=\"attr\">");
+ item.append(attr->value(key).toHtmlEscaped().replace(" ", "&nbsp;").replace("\n", "<br>"));
+ item.append("</td></tr>");
}
}
return item;
}
} // namespace
-bool HtmlExporter::exportDatabase(const QString& filename, const QSharedPointer<const Database>& db)
+bool HtmlExporter::exportDatabase(const QString& filename,
+ const QSharedPointer<const Database>& 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<const Database>& db)
+bool HtmlExporter::exportDatabase(QIODevice* device,
+ const QSharedPointer<const Database>& db,
+ bool sorted,
+ bool ascending)
{
const auto meta = db->metadata();
if (!meta) {
@@ -171,7 +179,7 @@ bool HtmlExporter::exportDatabase(QIODevice* device, const QSharedPointer<const
}
if (db->rootGroup()) {
- 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 QSharedPointer<const
return true;
}
-bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString path)
+bool HtmlExporter::writeGroup(QIODevice& device, const Group& group, QString path, bool sorted, bool ascending)
{
// Don't output the recycle bin
if (&group == group.database()->metadata()->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("<hr><h2>");
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("<table width=\"100%\">");
+ 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 3a592e54a..1ee9b4448 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<const Database>& db);
+ bool exportDatabase(const QString& filename,
+ 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 writeGroup(QIODevice& device, const Group& group, QString path = QString());
+ bool exportDatabase(QIODevice* device,
+ const QSharedPointer<const Database>& 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 000000000..3537505cc
--- /dev/null
+++ b/src/gui/export/ExportDialog.cpp
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2021 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 "ExportDialog.h"
+#include "ui_ExportDialog.h"
+
+#include "gui/FileDialog.h"
+#include "gui/HtmlExporter.h"
+
+ExportDialog::ExportDialog(QSharedPointer<const Database> 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 000000000..7e5986867
--- /dev/null
+++ b/src/gui/export/ExportDialog.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2021 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 KEEPASSXC_EXPORTDIALOG_H
+#define KEEPASSXC_EXPORTDIALOG_H
+
+#include "core/Database.h"
+#include "gui/DatabaseTabWidget.h"
+#include <QDialog>
+
+namespace Ui
+{
+ class ExportDialog;
+}
+
+class ExportDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit ExportDialog(QSharedPointer<const Database> 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<Ui::ExportDialog> m_ui;
+ QSharedPointer<const Database> 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 000000000..16500b13c
--- /dev/null
+++ b/src/gui/export/ExportDialog.ui
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ExportDialog</class>
+ <widget class="QDialog" name="ExportDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>186</width>
+ <height>164</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Export options</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="MessageWidget" name="messageWidget" native="true">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="sortingStrategyLabel">
+ <property name="text">
+ <string>Sort entries by...</string>
+ </property>
+ <property name="buddy">
+ <cstring>sortingStrategy</cstring>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="sortingStrategy">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>MessageWidget</class>
+ <extends>QWidget</extends>
+ <header>gui/MessageWidget.h</header>
+ <container>1</container>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>