diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index f974db170b..cc225c4feb 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -3806,6 +3806,10 @@ Supported extensions are: %1. Search toggle for this and sub groups + + Exclude from database reports + + EditWidgetIcons @@ -9499,6 +9503,47 @@ This option is deprecated, use --set-key-file instead. + + ReportsWidgetBase + + Please wait, report is being calculated… + + + + Edit Entry… + + + + Expire Entry(s)… + + + + + + + Delete Entry(s)… + + + + + + + Exclude Entry(s) from reports + + + + Exclude Group(s) from reports + + + + The Group for "%1" is excluded. Would you like to include all Entries from there as well? + + + + Include Group? + + + ReportsWidgetBrowserStatistics @@ -9533,10 +9578,6 @@ This option is deprecated, use --set-key-file instead. This entry is being excluded from reports - - Please wait, browser statistics is being calculated… - - No entries with a URL, or none has browser extension settings saved. @@ -9553,28 +9594,6 @@ This option is deprecated, use --set-key-file instead. URLs - - Edit Entry… - - - - Delete Entry(s)… - - - - - - - Exclude from reports - - - - Expire Entry(s)… - - - - - Only show entries that have a URL @@ -9598,6 +9617,14 @@ This option is deprecated, use --set-key-file instead. + + (Group Excluded) + + + + The group for this entry is being excluded from reports + + ReportsWidgetHealthcheck @@ -9633,10 +9660,6 @@ This option is deprecated, use --set-key-file instead. This entry is being excluded from reports - - Please wait, health data is being calculated… - - Congratulations, everything is healthy! @@ -9658,29 +9681,15 @@ This option is deprecated, use --set-key-file instead. - Edit Entry… + Show entries that have been excluded from reports - - Delete Entry(s)… - - - - - - Exclude from reports + (Group Excluded) - - Expire Entry(s)… - - - - - - Show entries that have been excluded from reports + The group for this entry is being excluded from reports @@ -9767,27 +9776,13 @@ This option is deprecated, use --set-key-file instead. - Edit Entry… + (Group Excluded) - - Delete Entry(s)… - - - - - - Exclude from reports + The group for this entry is being excluded from reports - - Expire Entry(s)… - - - - - ReportsWidgetPasskeys @@ -9850,10 +9845,6 @@ This option is deprecated, use --set-key-file instead. The passkey file will be vulnerable to theft and unauthorized use, if left unsecured. Are you sure you want to continue? - - Please wait, list of entries with passkeys is being updated… - - No entries with passkeys. @@ -9988,6 +9979,14 @@ This option is deprecated, use --set-key-file instead. + + Groups excluded from reports + + + + Excluding entire groups from reports isn't necessarily a problem but please exercise caution when excluding entire groups. + + SSHAgent diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6a5eb5d801..c16a25b8d7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -194,6 +194,7 @@ set(gui_SOURCES gui/remote/RemoteProcess.cpp gui/remote/RemoteSettings.cpp gui/reports/ReportsWidget.cpp + gui/reports/ReportsWidgetBase.cpp gui/reports/ReportsDialog.cpp gui/reports/ReportsWidgetHealthcheck.cpp gui/reports/ReportsPageHealthcheck.cpp diff --git a/src/core/DatabaseStats.cpp b/src/core/DatabaseStats.cpp index cf2364b08f..fbe985cf63 100644 --- a/src/core/DatabaseStats.cpp +++ b/src/core/DatabaseStats.cpp @@ -76,6 +76,10 @@ void DatabaseStats::gatherStats(const QList& groups) ++groupCount; + if (group->excludeFromReports()) { + ++excludedGroups; + } + for (const auto* entry : group->entries()) { // Don't count anything in the recycle bin if (entry->isRecycled()) { @@ -107,7 +111,7 @@ void DatabaseStats::gatherStats(const QList& groups) ++weakPasswords; } - if (entry->excludeFromReports()) { + if (entry->excludeFromReports() || group->excludeFromReports()) { ++excludedEntries; } diff --git a/src/core/DatabaseStats.h b/src/core/DatabaseStats.h index 2c0ad7c767..d6446e8341 100644 --- a/src/core/DatabaseStats.h +++ b/src/core/DatabaseStats.h @@ -30,6 +30,7 @@ class DatabaseStats int entryCount = 0; // Number of entries (across all groups) int expiredEntries = 0; // Number of expired entries int excludedEntries = 0; // Number of known bad entries + int excludedGroups = 0; // Number of excluded groups from reports int weakPasswords = 0; // Number of weak or poor passwords int shortPasswords = 0; // Number of passwords 8 characters or less in size int uniquePasswords = 0; // Number of unique passwords diff --git a/src/core/Group.cpp b/src/core/Group.cpp index 8b6cd75c51..29c765a6af 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -1281,6 +1281,33 @@ void Group::setPreviousParentGroup(const Group* group) setPreviousParentGroupUuid(group ? group->uuid() : QUuid()); } +void Group::setExcludeFromReports(bool excluded) +{ + customData()->set(CustomData::ExcludeFromReportsLegacy, excluded ? TRUE_STR : FALSE_STR); + + // Clear out the exclusion flag on entries when we set it on the + // group because it'll make it easier to individually set it for an + // entry later on + if (excluded) { + for (auto& entry : m_entries) { + entry->setExcludeFromReports(false); + } + } +} + +void Group::markAllEntriesExcludedFromReports() +{ + for (auto& entry : m_entries) { + entry->setExcludeFromReports(true); + } +} + +bool Group::excludeFromReports() const +{ + return customData()->contains(CustomData::ExcludeFromReportsLegacy) + && customData()->value(CustomData::ExcludeFromReportsLegacy) == TRUE_STR; +} + bool Group::GroupData::operator==(const Group::GroupData& other) const { return equals(other, CompareItemDefault); diff --git a/src/core/Group.h b/src/core/Group.h index 01c0b21202..b6cfbfe71f 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -107,6 +107,7 @@ class Group : public ModifiableObject QString resolveCustomDataString(const QString& key, bool checkParent = true) const; const Group* previousParentGroup() const; QUuid previousParentGroupUuid() const; + bool excludeFromReports() const; bool equals(const Group* other, CompareItemOptions options) const; @@ -140,6 +141,8 @@ class Group : public ModifiableObject void setMergeMode(MergeMode newMode); void setPreviousParentGroup(const Group* group); void setPreviousParentGroupUuid(const QUuid& uuid); + void setExcludeFromReports(bool exclude); + void markAllEntriesExcludedFromReports(); bool canUpdateTimeinfo() const; void setUpdateTimeinfo(bool value); diff --git a/src/gui/group/EditGroupWidget.cpp b/src/gui/group/EditGroupWidget.cpp index 4e406b0d0a..77c51e7776 100644 --- a/src/gui/group/EditGroupWidget.cpp +++ b/src/gui/group/EditGroupWidget.cpp @@ -126,6 +126,7 @@ void EditGroupWidget::setupModifiedTracking() connect(m_mainUi->autoTypeSequenceInherit, SIGNAL(toggled(bool)), SLOT(setModified())); connect(m_mainUi->autoTypeSequenceCustomRadio, SIGNAL(toggled(bool)), SLOT(setModified())); connect(m_mainUi->autoTypeSequenceCustomEdit, SIGNAL(textChanged(QString)), SLOT(setModified())); + connect(m_mainUi->excludeReportsCheckBox, SIGNAL(stateChanged(int)), SLOT(setModified())); // Icon tab connect(m_editGroupWidgetIcons, SIGNAL(widgetUpdated()), SLOT(setModified())); @@ -171,6 +172,7 @@ void EditGroupWidget::loadGroup(Group* group, bool create, const QSharedPointer< m_mainUi->autoTypeSequenceCustomRadio->setChecked(true); } m_mainUi->autoTypeSequenceCustomEdit->setText(group->effectiveAutoTypeSequence()); + m_mainUi->excludeReportsCheckBox->setChecked(m_group->excludeFromReports()); if (config()->get(Config::GUI_MonospaceNotes).toBool()) { m_mainUi->editNotes->setFont(Font::fixedFont()); @@ -265,6 +267,7 @@ void EditGroupWidget::apply() m_temporaryGroup->setSearchingEnabled(triStateFromIndex(m_mainUi->searchComboBox->currentIndex())); m_temporaryGroup->setAutoTypeEnabled(triStateFromIndex(m_mainUi->autotypeComboBox->currentIndex())); + m_temporaryGroup->setExcludeFromReports(m_mainUi->excludeReportsCheckBox->isChecked()); if (m_mainUi->autoTypeSequenceInherit->isChecked()) { m_temporaryGroup->setDefaultAutoTypeSequence(QString()); diff --git a/src/gui/group/EditGroupWidgetMain.ui b/src/gui/group/EditGroupWidgetMain.ui index faa8a30ff3..26af28b6e0 100644 --- a/src/gui/group/EditGroupWidgetMain.ui +++ b/src/gui/group/EditGroupWidgetMain.ui @@ -37,7 +37,7 @@ 523 - + 0 @@ -56,7 +56,7 @@ 8 - + Toggle expiration @@ -66,10 +66,10 @@ - - - - Name field + + + + Set default Auto-Type sequence @@ -86,30 +86,10 @@ - - - - Use default Auto-Type sequence of parent group - - - - - - - Auto-Type: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Search: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + Name field @@ -120,6 +100,13 @@ + + + + Search toggle for this and sub groups + + + @@ -199,42 +186,62 @@ - - + + + + Qt::Vertical + + + + 20 + 40 + + + + + + - Name: + Search: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - + + - Set default Auto-Type sequence + Auto-Type: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - Search toggle for this and sub groups + + + + Use default Auto-Type sequence of parent group - - - - Qt::Vertical + + + + Name: - - - 20 - 40 - + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + + + + + + Exclude from database reports + + diff --git a/src/gui/reports/ProxyModels.h b/src/gui/reports/ProxyModels.h new file mode 100644 index 0000000000..6311afeff2 --- /dev/null +++ b/src/gui/reports/ProxyModels.h @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2025 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_PROXYMODELS_H +#define KEEPASSXC_PROXYMODELS_H + +#include +#include + +enum class SortProxyModelKind +{ + Default = 0, + Hibp, + Healthcheck, +}; + +class HibpReportSortProxyModel : public QSortFilterProxyModel +{ +public: + HibpReportSortProxyModel(QObject* parent) + : QSortFilterProxyModel(parent) + { + } + ~HibpReportSortProxyModel() override = default; + +protected: + bool lessThan(const QModelIndex& left, const QModelIndex& right) const override + { + // Sort count column by user data + if (left.column() == 2) { + return sourceModel()->data(left, Qt::UserRole).toInt() < sourceModel()->data(right, Qt::UserRole).toInt(); + } + // Otherwise use default sorting + return QSortFilterProxyModel::lessThan(left, right); + } +}; + +class HealthcheckReportSortProxyModel : public QSortFilterProxyModel +{ +public: + HealthcheckReportSortProxyModel(QObject* parent) + : QSortFilterProxyModel(parent) + { + } + ~HealthcheckReportSortProxyModel() override = default; + +protected: + bool lessThan(const QModelIndex& left, const QModelIndex& right) const override + { + // Check if the display data is a number, convert and compare if so + bool ok = false; + int leftInt = sourceModel()->data(left).toString().toInt(&ok); + if (ok) { + return leftInt < sourceModel()->data(right).toString().toInt(); + } + // Otherwise use default sorting + return QSortFilterProxyModel::lessThan(left, right); + } +}; + +#endif // KEEPASSXC_PROXYMODELS_H diff --git a/src/gui/reports/ReportsWidgetBase.cpp b/src/gui/reports/ReportsWidgetBase.cpp new file mode 100644 index 0000000000..02b469c66b --- /dev/null +++ b/src/gui/reports/ReportsWidgetBase.cpp @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2025 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 "ReportsWidgetBase.h" + +#include "core/Group.h" +#include "core/Metadata.h" +#include "gui/GuiTools.h" +#include "gui/Icons.h" +#include "gui/MessageBox.h" + +#include +#include +#include +#include +#include +#include + +ReportsWidgetBase::ReportsWidgetBase(QWidget* parent, SortProxyModelKind proxyModel) + : QWidget{parent} + , m_referencesModel(new QStandardItemModel(this)) +{ + // We have to initialize this here; if we do it in the constructor initializer list, + // the base object isn't setup enough and the constructor for QSortFilterProxyModel + // crashes. + switch (proxyModel) { + case SortProxyModelKind::Default: + m_modelProxy.reset(new QSortFilterProxyModel(this)); + break; + case SortProxyModelKind::Healthcheck: + m_modelProxy.reset(new HealthcheckReportSortProxyModel(this)); + break; + case SortProxyModelKind::Hibp: + m_modelProxy.reset(new HibpReportSortProxyModel(this)); + break; + } +} + +ReportsWidgetBase::~ReportsWidgetBase() +{ +} + +void ReportsWidgetBase::loadSettings(QSharedPointer db) +{ + m_db = std::move(db); + m_widgetDataCalculated = false; + m_referencesModel->clear(); + m_rowToEntry.clear(); + + auto row = QList(); + row << new QStandardItem(tr("Please wait, report is being calculated…")); + m_referencesModel->appendRow(row); +} + +void ReportsWidgetBase::saveSettings() +{ + // Most report tabs are passive, so override them in derived classes if they need to + // save settings +} + +QMenu* ReportsWidgetBase::customMenuRequestedBase() +{ + auto selected = getTableView()->selectionModel()->selectedRows(); + if (selected.isEmpty()) { + return nullptr; + } + + // Create the context menu + const auto menu = new QMenu(this); + menu->setObjectName("customMenu"); + + // Create the "edit entry" menu item (only if 1 row is selected) + if (selected.size() == 1) { + const auto edit = new QAction(icons()->icon("entry-edit"), tr("Edit Entry…"), this); + edit->setObjectName("contextMenuEditAction"); + menu->addAction(edit); + connect(edit, &QAction::triggered, edit, [this, selected] { + auto row = m_modelProxy->mapToSource(selected[0]).row(); + auto entry = m_rowToEntry[row].second; + emit entryActivated(entry); + }); + } + + // Create the "Expire entry" menu item + const auto expEntry = new QAction(icons()->icon("entry-expire"), tr("Expire Entry(s)…", "", selected.size()), this); + expEntry->setObjectName("contextMenuExpireAction"); + menu->addAction(expEntry); + connect(expEntry, &QAction::triggered, this, &ReportsWidgetBase::expireSelectedEntries); + + // Create the "delete entry" menu item + const auto delEntry = new QAction(icons()->icon("entry-delete"), tr("Delete Entry(s)…", "", selected.size()), this); + menu->addAction(delEntry); + connect(delEntry, &QAction::triggered, this, &ReportsWidgetBase::deleteSelectedEntries); + + // Create the "exclude from reports" menu item + const auto excludeAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Entry(s) from reports"), this); + excludeAction->setObjectName("contextMenuExcludeAction"); + const auto excludeGroupsAction = + new QAction(icons()->icon("reports-exclude"), tr("Exclude Group(s) from reports"), this); + excludeGroupsAction->setObjectName("contextMenuExcludeGroupAction"); + + bool isExcluded = false; + bool isGroupExcluded = false; + + for (auto index : selected) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row].second; + if (entry) { + // If at least one entry is excluded switch to inclusion + if (entry->excludeFromReports() || entry->group()->excludeFromReports()) { + isExcluded = true; + } + if (entry->group()->excludeFromReports()) { + isGroupExcluded = true; + } + + break; + } + } + excludeAction->setCheckable(true); + excludeAction->setChecked(isExcluded); + + excludeGroupsAction->setCheckable(true); + excludeGroupsAction->setChecked(isGroupExcluded); + + menu->addAction(excludeAction); + connect(excludeAction, &QAction::toggled, excludeAction, [this, selected](bool checked) { + QSet groups; + + // If we are including entries (checked is false) but a group is excluded, ask the user if they + // would like to include the rest of the group as well (or keep it excluded). + // If they choose "No", we need to include the whole group, and then exclude + // the entries that aren't selected here. + if (!checked) { + for (const auto index : selected) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row].second; + + if (entry) { + auto* group = entry->group(); + if (group->excludeFromReports() && !groups.contains(group)) { + QString msg = tr("The Group for \"%1\" is excluded. Would you like to include all Entries from " + "there as well?") + .arg(entry->title()); + auto response = MessageBox::question(this, + tr("Include Group?"), + msg, + MessageBox::Yes | MessageBox::No | MessageBox::Cancel, + MessageBox::No); + + if (response == MessageBox::Cancel) { + return; + } else if (response == MessageBox::Yes) { + group->setExcludeFromReports(false); + } else if (response == MessageBox::No) { + // We'll exclude all entries from the group here and then + // include the selected ones below + group->setExcludeFromReports(false); + group->markAllEntriesExcludedFromReports(); + } + + groups.insert(group); + } + } + } + } + + for (auto index : selected) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row].second; + + // If the containing group is excluded but the user wants to include + // this entry, ask if they want to keep the remaining items in the group + // excluded or included + if (entry) { + entry->setExcludeFromReports(checked); + } + } + updateWidget(); + }); + + menu->addAction(excludeGroupsAction); + connect(excludeGroupsAction, &QAction::toggled, excludeGroupsAction, [this, selected](bool checked) { + for (const auto index : selected) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row].second; + if (entry) { + entry->group()->setExcludeFromReports(checked); + } + } + updateWidget(); + }); + + return menu; +} + +QList ReportsWidgetBase::getSelectedEntries() const +{ + QList selectedEntries; + for (auto index : getTableView()->selectionModel()->selectedRows()) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row].second; + if (entry) { + selectedEntries << entry; + } + } + return selectedEntries; +} + +void ReportsWidgetBase::expireSelectedEntries() +{ + for (auto entry : getSelectedEntries()) { + entry->expireNow(); + } + + updateWidget(); +} + +void ReportsWidgetBase::deleteSelectedEntries() +{ + const auto& selectedEntries = getSelectedEntries(); + bool permanent = !m_db->metadata()->recycleBinEnabled(); + + if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) { + GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent); + } + + updateWidget(); +} + +void ReportsWidgetBase::emitEntryActivated(const QModelIndex& index) +{ + if (!index.isValid()) { + return; + } + + auto mappedIndex = m_modelProxy->mapToSource(index); + const auto row = m_rowToEntry[mappedIndex.row()]; + const auto group = row.first; + const auto entry = row.second; + + if (group && entry) { + emit entryActivated(const_cast(entry)); + } +} diff --git a/src/gui/reports/ReportsWidgetBase.h b/src/gui/reports/ReportsWidgetBase.h new file mode 100644 index 0000000000..c0ac484a36 --- /dev/null +++ b/src/gui/reports/ReportsWidgetBase.h @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2025 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_REPORTSWIDGETBASE_H +#define KEEPASSXC_REPORTSWIDGETBASE_H + +#include + +#include "gui/entry/EntryModel.h" +#include "gui/reports/ProxyModels.h" + +class Database; +class Entry; +class Group; +class PasswordHealth; +class QSortFilterProxyModel; +class QStandardItemModel; +class QMenu; +class QTableView; + +/** + * @brief The ReportsWidgetBase class implements functionality common across the various + * database report widgets. + */ +class ReportsWidgetBase : public QWidget +{ + Q_OBJECT +public: + explicit ReportsWidgetBase(QWidget* parent, SortProxyModelKind); + virtual ~ReportsWidgetBase(); + + virtual void loadSettings(QSharedPointer db); + virtual void saveSettings(); + +protected: + virtual QTableView* getTableView() const = 0; + virtual void updateWidget() = 0; + + QMenu* customMenuRequestedBase(); + +public slots: + QList getSelectedEntries() const; + void expireSelectedEntries(); + void deleteSelectedEntries(); + void emitEntryActivated(const QModelIndex& index); + +signals: + void entryActivated(Entry*); + +protected: + bool m_widgetDataCalculated = false; + QScopedPointer m_referencesModel; + QScopedPointer m_modelProxy; + QSharedPointer m_db; + QList> m_rowToEntry; +}; + +#endif // KEEPASSXC_REPORTSWIDGETBASE_H diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp index 63267d77fd..d2caeb066f 100644 --- a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp +++ b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp @@ -50,7 +50,7 @@ namespace , entry(e) , hasUrls(hU) , hasSettings(hS) - , exclude(e->excludeFromReports()) + , exclude(e->excludeFromReports() || g->excludeFromReports()) { } }; @@ -92,10 +92,8 @@ BrowserStatistics::BrowserStatistics(QSharedPointer db) } ReportsWidgetBrowserStatistics::ReportsWidgetBrowserStatistics(QWidget* parent) - : QWidget(parent) + : ReportsWidgetBase(parent, SortProxyModelKind::Default) , m_ui(new Ui::ReportsWidgetBrowserStatistics()) - , m_referencesModel(new QStandardItemModel(this)) - , m_modelProxy(new QSortFilterProxyModel(this)) { m_ui->setupUi(this); @@ -141,7 +139,11 @@ void ReportsWidgetBrowserStatistics::addStatisticsRow(bool hasUrls, auto title = entry->title(); if (excluded) { - title.append(tr(" (Excluded)")); + if (group->excludeFromReports()) { + title.append(tr(" (Group Excluded)")); + } else { + title.append(tr(" (Excluded)")); + } } if (entry->isExpired()) { title.append(tr(" (Expired)")); @@ -159,7 +161,11 @@ void ReportsWidgetBrowserStatistics::addStatisticsRow(bool hasUrls, row[3]->setToolTip(allowedUrlsToolTip); row[4]->setToolTip(deniedUrlsToolTip); if (excluded) { - row[0]->setToolTip(tr("This entry is being excluded from reports")); + if (group->excludeFromReports()) { + row[0]->setToolTip(tr("The group for this entry is being excluded from reports")); + } else { + row[0]->setToolTip(tr("This entry is being excluded from reports")); + } } // Store entry pointer per table row (used in double click handler) @@ -167,25 +173,13 @@ void ReportsWidgetBrowserStatistics::addStatisticsRow(bool hasUrls, m_rowToEntry.append({group, entry}); } -void ReportsWidgetBrowserStatistics::loadSettings(QSharedPointer db) -{ - m_db = std::move(db); - m_statisticsCalculated = false; - m_referencesModel->clear(); - m_rowToEntry.clear(); - - auto row = QList(); - row << new QStandardItem(tr("Please wait, browser statistics is being calculated…")); - m_referencesModel->appendRow(row); -} - void ReportsWidgetBrowserStatistics::showEvent(QShowEvent* event) { QWidget::showEvent(event); - if (!m_statisticsCalculated) { + if (!m_widgetDataCalculated) { // Perform stats calculation on next event loop to allow widget to appear - m_statisticsCalculated = true; + m_widgetDataCalculated = true; QTimer::singleShot(0, this, SLOT(calculateBrowserStatistics())); } } @@ -238,134 +232,30 @@ void ReportsWidgetBrowserStatistics::calculateBrowserStatistics() m_ui->browserStatisticsTableView->resizeColumnsToContents(); } -void ReportsWidgetBrowserStatistics::emitEntryActivated(const QModelIndex& index) -{ - if (!index.isValid()) { - return; - } - - auto mappedIndex = m_modelProxy->mapToSource(index); - const auto row = m_rowToEntry[mappedIndex.row()]; - const auto group = row.first; - const auto entry = row.second; - - if (group && entry) { - emit entryActivated(const_cast(entry)); - } -} - void ReportsWidgetBrowserStatistics::customMenuRequested(QPoint pos) { - auto selected = m_ui->browserStatisticsTableView->selectionModel()->selectedRows(); - if (selected.isEmpty()) { - return; - } + auto menu = customMenuRequestedBase(); - // Create the context menu - const auto menu = new QMenu(this); - - // Create the "edit entry" menu item (only if 1 row is selected) - if (selected.size() == 1) { - const auto edit = new QAction(icons()->icon("entry-edit"), tr("Edit Entry…"), this); - menu->addAction(edit); - connect(edit, &QAction::triggered, edit, [this, selected] { - auto row = m_modelProxy->mapToSource(selected[0]).row(); - auto entry = m_rowToEntry[row].second; - emit entryActivated(entry); - }); + if (!menu) { + return; } - // Create the "expire entry" menu item - const auto expEntry = new QAction(icons()->icon("entry-expire"), tr("Expire Entry(s)…", "", selected.size()), this); - menu->addAction(expEntry); - connect(expEntry, &QAction::triggered, this, &ReportsWidgetBrowserStatistics::expireSelectedEntries); - - // Create the "delete entry" menu item - const auto deleteEntry = - new QAction(icons()->icon("entry-delete"), tr("Delete Entry(s)…", "", selected.size()), this); - menu->addAction(deleteEntry); - connect(deleteEntry, &QAction::triggered, this, &ReportsWidgetBrowserStatistics::deleteSelectedEntries); + auto selected = getTableView()->selectionModel()->selectedRows(); // Create the "delete plugin data" menu item const auto deletePluginData = new QAction(icons()->icon("entry-delete"), tr("Delete plugin data from Entry(s)…", "", selected.size()), this); - menu->addAction(deletePluginData); + menu->insertAction(menu->actions().at(3), + deletePluginData); // Index 3 is the one after "Delete Entry" so place "Delete plugin" before it connect(deletePluginData, &QAction::triggered, this, &ReportsWidgetBrowserStatistics::deletePluginDataFromSelectedEntries); - // Create the "exclude from reports" menu item - const auto exclude = new QAction(icons()->icon("reports-exclude"), tr("Exclude from reports"), this); - - bool isExcluded = false; - for (auto index : selected) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row].second; - if (entry && entry->excludeFromReports()) { - // If at least one entry is excluded switch to inclusion - isExcluded = true; - break; - } - } - exclude->setCheckable(true); - exclude->setChecked(isExcluded); - - menu->addAction(exclude); - connect(exclude, &QAction::toggled, exclude, [this, selected](bool state) { - for (auto index : selected) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row].second; - if (entry) { - entry->setExcludeFromReports(state); - } - } - calculateBrowserStatistics(); - }); - // Show the context menu menu->popup(m_ui->browserStatisticsTableView->viewport()->mapToGlobal(pos)); } -void ReportsWidgetBrowserStatistics::saveSettings() -{ - // Nothing to do - the tab is passive -} - -QList ReportsWidgetBrowserStatistics::getSelectedEntries() -{ - QList selectedEntries; - for (auto index : m_ui->browserStatisticsTableView->selectionModel()->selectedRows()) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row].second; - if (entry) { - selectedEntries << entry; - } - } - return selectedEntries; -} - -void ReportsWidgetBrowserStatistics::expireSelectedEntries() -{ - for (auto entry : getSelectedEntries()) { - entry->expireNow(); - } - - calculateBrowserStatistics(); -} - -void ReportsWidgetBrowserStatistics::deleteSelectedEntries() -{ - const auto& selectedEntries = getSelectedEntries(); - bool permanent = !m_db->metadata()->recycleBinEnabled(); - - if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) { - GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent); - } - - calculateBrowserStatistics(); -} - void ReportsWidgetBrowserStatistics::deletePluginDataFromSelectedEntries() { const auto& selectedEntries = getSelectedEntries(); @@ -414,16 +304,12 @@ QMap ReportsWidgetBrowserStatistics::getBrowserConfigFromE return configList; } -QList ReportsWidgetBrowserStatistics::getSelectedEntries() const +QTableView* ReportsWidgetBrowserStatistics::getTableView() const { - QList selectedEntries; - for (auto index : m_ui->browserStatisticsTableView->selectionModel()->selectedRows()) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row].second; - if (entry) { - selectedEntries << entry; - } - } + return m_ui->browserStatisticsTableView; +} - return selectedEntries; +void ReportsWidgetBrowserStatistics::updateWidget() +{ + calculateBrowserStatistics(); } diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.h b/src/gui/reports/ReportsWidgetBrowserStatistics.h index 9b1cc7d602..71a08a3a2e 100644 --- a/src/gui/reports/ReportsWidgetBrowserStatistics.h +++ b/src/gui/reports/ReportsWidgetBrowserStatistics.h @@ -19,6 +19,7 @@ #define KEEPASSXC_REPORTSWIDGETBROWSERSTATISTICS_H #include "gui/entry/EntryModel.h" +#include "gui/reports/ReportsWidgetBase.h" #include class Database; @@ -33,43 +34,28 @@ namespace Ui class ReportsWidgetBrowserStatistics; } -class ReportsWidgetBrowserStatistics : public QWidget +class ReportsWidgetBrowserStatistics : public ReportsWidgetBase { Q_OBJECT public: explicit ReportsWidgetBrowserStatistics(QWidget* parent = nullptr); ~ReportsWidgetBrowserStatistics() override; - void loadSettings(QSharedPointer db); - void saveSettings(); - protected: void showEvent(QShowEvent* event) override; - -signals: - void entryActivated(Entry*); + void updateWidget() override; + QTableView* getTableView() const override; public slots: void calculateBrowserStatistics(); - void emitEntryActivated(const QModelIndex& index); void customMenuRequested(QPoint); - QList getSelectedEntries(); - void expireSelectedEntries(); - void deleteSelectedEntries(); void deletePluginDataFromSelectedEntries(); private: void addStatisticsRow(bool hasUrls, bool hasSettings, Group*, Entry*, bool); - QList getSelectedEntries() const; QMap getBrowserConfigFromEntry(Entry* entry) const; QScopedPointer m_ui; - - bool m_statisticsCalculated = false; - QScopedPointer m_referencesModel; - QScopedPointer m_modelProxy; - QSharedPointer m_db; - QList> m_rowToEntry; }; #endif // KEEPASSXC_REPORTSWIDGETBROWSERSTATISTICS_H diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp index 1c34c2f36d..086dd776dc 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.cpp +++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp @@ -24,11 +24,11 @@ #include "core/PasswordHealth.h" #include "gui/GuiTools.h" #include "gui/Icons.h" +#include "gui/reports/ProxyModels.h" #include "gui/styles/StateColorPalette.h" #include #include -#include #include namespace @@ -47,7 +47,7 @@ namespace : group(g) , entry(e) , health(h) - , exclude(e->excludeFromReports()) + , exclude(e->excludeFromReports() || g->excludeFromReports()) { } @@ -75,27 +75,6 @@ namespace QList> m_items; bool m_anyExcludedEntries = false; }; - - class ReportSortProxyModel : public QSortFilterProxyModel - { - public: - ReportSortProxyModel(QObject* parent) - : QSortFilterProxyModel(parent){}; - ~ReportSortProxyModel() override = default; - - protected: - bool lessThan(const QModelIndex& left, const QModelIndex& right) const override - { - // Check if the display data is a number, convert and compare if so - bool ok = false; - int leftInt = sourceModel()->data(left).toString().toInt(&ok); - if (ok) { - return leftInt < sourceModel()->data(right).toString().toInt(); - } - // Otherwise use default sorting - return QSortFilterProxyModel::lessThan(left, right); - } - }; } // namespace Health::Health(QSharedPointer db) @@ -137,10 +116,8 @@ Health::Health(QSharedPointer db) } ReportsWidgetHealthcheck::ReportsWidgetHealthcheck(QWidget* parent) - : QWidget(parent) + : ReportsWidgetBase(parent, SortProxyModelKind::Healthcheck) , m_ui(new Ui::ReportsWidgetHealthcheck()) - , m_referencesModel(new QStandardItemModel(this)) - , m_modelProxy(new ReportSortProxyModel(this)) { m_ui->setupUi(this); @@ -197,7 +174,11 @@ void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer healt auto title = entry->title(); if (excluded) { - title.append(tr(" (Excluded)")); + if (group->excludeFromReports()) { + title.append(tr(" (Group Excluded)")); + } else { + title.append(tr(" (Excluded)")); + } } if (entry->isExpired()) { title.append(tr(" (Expired)")); @@ -219,7 +200,11 @@ void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer healt // Set tooltips row[0]->setToolTip(tip); if (excluded) { - row[1]->setToolTip(tr("This entry is being excluded from reports")); + if (group->excludeFromReports()) { + row[1]->setToolTip(tr("The group for this entry is being excluded from reports")); + } else { + row[1]->setToolTip(tr("This entry is being excluded from reports")); + } } row[4]->setToolTip(health->scoreDetails()); @@ -230,15 +215,7 @@ void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer healt void ReportsWidgetHealthcheck::loadSettings(QSharedPointer db) { - m_db = std::move(db); - m_healthCalculated = false; - m_referencesModel->clear(); - m_rowToEntry.clear(); - - auto row = QList(); - row << new QStandardItem(tr("Please wait, health data is being calculated…")); - m_referencesModel->appendRow(row); - // Default sort by first column (health score) + ReportsWidgetBase::loadSettings(db); m_ui->healthcheckTableView->sortByColumn(0, Qt::AscendingOrder); } @@ -246,9 +223,9 @@ void ReportsWidgetHealthcheck::showEvent(QShowEvent* event) { QWidget::showEvent(event); - if (!m_healthCalculated) { + if (!m_widgetDataCalculated) { // Perform stats calculation on next event loop to allow widget to appear - m_healthCalculated = true; + m_widgetDataCalculated = true; QTimer::singleShot(0, this, SLOT(calculateHealth())); } } @@ -294,120 +271,28 @@ void ReportsWidgetHealthcheck::calculateHealth() // Only show the "show excluded" checkbox if there are any excluded entries in the database m_ui->showExcluded->setVisible(health->anyExcludedEntries()); -} -void ReportsWidgetHealthcheck::emitEntryActivated(const QModelIndex& index) -{ - if (!index.isValid()) { - return; - } - - auto mappedIndex = m_modelProxy->mapToSource(index); - const auto row = m_rowToEntry[mappedIndex.row()]; - const auto group = row.first; - const auto entry = row.second; - if (group && entry) { - emit entryActivated(const_cast(entry)); - } + emit tablePopulated(); } void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos) { - auto selected = m_ui->healthcheckTableView->selectionModel()->selectedRows(); - if (selected.isEmpty()) { - return; - } + auto menu = customMenuRequestedBase(); - // Create the context menu - const auto menu = new QMenu(this); - - // Create the "edit entry" menu item (only if 1 row is selected) - if (selected.size() == 1) { - const auto edit = new QAction(icons()->icon("entry-edit"), tr("Edit Entry…"), this); - menu->addAction(edit); - connect(edit, &QAction::triggered, edit, [this, selected] { - auto row = m_modelProxy->mapToSource(selected[0]).row(); - auto entry = m_rowToEntry[row].second; - emit entryActivated(entry); - }); + if (!menu) { + return; } - // Create the "Expire entry" menu item - const auto expEntry = new QAction(icons()->icon("entry-expire"), tr("Expire Entry(s)…", "", selected.size()), this); - menu->addAction(expEntry); - connect(expEntry, &QAction::triggered, this, &ReportsWidgetHealthcheck::expireSelectedEntries); - - // Create the "delete entry" menu item - const auto delEntry = new QAction(icons()->icon("entry-delete"), tr("Delete Entry(s)…", "", selected.size()), this); - menu->addAction(delEntry); - connect(delEntry, &QAction::triggered, this, &ReportsWidgetHealthcheck::deleteSelectedEntries); - - // Create the "exclude from reports" menu item - const auto exclude = new QAction(icons()->icon("reports-exclude"), tr("Exclude from reports"), this); - - bool isExcluded = false; - for (auto index : selected) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row].second; - if (entry && entry->excludeFromReports()) { - // If at least one entry is excluded switch to inclusion - isExcluded = true; - break; - } - } - exclude->setCheckable(true); - exclude->setChecked(isExcluded); - - menu->addAction(exclude); - connect(exclude, &QAction::toggled, exclude, [this, selected](bool state) { - for (auto index : selected) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row].second; - if (entry) { - entry->setExcludeFromReports(state); - } - } - calculateHealth(); - }); - // Show the context menu menu->popup(m_ui->healthcheckTableView->viewport()->mapToGlobal(pos)); } -void ReportsWidgetHealthcheck::saveSettings() -{ - // nothing to do - the tab is passive -} - -QList ReportsWidgetHealthcheck::getSelectedEntries() +void ReportsWidgetHealthcheck::updateWidget() { - QList selectedEntries; - for (auto index : m_ui->healthcheckTableView->selectionModel()->selectedRows()) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row].second; - if (entry) { - selectedEntries << entry; - } - } - return selectedEntries; -} - -void ReportsWidgetHealthcheck::expireSelectedEntries() -{ - for (auto entry : getSelectedEntries()) { - entry->expireNow(); - } - calculateHealth(); } -void ReportsWidgetHealthcheck::deleteSelectedEntries() +QTableView* ReportsWidgetHealthcheck::getTableView() const { - QList selectedEntries = getSelectedEntries(); - bool permanent = !m_db->metadata()->recycleBinEnabled(); - if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) { - GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent); - } - - calculateHealth(); + return m_ui->healthcheckTableView; } diff --git a/src/gui/reports/ReportsWidgetHealthcheck.h b/src/gui/reports/ReportsWidgetHealthcheck.h index 9a46b36b1f..7dfe0cdfc0 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.h +++ b/src/gui/reports/ReportsWidgetHealthcheck.h @@ -19,6 +19,7 @@ #define KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H #include "gui/entry/EntryModel.h" +#include "gui/reports/ReportsWidgetBase.h" #include class Database; @@ -27,46 +28,38 @@ class Group; class PasswordHealth; class QSortFilterProxyModel; class QStandardItemModel; +class QTableView; namespace Ui { class ReportsWidgetHealthcheck; } -class ReportsWidgetHealthcheck : public QWidget +class ReportsWidgetHealthcheck : public ReportsWidgetBase { Q_OBJECT public: explicit ReportsWidgetHealthcheck(QWidget* parent = nullptr); ~ReportsWidgetHealthcheck() override; - void loadSettings(QSharedPointer db); - void saveSettings(); + void loadSettings(QSharedPointer db) override; protected: void showEvent(QShowEvent* event) override; + void updateWidget() override; + QTableView* getTableView() const override; signals: - void entryActivated(Entry*); + void tablePopulated(); public slots: void calculateHealth(); - void emitEntryActivated(const QModelIndex& index); void customMenuRequested(QPoint); - QList getSelectedEntries(); - void expireSelectedEntries(); - void deleteSelectedEntries(); private: void addHealthRow(QSharedPointer, Group*, Entry*, bool excluded); QScopedPointer m_ui; - - bool m_healthCalculated = false; - QScopedPointer m_referencesModel; - QScopedPointer m_modelProxy; - QSharedPointer m_db; - QList> m_rowToEntry; }; #endif // KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H diff --git a/src/gui/reports/ReportsWidgetHibp.cpp b/src/gui/reports/ReportsWidgetHibp.cpp index a559208aaa..b97b9986c2 100644 --- a/src/gui/reports/ReportsWidgetHibp.cpp +++ b/src/gui/reports/ReportsWidgetHibp.cpp @@ -23,42 +23,17 @@ #include "core/Metadata.h" #include "gui/GuiTools.h" #include "gui/Icons.h" +#include "gui/reports/ProxyModels.h" #include #include -#include #include #include -namespace -{ - class ReportSortProxyModel : public QSortFilterProxyModel - { - public: - ReportSortProxyModel(QObject* parent) - : QSortFilterProxyModel(parent){}; - ~ReportSortProxyModel() override = default; - - protected: - bool lessThan(const QModelIndex& left, const QModelIndex& right) const override - { - // Sort count column by user data - if (left.column() == 2) { - return sourceModel()->data(left, Qt::UserRole).toInt() - < sourceModel()->data(right, Qt::UserRole).toInt(); - } - // Otherwise use default sorting - return QSortFilterProxyModel::lessThan(left, right); - } - }; -} // namespace - ReportsWidgetHibp::ReportsWidgetHibp(QWidget* parent) - : QWidget(parent) + : ReportsWidgetBase(parent, SortProxyModelKind::Hibp) , m_ui(new Ui::ReportsWidgetHibp()) - , m_referencesModel(new QStandardItemModel(this)) - , m_modelProxy(new ReportSortProxyModel(this)) { m_ui->setupUi(this); @@ -85,13 +60,6 @@ ReportsWidgetHibp::~ReportsWidgetHibp() = default; void ReportsWidgetHibp::loadSettings(QSharedPointer db) { - // Re-initialize - m_db = std::move(db); - m_referencesModel->clear(); - m_pwndPasswords.clear(); - m_error.clear(); - m_rowToEntry.clear(); - m_editedEntry = nullptr; #ifdef WITH_XC_NETWORKING m_ui->stackedWidget->setCurrentIndex(0); m_ui->validationButton->setEnabled(true); @@ -100,6 +68,9 @@ void ReportsWidgetHibp::loadSettings(QSharedPointer db) // Compiled without networking, can't do anything m_ui->stackedWidget->setCurrentIndex(2); #endif + + ReportsWidgetBase::loadSettings(db); + m_referencesModel->clear(); } /* @@ -153,13 +124,17 @@ void ReportsWidgetHibp::makeHibpTable() auto title = entry->title(); // Hide entry if excluded unless explicitly requested - if (entry->excludeFromReports()) { + if (entry->excludeFromReports() || entry->group()->excludeFromReports()) { anyExcluded = true; if (!showExcluded) { continue; } - title.append(tr(" (Excluded)")); + if (group->excludeFromReports()) { + title.append(tr(" (Group Excluded)")); + } else { + title.append(tr(" (Excluded)")); + } } auto row = QList(); @@ -169,6 +144,8 @@ void ReportsWidgetHibp::makeHibpTable() if (entry->excludeFromReports()) { row[1]->setToolTip(tr("This entry is being excluded from reports")); + } else if (entry->group()->excludeFromReports()) { + row[1]->setToolTip(tr("The group for this entry is being excluded from reports")); } row[2]->setForeground(red); @@ -176,7 +153,7 @@ void ReportsWidgetHibp::makeHibpTable() m_referencesModel->appendRow(row); // Store entry pointer per table row (used in double click handler) - m_rowToEntry.append(entry); + m_rowToEntry.append({group, entry}); } // If there was an error, append the error message to the table @@ -311,12 +288,12 @@ void ReportsWidgetHibp::emitEntryActivated(const QModelIndex& index) // Find which database entry was double-clicked auto mappedIndex = m_modelProxy->mapToSource(index); const auto entry = m_rowToEntry[mappedIndex.row()]; - if (entry) { + if (entry.second) { // Found it, invoke entry editor - m_editedEntry = entry; - m_editedPassword = entry->password(); - m_editedExcluded = entry->excludeFromReports(); - emit entryActivated(const_cast(entry)); + m_editedEntry = entry.second; + m_editedPassword = entry.second->password(); + m_editedExcluded = entry.second->excludeFromReports(); + emit entryActivated(const_cast(entry.second)); } } @@ -355,101 +332,23 @@ void ReportsWidgetHibp::refreshAfterEdit() void ReportsWidgetHibp::customMenuRequested(QPoint pos) { - auto selected = m_ui->hibpTableView->selectionModel()->selectedRows(); - if (selected.isEmpty()) { - return; - } - // Create the context menu - const auto menu = new QMenu(this); - - // Create the "edit entry" menu item if 1 row is selected - if (selected.size() == 1) { - const auto edit = new QAction(icons()->icon("entry-edit"), tr("Edit Entry…"), this); - menu->addAction(edit); - connect(edit, &QAction::triggered, edit, [this, selected] { - auto row = m_modelProxy->mapToSource(selected[0]).row(); - auto entry = m_rowToEntry[row]; - emit entryActivated(entry); - }); - } + const auto menu = customMenuRequestedBase(); - // Create the "Expire entry" menu item - const auto expEntry = new QAction(icons()->icon("entry-expire"), tr("Expire Entry(s)…", "", selected.size()), this); - menu->addAction(expEntry); - connect(expEntry, &QAction::triggered, this, &ReportsWidgetHibp::expireSelectedEntries); - - // Create the "delete entry" menu item - const auto delEntry = new QAction(icons()->icon("entry-delete"), tr("Delete Entry(s)…", "", selected.size()), this); - menu->addAction(delEntry); - connect(delEntry, &QAction::triggered, this, &ReportsWidgetHibp::deleteSelectedEntries); - - // Create the "exclude from reports" menu item - const auto exclude = new QAction(icons()->icon("reports-exclude"), tr("Exclude from reports"), this); - - bool isExcluded = false; - for (auto index : selected) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row]; - if (entry && entry->excludeFromReports()) { - // If at least one entry is excluded switch to inclusion - isExcluded = true; - break; - } + if (!menu) { + return; } - exclude->setCheckable(true); - exclude->setChecked(isExcluded); - - menu->addAction(exclude); - connect(exclude, &QAction::toggled, exclude, [this, selected](bool state) { - for (auto index : selected) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row]; - if (entry) { - entry->setExcludeFromReports(state); - } - } - makeHibpTable(); - }); // Show the context menu menu->popup(m_ui->hibpTableView->viewport()->mapToGlobal(pos)); } -QList ReportsWidgetHibp::getSelectedEntries() -{ - QList selectedEntries; - for (auto index : m_ui->hibpTableView->selectionModel()->selectedRows()) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row]; - if (entry) { - selectedEntries << entry; - } - } - return selectedEntries; -} - -void ReportsWidgetHibp::expireSelectedEntries() +void ReportsWidgetHibp::updateWidget() { - for (auto entry : getSelectedEntries()) { - entry->expireNow(); - } - - makeHibpTable(); -} - -void ReportsWidgetHibp::deleteSelectedEntries() -{ - QList selectedEntries = getSelectedEntries(); - bool permanent = !m_db->metadata()->recycleBinEnabled(); - if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) { - GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent); - } - makeHibpTable(); } -void ReportsWidgetHibp::saveSettings() +QTableView* ReportsWidgetHibp::getTableView() const { - // nothing to do - the tab is passive + return m_ui->hibpTableView; } diff --git a/src/gui/reports/ReportsWidgetHibp.h b/src/gui/reports/ReportsWidgetHibp.h index 8e0d5e47bc..2d59a090a5 100644 --- a/src/gui/reports/ReportsWidgetHibp.h +++ b/src/gui/reports/ReportsWidgetHibp.h @@ -20,6 +20,7 @@ #include "config-keepassx.h" #include "gui/entry/EntryModel.h" +#include "gui/reports/ReportsWidgetBase.h" #include @@ -32,25 +33,26 @@ class Entry; class Group; class QSortFilterProxyModel; class QStandardItemModel; +class QTableView; namespace Ui { class ReportsWidgetHibp; } -class ReportsWidgetHibp : public QWidget +class ReportsWidgetHibp : public ReportsWidgetBase { Q_OBJECT public: explicit ReportsWidgetHibp(QWidget* parent = nullptr); ~ReportsWidgetHibp() override; - void loadSettings(QSharedPointer db); - void saveSettings(); + void loadSettings(QSharedPointer db) override; void refreshAfterEdit(); -signals: - void entryActivated(Entry*); +protected: + void updateWidget() override; + QTableView* getTableView() const override; public slots: void emitEntryActivated(const QModelIndex&); @@ -58,22 +60,15 @@ public slots: void fetchFailed(const QString& error); void makeHibpTable(); void customMenuRequested(QPoint); - QList getSelectedEntries(); - void expireSelectedEntries(); - void deleteSelectedEntries(); private: void startValidation(); static QString countToText(int count); QScopedPointer m_ui; - QScopedPointer m_referencesModel; - QScopedPointer m_modelProxy; - QSharedPointer m_db; QMap m_pwndPasswords; // Passwords we found to have been pwned (value is pwn count) QString m_error; // Error message if download failed, else empty - QList m_rowToEntry; // List index is table row QPointer m_editedEntry; // The entry we're currently editing QString m_editedPassword; // The old password of the entry we're editing bool m_editedExcluded; // The old "known bad" flag of the entry we're editing diff --git a/src/gui/reports/ReportsWidgetPasskeys.cpp b/src/gui/reports/ReportsWidgetPasskeys.cpp index 831f4c7211..737c72f687 100644 --- a/src/gui/reports/ReportsWidgetPasskeys.cpp +++ b/src/gui/reports/ReportsWidgetPasskeys.cpp @@ -29,6 +29,7 @@ #include "gui/MessageBox.h" #include "gui/passkeys/PasskeyExporter.h" #include "gui/passkeys/PasskeyImporter.h" +#include "gui/reports/ProxyModels.h" #include "gui/styles/StateColorPalette.h" #include @@ -87,10 +88,8 @@ PasskeyList::PasskeyList(const QSharedPointer& db) } ReportsWidgetPasskeys::ReportsWidgetPasskeys(QWidget* parent) - : QWidget(parent) + : ReportsWidgetBase(parent, SortProxyModelKind::Default) , m_ui(new Ui::ReportsWidgetPasskeys()) - , m_referencesModel(new QStandardItemModel(this)) - , m_modelProxy(new QSortFilterProxyModel(this)) { m_ui->setupUi(this); @@ -146,25 +145,13 @@ void ReportsWidgetPasskeys::addPasskeyRow(Group* group, Entry* entry) m_rowToEntry.append({group, entry}); } -void ReportsWidgetPasskeys::loadSettings(QSharedPointer db) -{ - m_db = std::move(db); - m_entriesUpdated = false; - m_referencesModel->clear(); - m_rowToEntry.clear(); - - auto row = QList(); - row << new QStandardItem(tr("Please wait, list of entries with passkeys is being updated…")); - m_referencesModel->appendRow(row); -} - void ReportsWidgetPasskeys::showEvent(QShowEvent* event) { QWidget::showEvent(event); - if (!m_entriesUpdated) { + if (!m_widgetDataCalculated) { // Perform stats calculation on next event loop to allow widget to appear - m_entriesUpdated = true; + m_widgetDataCalculated = true; QTimer::singleShot(0, this, SLOT(updateEntries())); } } @@ -200,22 +187,6 @@ void ReportsWidgetPasskeys::updateEntries() m_ui->passkeysTableView->resizeColumnsToContents(); } -void ReportsWidgetPasskeys::emitEntryActivated(const QModelIndex& index) -{ - if (!index.isValid()) { - return; - } - - auto mappedIndex = m_modelProxy->mapToSource(index); - const auto row = m_rowToEntry[mappedIndex.row()]; - const auto group = row.first; - const auto entry = row.second; - - if (group && entry) { - emit entryActivated(entry); - } -} - void ReportsWidgetPasskeys::customMenuRequested(QPoint pos) { auto selected = m_ui->passkeysTableView->selectionModel()->selectedRows(); @@ -246,37 +217,6 @@ void ReportsWidgetPasskeys::customMenuRequested(QPoint pos) menu->popup(m_ui->passkeysTableView->viewport()->mapToGlobal(pos)); } -void ReportsWidgetPasskeys::saveSettings() -{ - // Nothing to do - the tab is passive -} - -void ReportsWidgetPasskeys::deleteSelectedEntries() -{ - auto selectedEntries = getSelectedEntries(); - bool permanent = !m_db->metadata()->recycleBinEnabled(); - - if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) { - GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent); - } - - updateEntries(); -} - -QList ReportsWidgetPasskeys::getSelectedEntries() -{ - QList selectedEntries; - for (auto index : m_ui->passkeysTableView->selectionModel()->selectedRows()) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row].second; - if (entry) { - selectedEntries << entry; - } - } - - return selectedEntries; -} - void ReportsWidgetPasskeys::selectionChanged() { m_ui->exportButton->setEnabled(!m_ui->passkeysTableView->selectionModel()->selectedIndexes().isEmpty()); @@ -305,3 +245,13 @@ void ReportsWidgetPasskeys::exportPasskey() PasskeyExporter passkeyExporter(this); passkeyExporter.showExportDialog(getSelectedEntries()); } + +QTableView* ReportsWidgetPasskeys::getTableView() const +{ + return m_ui->passkeysTableView; +} + +void ReportsWidgetPasskeys::updateWidget() +{ + updateEntries(); +} diff --git a/src/gui/reports/ReportsWidgetPasskeys.h b/src/gui/reports/ReportsWidgetPasskeys.h index 3d0593350c..30945bba60 100644 --- a/src/gui/reports/ReportsWidgetPasskeys.h +++ b/src/gui/reports/ReportsWidgetPasskeys.h @@ -19,6 +19,7 @@ #define KEEPASSXC_REPORTSWIDGETPASSKEYS_H #include "gui/entry/EntryModel.h" +#include "gui/reports/ReportsWidgetBase.h" #include class Database; @@ -33,27 +34,21 @@ namespace Ui class ReportsWidgetPasskeys; } -class ReportsWidgetPasskeys : public QWidget +class ReportsWidgetPasskeys : public ReportsWidgetBase { Q_OBJECT public: explicit ReportsWidgetPasskeys(QWidget* parent = nullptr); ~ReportsWidgetPasskeys() override; - void loadSettings(QSharedPointer db); - void saveSettings(); - protected: void showEvent(QShowEvent* event) override; - -signals: - void entryActivated(Entry*); + void updateWidget() override; + QTableView* getTableView() const override; public slots: void updateEntries(); - void emitEntryActivated(const QModelIndex& index); void customMenuRequested(QPoint); - void deleteSelectedEntries(); private slots: void selectionChanged(); @@ -62,15 +57,8 @@ private slots: private: void addPasskeyRow(Group*, Entry*); - QList getSelectedEntries(); QScopedPointer m_ui; - - bool m_entriesUpdated = false; - QScopedPointer m_referencesModel; - QScopedPointer m_modelProxy; - QSharedPointer m_db; - QList> m_rowToEntry; }; #endif // KEEPASSXC_REPORTSWIDGETPASSKEYS_H diff --git a/src/gui/reports/ReportsWidgetStatistics.cpp b/src/gui/reports/ReportsWidgetStatistics.cpp index 0aced6ae5d..ff0b305ad0 100644 --- a/src/gui/reports/ReportsWidgetStatistics.cpp +++ b/src/gui/reports/ReportsWidgetStatistics.cpp @@ -121,6 +121,11 @@ void ReportsWidgetStatistics::calculateStats() stats->excludedEntries > 0, tr("Excluding entries from reports, e. g. because they are known to have a poor password, isn't " "necessarily a problem but you should keep an eye on them.")); + addStatsRow(tr("Groups excluded from reports"), + QString::number(stats->excludedGroups), + stats->excludedGroups > 0, + tr("Excluding entire groups from reports isn't necessarily a problem but please exercise caution " + "when excluding entire groups.")); addStatsRow(tr("Average password length"), tr("%1 character(s)", "", stats->averagePwdLength()).arg(stats->averagePwdLength()), stats->isAvgPwdTooShort(), diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index adf3593247..fe051fab7d 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -62,6 +62,8 @@ #include "gui/group/GroupModel.h" #include "gui/group/GroupView.h" #include "gui/remote/RemoteHandler.h" +#include "gui/reports/ReportsDialog.h" +#include "gui/reports/ReportsWidgetHealthcheck.h" #include "gui/tag/TagsEdit.h" #include "gui/wizard/NewDatabaseWizard.h" #include "keys/FileKey.h" @@ -2483,6 +2485,349 @@ void TestGui::testMenuActionStates() QVERIFY(isActionEnabled("actionPasswordGenerator")); } +void TestGui::testDatabaseReports() +{ + addGroup("Finance"); + addGroup("Entertainment"); + + addEntry("Finance", "Chase", "user1", "password"); + addEntry("Finance", "Amex", "user1", "password123"); + addEntry("Finance", "Capital One", "user1", "password456"); + + addEntry("Entertainment", "Netflix", "user1", "password"); + addEntry("Entertainment", "Hulu", "user1", "password321"); + addEntry("Entertainment", "Apple TV", "user1", "password123"); + + Group* entertainmentGroup = m_dbWidget->currentGroup()->findChildByName("Entertainment"); + QCOMPARE(entertainmentGroup->entries().size(), 3); + + Group* financeGroup = m_dbWidget->currentGroup()->findChildByName("Finance"); + QCOMPARE(financeGroup->entries().size(), 3); + + auto* actionReports = m_mainWindow->findChild("actionReports"); + QVERIFY(actionReports->isEnabled()); + + auto* toolBar = m_mainWindow->findChild("toolBar"); + QVERIFY(toolBar); + + QWidget* actionReportsWidget = toolBar->widgetForAction(actionReports); + QVERIFY(actionReportsWidget); + QVERIFY(actionReportsWidget->isVisible()); + QVERIFY(actionReportsWidget->isEnabled()); + + QTest::mouseClick(actionReportsWidget, Qt::LeftButton); + + auto* reportsDialog = m_dbWidget->findChild("reportsDialog"); + QVERIFY(reportsDialog); + + CategoryListWidget* categoryList = reportsDialog->findChild("categoryList"); + categoryList->setCurrentCategory(1); + + QStackedWidget* stackedWidget = reportsDialog->findChild("stackedWidget"); + QVERIFY(stackedWidget); + stackedWidget->setCurrentIndex(1); + + ReportsWidgetHealthcheck* healthCheckWidget = reportsDialog->findChild(); + QVERIFY(healthCheckWidget); + + QTest::mouseClick(healthCheckWidget, Qt::LeftButton); + QTableView* healthTable = healthCheckWidget->findChild("healthcheckTableView"); + QVERIFY(healthTable); + + QSignalSpy healthCheckWidgetSpy(healthCheckWidget, &ReportsWidgetHealthcheck::tablePopulated); + + QAbstractItemModel* healthModel = healthTable->model(); + QVERIFY(healthModel); + + QTRY_COMPARE(healthCheckWidgetSpy.count(), 1); + QCOMPARE(healthModel->rowCount(), 8); // account for 2 existing passwords at the start of each test case + + auto* reportsDialogButtonBox = reportsDialog->findChild("buttonBox"); + QTest::mouseClick(reportsDialogButtonBox->button(QDialogButtonBox::Close), Qt::LeftButton); + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); +} + +void TestGui::testExcludedGroupEntryInReports() +{ + addGroup("Finance"); + addGroup("Entertainment"); + + // Use bad passwords to make sure they all show up in health report + addEntry("Finance", "Chase", "user1", "password"); + addEntry("Finance", "Amex", "user1", "password123"); + addEntry("Finance", "Capital One", "user1", "password456"); + + addEntry("Entertainment", "Netflix", "user1", "password"); + addEntry("Entertainment", "Hulu", "user1", "password321"); + addEntry("Entertainment", "Apple TV", "user1", "password123"); + + Group* entertainmentGroup = m_dbWidget->currentGroup()->findChildByName("Entertainment"); + m_dbWidget->groupView()->setCurrentGroup(entertainmentGroup); + + auto* toolBar = m_mainWindow->findChild("toolBar"); + QVERIFY(toolBar); + + auto* editGroupAction = m_mainWindow->findChild("actionGroupEdit"); + QVERIFY(editGroupAction->isEnabled()); + triggerAction("actionGroupEdit"); + + auto* editGroupWidget = m_dbWidget->findChild("editGroupWidget"); + QVERIFY(editGroupWidget); + + // Bring up group edit page + QTest::mouseClick(editGroupWidget, Qt::LeftButton); + + QLineEdit* nameEdit = editGroupWidget->findChild("editName"); + QCOMPARE(nameEdit->text(), QString("Entertainment")); + + // Find database report exclusion checkbox and check it + QCheckBox* excludeGroupFromReportsCheckbox = editGroupWidget->findChild("excludeReportsCheckBox"); + QVERIFY(excludeGroupFromReportsCheckbox); + + excludeGroupFromReportsCheckbox->setChecked(true); + + auto* editGroupWidgetButtonBox = editGroupWidget->findChild("buttonBox"); + QVERIFY(editGroupWidgetButtonBox); + + // Apply and go back to main view + QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Apply), Qt::LeftButton); + QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); + + QVERIFY(entertainmentGroup->excludeFromReports()); + + // Verify they don't show up in the report + auto* actionReports = m_mainWindow->findChild("actionReports"); + QVERIFY(actionReports->isEnabled()); + + QWidget* actionReportsWidget = toolBar->widgetForAction(actionReports); + QVERIFY(actionReportsWidget); + QVERIFY(actionReportsWidget->isVisible()); + QVERIFY(actionReportsWidget->isEnabled()); + + QTest::mouseClick(actionReportsWidget, Qt::LeftButton); + + auto* reportsDialog = m_dbWidget->findChild("reportsDialog"); + QVERIFY(reportsDialog); + + CategoryListWidget* categoryList = reportsDialog->findChild("categoryList"); + categoryList->setCurrentCategory(1); + + QStackedWidget* stackedWidget = reportsDialog->findChild("stackedWidget"); + QVERIFY(stackedWidget); + stackedWidget->setCurrentIndex(1); + + ReportsWidgetHealthcheck* healthCheckWidget = reportsDialog->findChild(); + QVERIFY(healthCheckWidget); + + QTest::mouseClick(healthCheckWidget, Qt::LeftButton); + QTableView* healthTable = healthCheckWidget->findChild("healthcheckTableView"); + QVERIFY(healthTable); + + QSignalSpy healthCheckWidgetSpy(healthCheckWidget, &ReportsWidgetHealthcheck::tablePopulated); + + QAbstractItemModel* healthModel = healthTable->model(); + QVERIFY(healthModel); + + // There should be 3 showing + QTRY_COMPARE(healthCheckWidgetSpy.count(), 1); + QCOMPARE(healthModel->rowCount(), 5); // account for 2 existing passwords at the start of each test case + + QCheckBox* showExcludedCheckBox = healthCheckWidget->findChild("showExcluded"); + QVERIFY(showExcludedCheckBox); + QCOMPARE(showExcludedCheckBox->isChecked(), false); + + showExcludedCheckBox->click(); + QVERIFY(showExcludedCheckBox->isChecked()); + QTRY_COMPARE(healthCheckWidgetSpy.count(), 2); + + healthModel = healthTable->model(); + QCOMPARE(healthModel->rowCount(), 8); // account for 2 existing passwords at the start of each test case + + for (int i = 0; i < healthModel->rowCount(); ++i) { + QModelIndex index = healthModel->index(i, 1); + QVariant data = healthModel->data(index); + + if (data.toString().contains("Netflix")) { + auto rect = healthTable->visualRect(index); + auto centerPoint = rect.center(); + QTest::mouseClick(healthTable->viewport(), Qt::LeftButton, Qt::NoModifier, centerPoint); + healthCheckWidget->customMenuRequested(centerPoint); + + QMenu* menu = healthCheckWidget->findChild("customMenu"); + QVERIFY(menu); + QAction* excludeEntryAction = healthCheckWidget->findChild("contextMenuExcludeAction"); + QVERIFY(excludeEntryAction); + MessageBox::setNextAnswer(MessageBox::No); + + excludeEntryAction->trigger(); + QApplication::processEvents(); + break; + } + } + + QTRY_COMPARE(healthCheckWidgetSpy.count(), 3); + + for (int i = 0; i < healthModel->rowCount(); ++i) { + QModelIndex index = healthModel->index(i, 1); + QVariant data = healthModel->data(index); + + if (data.toString().contains("Netflix")) { + QVERIFY(!data.toString().contains("(Group Excluded)")); + break; + } + } + + showExcludedCheckBox->click(); + QVERIFY(!showExcludedCheckBox->isChecked()); + QTRY_COMPARE(healthCheckWidgetSpy.count(), 4); + + QCOMPARE(healthModel->rowCount(), 6); // 2 existing passwords from start, 3 from Finance, 1 from Entertainment + + auto* reportsDialogButtonBox = reportsDialog->findChild("buttonBox"); + QTest::mouseClick(reportsDialogButtonBox->button(QDialogButtonBox::Close), Qt::LeftButton); + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); +} + +void TestGui::testExcludedGroupReincludeAllEntries() +{ + addGroup("Finance"); + addGroup("Entertainment"); + + // Use bad passwords to make sure they all show up in health report + addEntry("Finance", "Chase", "user1", "password"); + addEntry("Finance", "Amex", "user1", "password123"); + addEntry("Finance", "Capital One", "user1", "password456"); + + addEntry("Entertainment", "Netflix", "user1", "password"); + addEntry("Entertainment", "Hulu", "user1", "password321"); + addEntry("Entertainment", "Apple TV", "user1", "password123"); + + Group* entertainmentGroup = m_dbWidget->currentGroup()->findChildByName("Entertainment"); + m_dbWidget->groupView()->setCurrentGroup(entertainmentGroup); + + auto* toolBar = m_mainWindow->findChild("toolBar"); + QVERIFY(toolBar); + + auto* editGroupAction = m_mainWindow->findChild("actionGroupEdit"); + QVERIFY(editGroupAction->isEnabled()); + triggerAction("actionGroupEdit"); + + auto* editGroupWidget = m_dbWidget->findChild("editGroupWidget"); + QVERIFY(editGroupWidget); + + // Bring up group edit page + QTest::mouseClick(editGroupWidget, Qt::LeftButton); + + QLineEdit* nameEdit = editGroupWidget->findChild("editName"); + QCOMPARE(nameEdit->text(), QString("Entertainment")); + + // Find database report exclusion checkbox and check it + QCheckBox* excludeGroupFromReportsCheckbox = editGroupWidget->findChild("excludeReportsCheckBox"); + QVERIFY(excludeGroupFromReportsCheckbox); + + excludeGroupFromReportsCheckbox->setChecked(true); + + auto* editGroupWidgetButtonBox = editGroupWidget->findChild("buttonBox"); + QVERIFY(editGroupWidgetButtonBox); + + // Apply and go back to main view + QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Apply), Qt::LeftButton); + QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); + + QVERIFY(entertainmentGroup->excludeFromReports()); + + // Verify they don't show up in the report + auto* actionReports = m_mainWindow->findChild("actionReports"); + QVERIFY(actionReports->isEnabled()); + + QWidget* actionReportsWidget = toolBar->widgetForAction(actionReports); + QVERIFY(actionReportsWidget); + QVERIFY(actionReportsWidget->isVisible()); + QVERIFY(actionReportsWidget->isEnabled()); + + QTest::mouseClick(actionReportsWidget, Qt::LeftButton); + + auto* reportsDialog = m_dbWidget->findChild("reportsDialog"); + QVERIFY(reportsDialog); + + CategoryListWidget* categoryList = reportsDialog->findChild("categoryList"); + categoryList->setCurrentCategory(1); + + QStackedWidget* stackedWidget = reportsDialog->findChild("stackedWidget"); + QVERIFY(stackedWidget); + stackedWidget->setCurrentIndex(1); + + ReportsWidgetHealthcheck* healthCheckWidget = reportsDialog->findChild(); + QVERIFY(healthCheckWidget); + + QTest::mouseClick(healthCheckWidget, Qt::LeftButton); + QTableView* healthTable = healthCheckWidget->findChild("healthcheckTableView"); + QVERIFY(healthTable); + + QSignalSpy healthCheckWidgetSpy(healthCheckWidget, &ReportsWidgetHealthcheck::tablePopulated); + + QAbstractItemModel* healthModel = healthTable->model(); + QVERIFY(healthModel); + + // There should be 3 showing + QTRY_COMPARE(healthCheckWidgetSpy.count(), 1); + QCOMPARE(healthModel->rowCount(), 5); // account for 2 existing passwords at the start of each test case + + QCheckBox* showExcludedCheckBox = healthCheckWidget->findChild("showExcluded"); + QVERIFY(showExcludedCheckBox); + QCOMPARE(showExcludedCheckBox->isChecked(), false); + + showExcludedCheckBox->click(); + QVERIFY(showExcludedCheckBox->isChecked()); + QTRY_COMPARE(healthCheckWidgetSpy.count(), 2); + + healthModel = healthTable->model(); + QCOMPARE(healthModel->rowCount(), 8); // account for 2 existing passwords at the start of each test case + + for (int i = 0; i < healthModel->rowCount(); ++i) { + QModelIndex index = healthModel->index(i, 1); + QVariant data = healthModel->data(index); + + if (data.toString().contains("Netflix")) { + auto rect = healthTable->visualRect(index); + auto centerPoint = rect.center(); + QTest::mouseClick(healthTable->viewport(), Qt::LeftButton, Qt::NoModifier, centerPoint); + healthCheckWidget->customMenuRequested(centerPoint); + + QMenu* menu = healthCheckWidget->findChild("customMenu"); + QVERIFY(menu); + QAction* excludeEntryAction = healthCheckWidget->findChild("contextMenuExcludeAction"); + QVERIFY(excludeEntryAction); + MessageBox::setNextAnswer(MessageBox::Yes); + + excludeEntryAction->trigger(); + QApplication::processEvents(); + break; + } + } + + QTRY_COMPARE(healthCheckWidgetSpy.count(), 3); + + for (int i = 0; i < healthModel->rowCount(); ++i) { + QModelIndex index = healthModel->index(i, 1); + QVariant data = healthModel->data(index); + + QVERIFY(!data.toString().contains("(Group Excluded)")); + } + + showExcludedCheckBox->click(); + QVERIFY(!showExcludedCheckBox->isChecked()); + QTRY_COMPARE(healthCheckWidgetSpy.count(), 4); + + QCOMPARE(healthModel->rowCount(), 8); // 2 existing passwords from start, 3 from Finance, 3 from Entertainment + + auto* reportsDialogButtonBox = reportsDialog->findChild("buttonBox"); + QTest::mouseClick(reportsDialogButtonBox->button(QDialogButtonBox::Close), Qt::LeftButton); + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); +} + void TestGui::addCannedEntries() { // Find buttons @@ -2513,6 +2858,51 @@ void TestGui::addCannedEntries() QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); } +void TestGui::addGroup(const QString& name) +{ + // Find buttons for group creation + auto* editGroupWidget = m_dbWidget->findChild("editGroupWidget"); + auto* nameEdit = editGroupWidget->findChild("editName"); + auto* editGroupWidgetButtonBox = editGroupWidget->findChild("buttonBox"); + + // Add group with specified name + Group* rootGroup = m_db->rootGroup(); + m_dbWidget->groupView()->setCurrentGroup(rootGroup); // Add group on root level + m_dbWidget->createGroup(); + QTest::keyClicks(nameEdit, name); + QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + m_dbWidget->groupView()->setCurrentGroup(rootGroup); // Reset to root level +} + +void TestGui::addEntry(const QString& groupName, const QString& title, const QString& username, const QString& password) +{ + // Find buttons + auto* toolBar = m_mainWindow->findChild("toolBar"); + QWidget* entryNewWidget = toolBar->widgetForAction(m_mainWindow->findChild("actionEntryNew")); + auto* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); + auto* titleEdit = editEntryWidget->findChild("titleEdit"); + auto* usernameComboBox = editEntryWidget->findChild("usernameComboBox"); + auto* passwordEdit = + editEntryWidget->findChild("passwordEdit")->findChild("passwordEdit"); + auto* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox"); + + // Add entry to specified group + QVERIFY(m_dbWidget->currentGroup()); + Group* group = m_dbWidget->currentGroup()->findChildByName(groupName); + m_dbWidget->groupView()->setCurrentGroup(group); + + QTest::mouseClick(entryNewWidget, Qt::LeftButton); + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode); + + QTest::keyClicks(titleEdit, title); + QTest::keyClicks(usernameComboBox, username); + QTest::keyClicks(passwordEdit, password); + QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); + m_dbWidget->groupView()->setCurrentGroup(m_db->rootGroup()); +} + void TestGui::checkDatabase(const QString& filePath, const QString& expectedDbName) { auto key = QSharedPointer::create(); diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h index 514f7ce95b..ef6c3d61bd 100644 --- a/tests/gui/TestGui.h +++ b/tests/gui/TestGui.h @@ -71,9 +71,14 @@ private slots: void testTrayRestoreHide(); void testShortcutConfig(); void testMenuActionStates(); + void testDatabaseReports(); + void testExcludedGroupEntryInReports(); + void testExcludedGroupReincludeAllEntries(); private: void addCannedEntries(); + void addGroup(const QString& name); + void addEntry(const QString& groupName, const QString& title, const QString& username, const QString& password); void checkDatabase(const QString& filePath, const QString& expectedDbName); void checkDatabase(const QString& filePath = {}); void triggerAction(const QString& name);