diff options
author | Patrick Sean Klein <patrick@libklein.com> | 2021-10-10 17:36:19 +0300 |
---|---|---|
committer | Jonathan White <support@dmapps.us> | 2021-11-24 08:12:13 +0300 |
commit | 296cbf0df77020336060795a30fad87cd4994c90 (patch) | |
tree | ef8b89bed8b05e881fed9495d7cbb613d26b2339 /src/gui | |
parent | d3b28f86515df73194d1102253b739b51b1909f5 (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.cpp | 26 | ||||
-rw-r--r-- | src/gui/DatabaseTabWidget.h | 1 | ||||
-rw-r--r-- | src/gui/HtmlExporter.cpp | 133 | ||||
-rw-r--r-- | src/gui/HtmlExporter.h | 16 | ||||
-rw-r--r-- | src/gui/export/ExportDialog.cpp | 85 | ||||
-rw-r--r-- | src/gui/export/ExportDialog.h | 58 | ||||
-rw-r--r-- | src/gui/export/ExportDialog.ui | 79 |
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(" ", " ").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("…"); + if (r.size() <= maxlen) { + item.append(r.toHtmlEscaped()); + } else { + item.append(r.mid(0, maxlen).toHtmlEscaped()); + item.append("…"); } - 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(" ", " ").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> |