diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index b30987ee..6e6f6919 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -2217,14 +2217,6 @@ This is definitely a bug, please report it to the developers. Failed to open %1. It either does not exist or is not accessible. - - Export database to HTML file - - - - HTML file - - Writing the HTML file failed. @@ -3836,6 +3828,47 @@ Would you like to overwrite the existing attachment? Reset to defaults + + ExportDialog + + Export options + + + + You are about to export your database to an unencrypted file. +This will leave your passwords and sensitive information vulnerable! + + + + + Export database to HTML file + + + + HTML file + + + + database order + + + + name (ascending) + + + + name (descending) + + + + Sort entries by... + + + + unknown + + + FdoSecrets::DBusMgr diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 43051518..a6dcd8e9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -145,6 +145,7 @@ set(keepassx_SOURCES gui/entry/EntryHistoryModel.cpp gui/entry/EntryModel.cpp gui/entry/EntryView.cpp + gui/export/ExportDialog.cpp gui/group/EditGroupWidget.cpp gui/group/GroupModel.cpp gui/group/GroupView.cpp diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index 40d42f4e..bcdf8f10 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 4e539339..faad7455 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 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 +
gui/MessageWidget.h
+ 1 +
+
+ + +