mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-01-14 08:50:08 -05:00
Add a context menu entry to delete entries from health check reports (#6537)
* Closes #4986 - Allow deleting entries from the reports view * Closes #4533 - Exclude & delete multiple entries in a report * Also allow deleting selected entries using the delete key * Introduce GuiTools namespace to collect shared GUI prompts and actions * Add functionality to HIBP report to mirror health check report Co-authored-by: Jonathan White <support@dmapps.us>
This commit is contained in:
parent
1f50ac6045
commit
6acd0b25ae
@ -114,6 +114,7 @@ set(keepassx_SOURCES
|
|||||||
gui/EditWidgetProperties.cpp
|
gui/EditWidgetProperties.cpp
|
||||||
gui/FileDialog.cpp
|
gui/FileDialog.cpp
|
||||||
gui/Font.cpp
|
gui/Font.cpp
|
||||||
|
gui/GuiTools.cpp
|
||||||
gui/IconModels.cpp
|
gui/IconModels.cpp
|
||||||
gui/KeePass1OpenWidget.cpp
|
gui/KeePass1OpenWidget.cpp
|
||||||
gui/KMessageWidget.cpp
|
gui/KMessageWidget.cpp
|
||||||
|
@ -50,6 +50,7 @@
|
|||||||
#include "gui/DatabaseOpenWidget.h"
|
#include "gui/DatabaseOpenWidget.h"
|
||||||
#include "gui/EntryPreviewWidget.h"
|
#include "gui/EntryPreviewWidget.h"
|
||||||
#include "gui/FileDialog.h"
|
#include "gui/FileDialog.h"
|
||||||
|
#include "gui/GuiTools.h"
|
||||||
#include "gui/KeePass1OpenWidget.h"
|
#include "gui/KeePass1OpenWidget.h"
|
||||||
#include "gui/MainWindow.h"
|
#include "gui/MainWindow.h"
|
||||||
#include "gui/MessageBox.h"
|
#include "gui/MessageBox.h"
|
||||||
@ -487,57 +488,11 @@ void DatabaseWidget::deleteEntries(QList<Entry*> selectedEntries, bool confirm)
|
|||||||
bool permanent = (recycleBin && recycleBin->findEntryByUuid(selectedEntries.first()->uuid()))
|
bool permanent = (recycleBin && recycleBin->findEntryByUuid(selectedEntries.first()->uuid()))
|
||||||
|| !m_db->metadata()->recycleBinEnabled();
|
|| !m_db->metadata()->recycleBinEnabled();
|
||||||
|
|
||||||
if (confirm && !confirmDeleteEntries(selectedEntries, permanent)) {
|
if (confirm && !GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find references to selected entries and prompt for direction if necessary
|
GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent);
|
||||||
auto it = selectedEntries.begin();
|
|
||||||
while (confirm && it != selectedEntries.end()) {
|
|
||||||
auto references = m_db->rootGroup()->referencesRecursive(*it);
|
|
||||||
if (!references.isEmpty()) {
|
|
||||||
// Ignore references that are selected for deletion
|
|
||||||
for (auto* entry : selectedEntries) {
|
|
||||||
references.removeAll(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!references.isEmpty()) {
|
|
||||||
// Prompt for reference handling
|
|
||||||
auto result = MessageBox::question(
|
|
||||||
this,
|
|
||||||
tr("Replace references to entry?"),
|
|
||||||
tr("Entry \"%1\" has %2 reference(s). "
|
|
||||||
"Do you want to overwrite references with values, skip this entry, or delete anyway?",
|
|
||||||
"",
|
|
||||||
references.size())
|
|
||||||
.arg((*it)->title().toHtmlEscaped())
|
|
||||||
.arg(references.size()),
|
|
||||||
MessageBox::Overwrite | MessageBox::Skip | MessageBox::Delete,
|
|
||||||
MessageBox::Overwrite);
|
|
||||||
|
|
||||||
if (result == MessageBox::Overwrite) {
|
|
||||||
for (auto* entry : references) {
|
|
||||||
entry->replaceReferencesWithValues(*it);
|
|
||||||
}
|
|
||||||
} else if (result == MessageBox::Skip) {
|
|
||||||
it = selectedEntries.erase(it);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
it++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (permanent) {
|
|
||||||
for (auto* entry : asConst(selectedEntries)) {
|
|
||||||
delete entry;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (auto* entry : asConst(selectedEntries)) {
|
|
||||||
m_db->recycleEntry(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshSearch();
|
refreshSearch();
|
||||||
|
|
||||||
@ -550,49 +505,6 @@ void DatabaseWidget::deleteEntries(QList<Entry*> selectedEntries, bool confirm)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DatabaseWidget::confirmDeleteEntries(QList<Entry*> entries, bool permanent)
|
|
||||||
{
|
|
||||||
if (entries.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (permanent) {
|
|
||||||
QString prompt;
|
|
||||||
if (entries.size() == 1) {
|
|
||||||
prompt = tr("Do you really want to delete the entry \"%1\" for good?")
|
|
||||||
.arg(entries.first()->title().toHtmlEscaped());
|
|
||||||
} else {
|
|
||||||
prompt = tr("Do you really want to delete %n entry(s) for good?", "", entries.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
auto answer = MessageBox::question(this,
|
|
||||||
tr("Delete entry(s)?", "", entries.size()),
|
|
||||||
prompt,
|
|
||||||
MessageBox::Delete | MessageBox::Cancel,
|
|
||||||
MessageBox::Cancel);
|
|
||||||
|
|
||||||
return answer == MessageBox::Delete;
|
|
||||||
} else if (config()->get(Config::Security_NoConfirmMoveEntryToRecycleBin).toBool()) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
QString prompt;
|
|
||||||
if (entries.size() == 1) {
|
|
||||||
prompt = tr("Do you really want to move entry \"%1\" to the recycle bin?")
|
|
||||||
.arg(entries.first()->title().toHtmlEscaped());
|
|
||||||
} else {
|
|
||||||
prompt = tr("Do you really want to move %n entry(s) to the recycle bin?", "", entries.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
auto answer = MessageBox::question(this,
|
|
||||||
tr("Move entry(s) to recycle bin?", "", entries.size()),
|
|
||||||
prompt,
|
|
||||||
MessageBox::Move | MessageBox::Cancel,
|
|
||||||
MessageBox::Cancel);
|
|
||||||
|
|
||||||
return answer == MessageBox::Move;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DatabaseWidget::setFocus(Qt::FocusReason reason)
|
void DatabaseWidget::setFocus(Qt::FocusReason reason)
|
||||||
{
|
{
|
||||||
if (reason == Qt::BacktabFocusReason) {
|
if (reason == Qt::BacktabFocusReason) {
|
||||||
|
@ -74,7 +74,7 @@ public:
|
|||||||
|
|
||||||
explicit DatabaseWidget(QSharedPointer<Database> db, QWidget* parent = nullptr);
|
explicit DatabaseWidget(QSharedPointer<Database> db, QWidget* parent = nullptr);
|
||||||
explicit DatabaseWidget(const QString& filePath, QWidget* parent = nullptr);
|
explicit DatabaseWidget(const QString& filePath, QWidget* parent = nullptr);
|
||||||
~DatabaseWidget();
|
~DatabaseWidget() override;
|
||||||
|
|
||||||
void setFocus(Qt::FocusReason reason);
|
void setFocus(Qt::FocusReason reason);
|
||||||
|
|
||||||
@ -255,7 +255,6 @@ private:
|
|||||||
void setClipboardTextAndMinimize(const QString& text);
|
void setClipboardTextAndMinimize(const QString& text);
|
||||||
void processAutoOpen();
|
void processAutoOpen();
|
||||||
void openDatabaseFromEntry(const Entry* entry, bool inBackground = true);
|
void openDatabaseFromEntry(const Entry* entry, bool inBackground = true);
|
||||||
bool confirmDeleteEntries(QList<Entry*> entries, bool permanent);
|
|
||||||
void performIconDownloads(const QList<Entry*>& entries, bool force = false);
|
void performIconDownloads(const QList<Entry*>& entries, bool force = false);
|
||||||
bool performSave(QString& errorMessage, const QString& fileName = {});
|
bool performSave(QString& errorMessage, const QString& fileName = {});
|
||||||
|
|
||||||
|
122
src/gui/GuiTools.cpp
Normal file
122
src/gui/GuiTools.cpp
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
/*
|
||||||
|
* 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 "GuiTools.h"
|
||||||
|
|
||||||
|
#include "core/Config.h"
|
||||||
|
#include "core/Database.h"
|
||||||
|
#include "core/Entry.h"
|
||||||
|
#include "core/Group.h"
|
||||||
|
#include "gui/MessageBox.h"
|
||||||
|
|
||||||
|
namespace GuiTools
|
||||||
|
{
|
||||||
|
bool confirmDeleteEntries(QWidget* parent, const QList<Entry*>& entries, bool permanent)
|
||||||
|
{
|
||||||
|
if (!parent || entries.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permanent) {
|
||||||
|
QString prompt;
|
||||||
|
if (entries.size() == 1) {
|
||||||
|
prompt = QObject::tr("Do you really want to delete the entry \"%1\" for good?")
|
||||||
|
.arg(entries.first()->title().toHtmlEscaped());
|
||||||
|
} else {
|
||||||
|
prompt = QObject::tr("Do you really want to delete %n entry(s) for good?", "", entries.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto answer = MessageBox::question(parent,
|
||||||
|
QObject::tr("Delete entry(s)?", "", entries.size()),
|
||||||
|
prompt,
|
||||||
|
MessageBox::Delete | MessageBox::Cancel,
|
||||||
|
MessageBox::Cancel);
|
||||||
|
|
||||||
|
return answer == MessageBox::Delete;
|
||||||
|
} else if (config()->get(Config::Security_NoConfirmMoveEntryToRecycleBin).toBool()) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
QString prompt;
|
||||||
|
if (entries.size() == 1) {
|
||||||
|
prompt = QObject::tr("Do you really want to move entry \"%1\" to the recycle bin?")
|
||||||
|
.arg(entries.first()->title().toHtmlEscaped());
|
||||||
|
} else {
|
||||||
|
prompt = QObject::tr("Do you really want to move %n entry(s) to the recycle bin?", "", entries.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto answer = MessageBox::question(parent,
|
||||||
|
QObject::tr("Move entry(s) to recycle bin?", "", entries.size()),
|
||||||
|
prompt,
|
||||||
|
MessageBox::Move | MessageBox::Cancel,
|
||||||
|
MessageBox::Cancel);
|
||||||
|
|
||||||
|
return answer == MessageBox::Move;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteEntriesResolveReferences(QWidget* parent, const QList<Entry*>& entries, bool permanent)
|
||||||
|
{
|
||||||
|
if (!parent || entries.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<Entry*> selectedEntries;
|
||||||
|
// Find references to entries and prompt for direction if necessary
|
||||||
|
for (auto entry : entries) {
|
||||||
|
if (permanent) {
|
||||||
|
auto references = entry->database()->rootGroup()->referencesRecursive(entry);
|
||||||
|
if (!references.isEmpty()) {
|
||||||
|
// Ignore references that are part of this cohort
|
||||||
|
for (auto e : entries) {
|
||||||
|
references.removeAll(e);
|
||||||
|
}
|
||||||
|
// Prompt the user on what to do with the reference (Overwrite, Delete, Skip)
|
||||||
|
auto result = MessageBox::question(
|
||||||
|
parent,
|
||||||
|
QObject::tr("Replace references to entry?"),
|
||||||
|
QObject::tr(
|
||||||
|
"Entry \"%1\" has %2 reference(s). "
|
||||||
|
"Do you want to overwrite references with values, skip this entry, or delete anyway?",
|
||||||
|
"",
|
||||||
|
references.size())
|
||||||
|
.arg(entry->resolvePlaceholder(entry->title()).toHtmlEscaped())
|
||||||
|
.arg(references.size()),
|
||||||
|
MessageBox::Overwrite | MessageBox::Skip | MessageBox::Delete,
|
||||||
|
MessageBox::Overwrite);
|
||||||
|
|
||||||
|
if (result == MessageBox::Overwrite) {
|
||||||
|
for (auto ref : references) {
|
||||||
|
ref->replaceReferencesWithValues(entry);
|
||||||
|
}
|
||||||
|
} else if (result == MessageBox::Skip) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Marked for deletion
|
||||||
|
selectedEntries << entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto entry : asConst(selectedEntries)) {
|
||||||
|
if (permanent) {
|
||||||
|
delete entry;
|
||||||
|
} else {
|
||||||
|
entry->database()->recycleEntry(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace GuiTools
|
31
src/gui/GuiTools.h
Normal file
31
src/gui/GuiTools.h
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* 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_GUITOOLS_H
|
||||||
|
#define KEEPASSXC_GUITOOLS_H
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
class Entry;
|
||||||
|
|
||||||
|
namespace GuiTools
|
||||||
|
{
|
||||||
|
bool confirmDeleteEntries(QWidget* parent, const QList<Entry*>& entries, bool permanent);
|
||||||
|
void deleteEntriesResolveReferences(QWidget* parent, const QList<Entry*>& entries, bool permanent);
|
||||||
|
} // namespace GuiTools
|
||||||
|
#endif // KEEPASSXC_GUITOOLS_H
|
@ -22,12 +22,15 @@
|
|||||||
#include "core/Database.h"
|
#include "core/Database.h"
|
||||||
#include "core/Global.h"
|
#include "core/Global.h"
|
||||||
#include "core/Group.h"
|
#include "core/Group.h"
|
||||||
|
#include "core/Metadata.h"
|
||||||
#include "core/PasswordHealth.h"
|
#include "core/PasswordHealth.h"
|
||||||
|
#include "gui/GuiTools.h"
|
||||||
#include "gui/Icons.h"
|
#include "gui/Icons.h"
|
||||||
#include "gui/styles/StateColorPalette.h"
|
#include "gui/styles/StateColorPalette.h"
|
||||||
|
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
|
#include <QShortcut>
|
||||||
#include <QSortFilterProxyModel>
|
#include <QSortFilterProxyModel>
|
||||||
#include <QStandardItemModel>
|
#include <QStandardItemModel>
|
||||||
|
|
||||||
@ -38,12 +41,12 @@ namespace
|
|||||||
public:
|
public:
|
||||||
struct Item
|
struct Item
|
||||||
{
|
{
|
||||||
QPointer<const Group> group;
|
QPointer<Group> group;
|
||||||
QPointer<const Entry> entry;
|
QPointer<Entry> entry;
|
||||||
QSharedPointer<PasswordHealth> health;
|
QSharedPointer<PasswordHealth> health;
|
||||||
bool exclude = false;
|
bool exclude = false;
|
||||||
|
|
||||||
Item(const Group* g, const Entry* e, QSharedPointer<PasswordHealth> h)
|
Item(Group* g, Entry* e, QSharedPointer<PasswordHealth> h)
|
||||||
: group(g)
|
: group(g)
|
||||||
, entry(e)
|
, entry(e)
|
||||||
, health(h)
|
, health(h)
|
||||||
@ -102,13 +105,13 @@ Health::Health(QSharedPointer<Database> db)
|
|||||||
: m_db(db)
|
: m_db(db)
|
||||||
, m_checker(db)
|
, m_checker(db)
|
||||||
{
|
{
|
||||||
for (const auto* group : db->rootGroup()->groupsRecursive(true)) {
|
for (auto group : db->rootGroup()->groupsRecursive(true)) {
|
||||||
// Skip recycle bin
|
// Skip recycle bin
|
||||||
if (group->isRecycled()) {
|
if (group->isRecycled()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto* entry : group->entries()) {
|
for (auto entry : group->entries()) {
|
||||||
if (entry->isRecycled()) {
|
if (entry->isRecycled()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -147,16 +150,15 @@ ReportsWidgetHealthcheck::ReportsWidgetHealthcheck(QWidget* parent)
|
|||||||
m_modelProxy->setSourceModel(m_referencesModel.data());
|
m_modelProxy->setSourceModel(m_referencesModel.data());
|
||||||
m_modelProxy->setSortLocaleAware(true);
|
m_modelProxy->setSortLocaleAware(true);
|
||||||
m_ui->healthcheckTableView->setModel(m_modelProxy.data());
|
m_ui->healthcheckTableView->setModel(m_modelProxy.data());
|
||||||
m_ui->healthcheckTableView->setSelectionMode(QAbstractItemView::NoSelection);
|
|
||||||
m_ui->healthcheckTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
|
m_ui->healthcheckTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
|
||||||
m_ui->healthcheckTableView->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
m_ui->healthcheckTableView->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||||
m_ui->healthcheckTableView->setSortingEnabled(true);
|
|
||||||
m_ui->healthcheckTableView->setWordWrap(true);
|
|
||||||
|
|
||||||
connect(m_ui->healthcheckTableView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customMenuRequested(QPoint)));
|
connect(m_ui->healthcheckTableView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customMenuRequested(QPoint)));
|
||||||
connect(m_ui->healthcheckTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex)));
|
connect(m_ui->healthcheckTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex)));
|
||||||
connect(m_ui->showKnownBadCheckBox, SIGNAL(stateChanged(int)), this, SLOT(calculateHealth()));
|
connect(m_ui->showKnownBadCheckBox, SIGNAL(stateChanged(int)), this, SLOT(calculateHealth()));
|
||||||
connect(m_ui->excludeExpired, SIGNAL(stateChanged(int)), this, SLOT(calculateHealth()));
|
connect(m_ui->excludeExpired, SIGNAL(stateChanged(int)), this, SLOT(calculateHealth()));
|
||||||
|
|
||||||
|
new QShortcut(Qt::Key_Delete, this, SLOT(deleteSelectedEntries()));
|
||||||
}
|
}
|
||||||
|
|
||||||
ReportsWidgetHealthcheck::~ReportsWidgetHealthcheck()
|
ReportsWidgetHealthcheck::~ReportsWidgetHealthcheck()
|
||||||
@ -164,8 +166,8 @@ ReportsWidgetHealthcheck::~ReportsWidgetHealthcheck()
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer<PasswordHealth> health,
|
void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer<PasswordHealth> health,
|
||||||
const Group* group,
|
Group* group,
|
||||||
const Entry* entry,
|
Entry* entry,
|
||||||
bool knownBad)
|
bool knownBad)
|
||||||
{
|
{
|
||||||
QString descr, tip;
|
QString descr, tip;
|
||||||
@ -312,50 +314,82 @@ void ReportsWidgetHealthcheck::emitEntryActivated(const QModelIndex& index)
|
|||||||
|
|
||||||
void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos)
|
void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos)
|
||||||
{
|
{
|
||||||
|
auto selected = m_ui->healthcheckTableView->selectionModel()->selectedRows();
|
||||||
// Find which entry has been clicked
|
if (selected.isEmpty()) {
|
||||||
const auto index = m_ui->healthcheckTableView->indexAt(pos);
|
|
||||||
if (!index.isValid()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto mappedIndex = m_modelProxy->mapToSource(index);
|
|
||||||
m_contextmenuEntry = const_cast<Entry*>(m_rowToEntry[mappedIndex.row()].second);
|
|
||||||
if (!m_contextmenuEntry) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the context menu
|
// Create the context menu
|
||||||
const auto menu = new QMenu(this);
|
const auto menu = new QMenu(this);
|
||||||
|
|
||||||
// Create the "edit entry" menu item
|
// Create the "edit entry" menu item (only if 1 row is selected)
|
||||||
const auto edit = new QAction(icons()->icon("entry-edit"), tr("Edit Entry…"), this);
|
if (selected.size() == 1) {
|
||||||
menu->addAction(edit);
|
const auto edit = new QAction(icons()->icon("entry-edit"), tr("Edit Entry…"), this);
|
||||||
connect(edit, SIGNAL(triggered()), SLOT(editFromContextmenu()));
|
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 "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
|
// Create the "exclude from reports" menu item
|
||||||
const auto exclude = new QAction(icons()->icon("reports-exclude"), tr("Exclude from reports"), this);
|
const auto exclude = new QAction(icons()->icon("reports-exclude"), tr("Exclude from reports"), this);
|
||||||
exclude->setCheckable(true);
|
|
||||||
exclude->setChecked(m_contextmenuEntry->excludeFromReports());
|
bool isExcluded = false;
|
||||||
menu->addAction(exclude);
|
for (auto index : selected) {
|
||||||
connect(exclude, &QAction::toggled, exclude, [this](bool state) {
|
auto row = m_modelProxy->mapToSource(index).row();
|
||||||
if (m_contextmenuEntry) {
|
auto entry = m_rowToEntry[row].second;
|
||||||
m_contextmenuEntry->setExcludeFromReports(state);
|
if (entry && entry->excludeFromReports()) {
|
||||||
calculateHealth();
|
// 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
|
// Show the context menu
|
||||||
menu->popup(m_ui->healthcheckTableView->viewport()->mapToGlobal(pos));
|
menu->popup(m_ui->healthcheckTableView->viewport()->mapToGlobal(pos));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReportsWidgetHealthcheck::editFromContextmenu()
|
|
||||||
{
|
|
||||||
if (m_contextmenuEntry) {
|
|
||||||
emit entryActivated(m_contextmenuEntry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ReportsWidgetHealthcheck::saveSettings()
|
void ReportsWidgetHealthcheck::saveSettings()
|
||||||
{
|
{
|
||||||
// nothing to do - the tab is passive
|
// nothing to do - the tab is passive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ReportsWidgetHealthcheck::deleteSelectedEntries()
|
||||||
|
{
|
||||||
|
QList<Entry*> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool permanent = !m_db->metadata()->recycleBinEnabled();
|
||||||
|
if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) {
|
||||||
|
GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent);
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateHealth();
|
||||||
|
}
|
||||||
|
@ -56,10 +56,10 @@ public slots:
|
|||||||
void calculateHealth();
|
void calculateHealth();
|
||||||
void emitEntryActivated(const QModelIndex& index);
|
void emitEntryActivated(const QModelIndex& index);
|
||||||
void customMenuRequested(QPoint);
|
void customMenuRequested(QPoint);
|
||||||
void editFromContextmenu();
|
void deleteSelectedEntries();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void addHealthRow(QSharedPointer<PasswordHealth>, const Group*, const Entry*, bool knownBad);
|
void addHealthRow(QSharedPointer<PasswordHealth>, Group*, Entry*, bool knownBad);
|
||||||
|
|
||||||
QScopedPointer<Ui::ReportsWidgetHealthcheck> m_ui;
|
QScopedPointer<Ui::ReportsWidgetHealthcheck> m_ui;
|
||||||
|
|
||||||
@ -67,8 +67,7 @@ private:
|
|||||||
QScopedPointer<QStandardItemModel> m_referencesModel;
|
QScopedPointer<QStandardItemModel> m_referencesModel;
|
||||||
QScopedPointer<QSortFilterProxyModel> m_modelProxy;
|
QScopedPointer<QSortFilterProxyModel> m_modelProxy;
|
||||||
QSharedPointer<Database> m_db;
|
QSharedPointer<Database> m_db;
|
||||||
QList<QPair<const Group*, const Entry*>> m_rowToEntry;
|
QList<QPair<Group*, Entry*>> m_rowToEntry;
|
||||||
Entry* m_contextmenuEntry = nullptr;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H
|
#endif // KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<height>379</height>
|
<height>379</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0">
|
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,0">
|
||||||
<property name="leftMargin">
|
<property name="leftMargin">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
@ -37,11 +37,14 @@
|
|||||||
<property name="alternatingRowColors">
|
<property name="alternatingRowColors">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="selectionBehavior">
|
||||||
|
<enum>QAbstractItemView::SelectRows</enum>
|
||||||
|
</property>
|
||||||
<property name="textElideMode">
|
<property name="textElideMode">
|
||||||
<enum>Qt::ElideMiddle</enum>
|
<enum>Qt::ElideMiddle</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="sortingEnabled">
|
<property name="sortingEnabled">
|
||||||
<bool>false</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<attribute name="horizontalHeaderStretchLastSection">
|
<attribute name="horizontalHeaderStretchLastSection">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
|
@ -22,11 +22,14 @@
|
|||||||
#include "core/Database.h"
|
#include "core/Database.h"
|
||||||
#include "core/Global.h"
|
#include "core/Global.h"
|
||||||
#include "core/Group.h"
|
#include "core/Group.h"
|
||||||
|
#include "core/Metadata.h"
|
||||||
#include "core/PasswordHealth.h"
|
#include "core/PasswordHealth.h"
|
||||||
|
#include "gui/GuiTools.h"
|
||||||
#include "gui/Icons.h"
|
#include "gui/Icons.h"
|
||||||
#include "gui/MessageBox.h"
|
#include "gui/MessageBox.h"
|
||||||
|
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
|
#include <QShortcut>
|
||||||
#include <QSortFilterProxyModel>
|
#include <QSortFilterProxyModel>
|
||||||
#include <QStandardItemModel>
|
#include <QStandardItemModel>
|
||||||
|
|
||||||
@ -64,10 +67,8 @@ ReportsWidgetHibp::ReportsWidgetHibp(QWidget* parent)
|
|||||||
m_modelProxy->setSourceModel(m_referencesModel.data());
|
m_modelProxy->setSourceModel(m_referencesModel.data());
|
||||||
m_modelProxy->setSortLocaleAware(true);
|
m_modelProxy->setSortLocaleAware(true);
|
||||||
m_ui->hibpTableView->setModel(m_modelProxy.data());
|
m_ui->hibpTableView->setModel(m_modelProxy.data());
|
||||||
m_ui->hibpTableView->setSelectionMode(QAbstractItemView::NoSelection);
|
|
||||||
m_ui->hibpTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
|
m_ui->hibpTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
|
||||||
m_ui->hibpTableView->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
m_ui->hibpTableView->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||||
m_ui->hibpTableView->setSortingEnabled(true);
|
|
||||||
|
|
||||||
connect(m_ui->hibpTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex)));
|
connect(m_ui->hibpTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex)));
|
||||||
connect(m_ui->hibpTableView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customMenuRequested(QPoint)));
|
connect(m_ui->hibpTableView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customMenuRequested(QPoint)));
|
||||||
@ -78,6 +79,8 @@ ReportsWidgetHibp::ReportsWidgetHibp(QWidget* parent)
|
|||||||
|
|
||||||
connect(m_ui->validationButton, &QPushButton::pressed, [this] { startValidation(); });
|
connect(m_ui->validationButton, &QPushButton::pressed, [this] { startValidation(); });
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
new QShortcut(Qt::Key_Delete, this, SLOT(deleteSelectedEntries()));
|
||||||
}
|
}
|
||||||
|
|
||||||
ReportsWidgetHibp::~ReportsWidgetHibp()
|
ReportsWidgetHibp::~ReportsWidgetHibp()
|
||||||
@ -124,8 +127,8 @@ void ReportsWidgetHibp::makeHibpTable()
|
|||||||
m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Title") << tr("Path") << tr("Password exposed…"));
|
m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Title") << tr("Path") << tr("Password exposed…"));
|
||||||
|
|
||||||
// Search database for passwords that we've found so far
|
// Search database for passwords that we've found so far
|
||||||
QList<QPair<const Entry*, int>> items;
|
QList<QPair<Entry*, int>> items;
|
||||||
for (const auto* entry : m_db->rootGroup()->entriesRecursive()) {
|
for (auto entry : m_db->rootGroup()->entriesRecursive()) {
|
||||||
if (!entry->isRecycled()) {
|
if (!entry->isRecycled()) {
|
||||||
const auto found = m_pwndPasswords.find(entry->password());
|
const auto found = m_pwndPasswords.find(entry->password());
|
||||||
if (found != m_pwndPasswords.end()) {
|
if (found != m_pwndPasswords.end()) {
|
||||||
@ -135,7 +138,7 @@ void ReportsWidgetHibp::makeHibpTable()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sort decending by the number the password has been exposed
|
// Sort decending by the number the password has been exposed
|
||||||
qSort(items.begin(), items.end(), [](QPair<const Entry*, int>& lhs, QPair<const Entry*, int>& rhs) {
|
qSort(items.begin(), items.end(), [](QPair<Entry*, int>& lhs, QPair<Entry*, int>& rhs) {
|
||||||
return lhs.second > rhs.second;
|
return lhs.second > rhs.second;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -356,47 +359,79 @@ void ReportsWidgetHibp::refreshAfterEdit()
|
|||||||
|
|
||||||
void ReportsWidgetHibp::customMenuRequested(QPoint pos)
|
void ReportsWidgetHibp::customMenuRequested(QPoint pos)
|
||||||
{
|
{
|
||||||
|
auto selected = m_ui->hibpTableView->selectionModel()->selectedRows();
|
||||||
// Find which entry has been clicked
|
if (selected.isEmpty()) {
|
||||||
const auto index = m_ui->hibpTableView->indexAt(pos);
|
|
||||||
if (!index.isValid()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto mappedIndex = m_modelProxy->mapToSource(index);
|
|
||||||
m_contextmenuEntry = const_cast<Entry*>(m_rowToEntry[mappedIndex.row()]);
|
|
||||||
if (!m_contextmenuEntry) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the context menu
|
// Create the context menu
|
||||||
const auto menu = new QMenu(this);
|
const auto menu = new QMenu(this);
|
||||||
|
|
||||||
// Create the "edit entry" menu item
|
// Create the "edit entry" menu item if 1 row is selected
|
||||||
const auto edit = new QAction(icons()->icon("entry-edit"), tr("Edit Entry…"), this);
|
if (selected.size() == 1) {
|
||||||
menu->addAction(edit);
|
const auto edit = new QAction(icons()->icon("entry-edit"), tr("Edit Entry…"), this);
|
||||||
connect(edit, SIGNAL(triggered()), SLOT(editFromContextmenu()));
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
// Create the "exclude from reports" menu item
|
||||||
const auto exclude = new QAction(icons()->icon("reports-exclude"), tr("Exclude from reports"), this);
|
const auto exclude = new QAction(icons()->icon("reports-exclude"), tr("Exclude from reports"), this);
|
||||||
exclude->setCheckable(true);
|
|
||||||
exclude->setChecked(m_contextmenuEntry->excludeFromReports());
|
bool isExcluded = false;
|
||||||
menu->addAction(exclude);
|
for (auto index : selected) {
|
||||||
connect(exclude, &QAction::toggled, exclude, [this](bool state) {
|
auto row = m_modelProxy->mapToSource(index).row();
|
||||||
if (m_contextmenuEntry) {
|
auto entry = m_rowToEntry[row];
|
||||||
m_contextmenuEntry->setExcludeFromReports(state);
|
if (entry && entry->excludeFromReports()) {
|
||||||
makeHibpTable();
|
// 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];
|
||||||
|
if (entry) {
|
||||||
|
entry->setExcludeFromReports(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
makeHibpTable();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show the context menu
|
// Show the context menu
|
||||||
menu->popup(m_ui->hibpTableView->viewport()->mapToGlobal(pos));
|
menu->popup(m_ui->hibpTableView->viewport()->mapToGlobal(pos));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReportsWidgetHibp::editFromContextmenu()
|
void ReportsWidgetHibp::deleteSelectedEntries()
|
||||||
{
|
{
|
||||||
if (m_contextmenuEntry) {
|
QList<Entry*> selectedEntries;
|
||||||
emit entryActivated(m_contextmenuEntry);
|
for (auto index : m_ui->hibpTableView->selectionModel()->selectedRows()) {
|
||||||
|
auto row = m_modelProxy->mapToSource(index).row();
|
||||||
|
auto entry = m_rowToEntry[row];
|
||||||
|
if (entry) {
|
||||||
|
selectedEntries << entry;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool permanent = !m_db->metadata()->recycleBinEnabled();
|
||||||
|
if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) {
|
||||||
|
GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent);
|
||||||
|
}
|
||||||
|
|
||||||
|
makeHibpTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReportsWidgetHibp::saveSettings()
|
void ReportsWidgetHibp::saveSettings()
|
||||||
|
@ -61,7 +61,7 @@ public slots:
|
|||||||
void fetchFailed(const QString& error);
|
void fetchFailed(const QString& error);
|
||||||
void makeHibpTable();
|
void makeHibpTable();
|
||||||
void customMenuRequested(QPoint);
|
void customMenuRequested(QPoint);
|
||||||
void editFromContextmenu();
|
void deleteSelectedEntries();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void startValidation();
|
void startValidation();
|
||||||
@ -74,11 +74,10 @@ private:
|
|||||||
|
|
||||||
QMap<QString, int> m_pwndPasswords; // Passwords we found to have been pwned (value is pwn count)
|
QMap<QString, int> m_pwndPasswords; // Passwords we found to have been pwned (value is pwn count)
|
||||||
QString m_error; // Error message if download failed, else empty
|
QString m_error; // Error message if download failed, else empty
|
||||||
QList<const Entry*> m_rowToEntry; // List index is table row
|
QList<Entry*> m_rowToEntry; // List index is table row
|
||||||
QPointer<const Entry> m_editedEntry; // The entry we're currently editing
|
QPointer<Entry> m_editedEntry; // The entry we're currently editing
|
||||||
QString m_editedPassword; // The old password of the entry we're 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
|
bool m_editedExcluded; // The old "known bad" flag of the entry we're editing
|
||||||
Entry* m_contextmenuEntry = nullptr; // The entry that was right-clicked
|
|
||||||
|
|
||||||
#ifdef WITH_XC_NETWORKING
|
#ifdef WITH_XC_NETWORKING
|
||||||
HibpDownloader m_downloader; // This performs the actual HIBP online query
|
HibpDownloader m_downloader; // This performs the actual HIBP online query
|
||||||
|
@ -151,11 +151,14 @@
|
|||||||
<property name="alternatingRowColors">
|
<property name="alternatingRowColors">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="selectionBehavior">
|
||||||
|
<enum>QAbstractItemView::SelectRows</enum>
|
||||||
|
</property>
|
||||||
<property name="textElideMode">
|
<property name="textElideMode">
|
||||||
<enum>Qt::ElideMiddle</enum>
|
<enum>Qt::ElideMiddle</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="sortingEnabled">
|
<property name="sortingEnabled">
|
||||||
<bool>false</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
<attribute name="horizontalHeaderStretchLastSection">
|
<attribute name="horizontalHeaderStretchLastSection">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
|
Loading…
Reference in New Issue
Block a user