mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-02-08 18:58:29 -05:00
Add entry view column for password strength
* Closes #4216 Reduced to three-tiered rating system and fixed column implementation. Hide password strength indicator in entry view if excluded from reports. Introduce password health caching to prevent unnecessary calculations.
This commit is contained in:
parent
c9c19d043f
commit
022154462e
1
COPYING
1
COPYING
@ -166,6 +166,7 @@ Files: share/icons/application/scalable/actions/chevron-double-down.svg
|
|||||||
share/icons/application/scalable/actions/group-new.svg
|
share/icons/application/scalable/actions/group-new.svg
|
||||||
share/icons/application/scalable/actions/help-about.svg
|
share/icons/application/scalable/actions/help-about.svg
|
||||||
share/icons/application/scalable/actions/key-enter.svg
|
share/icons/application/scalable/actions/key-enter.svg
|
||||||
|
share/icons/application/scalable/actions/lock-question.svg
|
||||||
share/icons/application/scalable/actions/message-close.svg
|
share/icons/application/scalable/actions/message-close.svg
|
||||||
share/icons/application/scalable/actions/move-down.svg
|
share/icons/application/scalable/actions/move-down.svg
|
||||||
share/icons/application/scalable/actions/move-up.svg
|
share/icons/application/scalable/actions/move-up.svg
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12,1A5,5 0 0,0 7,6V8H6A2,2 0 0,0 4,10V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V10A2,2 0 0,0 18,8H17V6A5,5 0 0,0 12,1M12,2.9C13.71,2.9 15.1,4.29 15.1,6V8H8.9V6C8.9,4.29 10.29,2.9 12,2.9M12.19,10.5C13.13,10.5 13.88,10.71 14.42,11.12C14.96,11.54 15.23,12.1 15.23,12.8C15.23,13.24 15.08,13.63 14.79,14C14.5,14.36 14.12,14.64 13.66,14.85C13.4,15 13.23,15.15 13.14,15.32C13.05,15.5 13,15.72 13,16H11C11,15.5 11.1,15.16 11.29,14.92C11.5,14.68 11.84,14.4 12.36,14.08C12.62,13.94 12.83,13.76 13,13.54C13.14,13.33 13.22,13.08 13.22,12.8C13.22,12.5 13.13,12.28 12.95,12.11C12.77,11.93 12.5,11.85 12.19,11.85C11.92,11.85 11.7,11.92 11.5,12.06C11.34,12.2 11.24,12.41 11.24,12.69H9.27C9.22,12 9.5,11.4 10.05,11.04C10.59,10.68 11.3,10.5 12.19,10.5M11,17H13V19H11V17Z" /></svg>
|
After Width: | Height: | Size: 1.0 KiB |
@ -46,6 +46,7 @@
|
|||||||
<file>application/scalable/actions/help-about.svg</file>
|
<file>application/scalable/actions/help-about.svg</file>
|
||||||
<file>application/scalable/actions/hibp.svg</file>
|
<file>application/scalable/actions/hibp.svg</file>
|
||||||
<file>application/scalable/actions/key-enter.svg</file>
|
<file>application/scalable/actions/key-enter.svg</file>
|
||||||
|
<file>application/scalable/actions/lock-question.svg</file>
|
||||||
<file>application/scalable/actions/keyboard-shortcuts.svg</file>
|
<file>application/scalable/actions/keyboard-shortcuts.svg</file>
|
||||||
<file>application/scalable/actions/message-close.svg</file>
|
<file>application/scalable/actions/message-close.svg</file>
|
||||||
<file>application/scalable/actions/move-down.svg</file>
|
<file>application/scalable/actions/move-down.svg</file>
|
||||||
|
@ -172,6 +172,6 @@ int Estimate::execute(const QStringList& arguments)
|
|||||||
password = in.readLine();
|
password = in.readLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
estimate(password.toLatin1(), parser->isSet(Estimate::AdvancedOption));
|
estimate(password.toUtf8(), parser->isSet(Estimate::AdvancedOption));
|
||||||
return EXIT_SUCCESS;
|
return EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ const QString CustomData::LastModified = QStringLiteral("_LAST_MODIFIED");
|
|||||||
const QString CustomData::Created = QStringLiteral("_CREATED");
|
const QString CustomData::Created = QStringLiteral("_CREATED");
|
||||||
const QString CustomData::BrowserKeyPrefix = QStringLiteral("KPXC_BROWSER_");
|
const QString CustomData::BrowserKeyPrefix = QStringLiteral("KPXC_BROWSER_");
|
||||||
const QString CustomData::BrowserLegacyKeyPrefix = QStringLiteral("Public Key: ");
|
const QString CustomData::BrowserLegacyKeyPrefix = QStringLiteral("Public Key: ");
|
||||||
|
const QString CustomData::ExcludeFromReports = QStringLiteral("KnownBad");
|
||||||
|
|
||||||
CustomData::CustomData(QObject* parent)
|
CustomData::CustomData(QObject* parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
|
@ -51,6 +51,7 @@ public:
|
|||||||
static const QString Created;
|
static const QString Created;
|
||||||
static const QString BrowserKeyPrefix;
|
static const QString BrowserKeyPrefix;
|
||||||
static const QString BrowserLegacyKeyPrefix;
|
static const QString BrowserLegacyKeyPrefix;
|
||||||
|
static const QString ExcludeFromReports;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void customDataModified();
|
void customDataModified();
|
||||||
|
@ -17,14 +17,12 @@
|
|||||||
*/
|
*/
|
||||||
#include "Entry.h"
|
#include "Entry.h"
|
||||||
|
|
||||||
#include "config-keepassx.h"
|
|
||||||
|
|
||||||
#include "core/Clock.h"
|
|
||||||
#include "core/Config.h"
|
#include "core/Config.h"
|
||||||
#include "core/Database.h"
|
#include "core/Database.h"
|
||||||
#include "core/DatabaseIcons.h"
|
#include "core/DatabaseIcons.h"
|
||||||
#include "core/Group.h"
|
#include "core/Group.h"
|
||||||
#include "core/Metadata.h"
|
#include "core/Metadata.h"
|
||||||
|
#include "core/PasswordHealth.h"
|
||||||
#include "core/Tools.h"
|
#include "core/Tools.h"
|
||||||
#include "totp/totp.h"
|
#include "totp/totp.h"
|
||||||
|
|
||||||
@ -245,6 +243,25 @@ QString Entry::defaultAutoTypeSequence() const
|
|||||||
return m_data.defaultAutoTypeSequence;
|
return m_data.defaultAutoTypeSequence;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QSharedPointer<PasswordHealth>& Entry::passwordHealth()
|
||||||
|
{
|
||||||
|
if (!m_data.passwordHealth) {
|
||||||
|
m_data.passwordHealth.reset(new PasswordHealth(resolvePlaceholder(password())));
|
||||||
|
}
|
||||||
|
return m_data.passwordHealth;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Entry::excludeFromReports() const
|
||||||
|
{
|
||||||
|
return customData()->contains(CustomData::ExcludeFromReports)
|
||||||
|
&& customData()->value(CustomData::ExcludeFromReports) == TRUE_STR;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Entry::setExcludeFromReports(bool state)
|
||||||
|
{
|
||||||
|
customData()->set(CustomData::ExcludeFromReports, state ? TRUE_STR : FALSE_STR);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine the effective sequence that will be injected
|
* Determine the effective sequence that will be injected
|
||||||
* This function return an empty string if a parent group has autotype disabled or if the entry has no parent
|
* This function return an empty string if a parent group has autotype disabled or if the entry has no parent
|
||||||
@ -673,6 +690,8 @@ void Entry::setUsername(const QString& username)
|
|||||||
|
|
||||||
void Entry::setPassword(const QString& password)
|
void Entry::setPassword(const QString& password)
|
||||||
{
|
{
|
||||||
|
// Reset Password Health
|
||||||
|
m_data.passwordHealth.reset();
|
||||||
m_attributes->set(EntryAttributes::PasswordKey, password, m_attributes->isProtected(EntryAttributes::PasswordKey));
|
m_attributes->set(EntryAttributes::PasswordKey, password, m_attributes->isProtected(EntryAttributes::PasswordKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +36,8 @@
|
|||||||
|
|
||||||
class Database;
|
class Database;
|
||||||
class Group;
|
class Group;
|
||||||
|
class PasswordHealth;
|
||||||
|
|
||||||
namespace Totp
|
namespace Totp
|
||||||
{
|
{
|
||||||
struct Settings;
|
struct Settings;
|
||||||
@ -66,6 +68,7 @@ struct EntryData
|
|||||||
QString defaultAutoTypeSequence;
|
QString defaultAutoTypeSequence;
|
||||||
TimeInfo timeInfo;
|
TimeInfo timeInfo;
|
||||||
QSharedPointer<Totp::Settings> totpSettings;
|
QSharedPointer<Totp::Settings> totpSettings;
|
||||||
|
QSharedPointer<PasswordHealth> passwordHealth;
|
||||||
|
|
||||||
bool operator==(const EntryData& other) const;
|
bool operator==(const EntryData& other) const;
|
||||||
bool operator!=(const EntryData& other) const;
|
bool operator!=(const EntryData& other) const;
|
||||||
@ -94,7 +97,6 @@ public:
|
|||||||
int autoTypeObfuscation() const;
|
int autoTypeObfuscation() const;
|
||||||
QString defaultAutoTypeSequence() const;
|
QString defaultAutoTypeSequence() const;
|
||||||
QString effectiveAutoTypeSequence() const;
|
QString effectiveAutoTypeSequence() const;
|
||||||
QString effectiveNewAutoTypeSequence() const;
|
|
||||||
QList<QString> autoTypeSequences(const QString& pattern = {}) const;
|
QList<QString> autoTypeSequences(const QString& pattern = {}) const;
|
||||||
AutoTypeAssociations* autoTypeAssociations();
|
AutoTypeAssociations* autoTypeAssociations();
|
||||||
const AutoTypeAssociations* autoTypeAssociations() const;
|
const AutoTypeAssociations* autoTypeAssociations() const;
|
||||||
@ -111,6 +113,9 @@ public:
|
|||||||
QSharedPointer<Totp::Settings> totpSettings() const;
|
QSharedPointer<Totp::Settings> totpSettings() const;
|
||||||
int size() const;
|
int size() const;
|
||||||
QString path() const;
|
QString path() const;
|
||||||
|
const QSharedPointer<PasswordHealth>& passwordHealth();
|
||||||
|
bool excludeFromReports() const;
|
||||||
|
void setExcludeFromReports(bool state);
|
||||||
|
|
||||||
bool hasTotp() const;
|
bool hasTotp() const;
|
||||||
bool isExpired() const;
|
bool isExpired() const;
|
||||||
|
@ -24,9 +24,6 @@
|
|||||||
#include "PasswordHealth.h"
|
#include "PasswordHealth.h"
|
||||||
#include "zxcvbn.h"
|
#include "zxcvbn.h"
|
||||||
|
|
||||||
// Define the static member variable with the custom field name
|
|
||||||
const QString PasswordHealth::OPTION_KNOWN_BAD = QStringLiteral("KnownBad");
|
|
||||||
|
|
||||||
PasswordHealth::PasswordHealth(double entropy)
|
PasswordHealth::PasswordHealth(double entropy)
|
||||||
: m_score(entropy)
|
: m_score(entropy)
|
||||||
, m_entropy(entropy)
|
, m_entropy(entropy)
|
||||||
@ -49,8 +46,8 @@ PasswordHealth::PasswordHealth(double entropy)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PasswordHealth::PasswordHealth(QString pwd)
|
PasswordHealth::PasswordHealth(const QString& pwd)
|
||||||
: PasswordHealth(ZxcvbnMatch(pwd.toLatin1(), nullptr, nullptr))
|
: PasswordHealth(ZxcvbnMatch(pwd.toUtf8(), nullptr, nullptr))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ class PasswordHealth
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
explicit PasswordHealth(double entropy);
|
explicit PasswordHealth(double entropy);
|
||||||
explicit PasswordHealth(QString pwd);
|
explicit PasswordHealth(const QString& pwd);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The password score is defined to be the greater the better
|
* The password score is defined to be the greater the better
|
||||||
@ -83,14 +83,6 @@ public:
|
|||||||
return m_entropy;
|
return m_entropy;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Name of custom data field that holds the "this is a known
|
|
||||||
* bad password" flag. Legal values of the field are TRUE_STR
|
|
||||||
* and FALSE_STR, the default (used if the field doesn't exist)
|
|
||||||
* is false.
|
|
||||||
*/
|
|
||||||
static const QString OPTION_KNOWN_BAD;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int m_score = 0;
|
int m_score = 0;
|
||||||
double m_entropy = 0.0;
|
double m_entropy = 0.0;
|
||||||
|
@ -440,7 +440,7 @@ void EditEntryWidget::setupEntryUpdate()
|
|||||||
// Advanced tab
|
// Advanced tab
|
||||||
connect(m_advancedUi->attributesEdit, SIGNAL(textChanged()), this, SLOT(setModified()));
|
connect(m_advancedUi->attributesEdit, SIGNAL(textChanged()), this, SLOT(setModified()));
|
||||||
connect(m_advancedUi->protectAttributeButton, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
|
connect(m_advancedUi->protectAttributeButton, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
|
||||||
connect(m_advancedUi->knownBadCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
|
connect(m_advancedUi->excludeReportsCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
|
||||||
connect(m_advancedUi->fgColorCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
|
connect(m_advancedUi->fgColorCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
|
||||||
connect(m_advancedUi->bgColorCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
|
connect(m_advancedUi->bgColorCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setModified()));
|
||||||
connect(m_advancedUi->attachmentsWidget, SIGNAL(widgetUpdated()), this, SLOT(setModified()));
|
connect(m_advancedUi->attachmentsWidget, SIGNAL(widgetUpdated()), this, SLOT(setModified()));
|
||||||
@ -861,9 +861,7 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
|
|||||||
editTriggers = QAbstractItemView::DoubleClicked;
|
editTriggers = QAbstractItemView::DoubleClicked;
|
||||||
}
|
}
|
||||||
m_advancedUi->attributesView->setEditTriggers(editTriggers);
|
m_advancedUi->attributesView->setEditTriggers(editTriggers);
|
||||||
m_advancedUi->knownBadCheckBox->setChecked(entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
|
m_advancedUi->excludeReportsCheckBox->setChecked(entry->excludeFromReports());
|
||||||
&& entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD)
|
|
||||||
== TRUE_STR);
|
|
||||||
setupColorButton(true, entry->foregroundColor());
|
setupColorButton(true, entry->foregroundColor());
|
||||||
setupColorButton(false, entry->backgroundColor());
|
setupColorButton(false, entry->backgroundColor());
|
||||||
m_iconsWidget->setEnabled(!m_history);
|
m_iconsWidget->setEnabled(!m_history);
|
||||||
@ -1126,11 +1124,8 @@ void EditEntryWidget::updateEntryData(Entry* entry) const
|
|||||||
|
|
||||||
entry->setNotes(m_mainUi->notesEdit->toPlainText());
|
entry->setNotes(m_mainUi->notesEdit->toPlainText());
|
||||||
|
|
||||||
const auto wasKnownBad = entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
|
if (entry->excludeFromReports() != m_advancedUi->excludeReportsCheckBox->isChecked()) {
|
||||||
&& entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR;
|
entry->setExcludeFromReports(m_advancedUi->excludeReportsCheckBox->isChecked());
|
||||||
const auto isKnownBad = m_advancedUi->knownBadCheckBox->isChecked();
|
|
||||||
if (isKnownBad != wasKnownBad) {
|
|
||||||
entry->customData()->set(PasswordHealth::OPTION_KNOWN_BAD, isKnownBad ? TRUE_STR : FALSE_STR);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_advancedUi->fgColorCheckBox->isChecked() && m_advancedUi->fgColorButton->property("color").isValid()) {
|
if (m_advancedUi->fgColorCheckBox->isChecked() && m_advancedUi->fgColorButton->property("color").isValid()) {
|
||||||
|
@ -187,9 +187,9 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="knownBadCheckBox">
|
<widget class="QCheckBox" name="excludeReportsCheckBox">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string><html><head/><body><p>If checked, the entry will not appear in reports like Health Check and HIBP even if it doesn't match the quality requirements (e. g. password entropy or re-use). You can set the check mark if the password is beyond your control (e. g. if it needs to be a four-digit PIN) to prevent it from cluttering the reports.</p></body></html></string>
|
<string>If checked, the entry will not appear in reports like Health Check and HIBP even if it doesn't match the quality requirements.</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Exclude from database reports</string>
|
<string>Exclude from database reports</string>
|
||||||
@ -327,7 +327,7 @@
|
|||||||
<tabstop>editAttributeButton</tabstop>
|
<tabstop>editAttributeButton</tabstop>
|
||||||
<tabstop>protectAttributeButton</tabstop>
|
<tabstop>protectAttributeButton</tabstop>
|
||||||
<tabstop>revealAttributeButton</tabstop>
|
<tabstop>revealAttributeButton</tabstop>
|
||||||
<tabstop>knownBadCheckBox</tabstop>
|
<tabstop>excludeReportsCheckBox</tabstop>
|
||||||
<tabstop>fgColorCheckBox</tabstop>
|
<tabstop>fgColorCheckBox</tabstop>
|
||||||
<tabstop>fgColorButton</tabstop>
|
<tabstop>fgColorButton</tabstop>
|
||||||
<tabstop>bgColorCheckBox</tabstop>
|
<tabstop>bgColorCheckBox</tabstop>
|
||||||
|
@ -18,18 +18,17 @@
|
|||||||
#include "EntryModel.h"
|
#include "EntryModel.h"
|
||||||
|
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QFont>
|
|
||||||
#include <QMimeData>
|
#include <QMimeData>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPalette>
|
#include <QPalette>
|
||||||
|
|
||||||
#include "core/Config.h"
|
|
||||||
#include "core/DatabaseIcons.h"
|
#include "core/DatabaseIcons.h"
|
||||||
#include "core/Entry.h"
|
#include "core/Entry.h"
|
||||||
#include "core/Global.h"
|
|
||||||
#include "core/Group.h"
|
#include "core/Group.h"
|
||||||
#include "core/Metadata.h"
|
#include "core/Metadata.h"
|
||||||
|
#include "core/PasswordHealth.h"
|
||||||
#include "gui/Icons.h"
|
#include "gui/Icons.h"
|
||||||
|
#include "gui/styles/StateColorPalette.h"
|
||||||
#ifdef Q_OS_MACOS
|
#ifdef Q_OS_MACOS
|
||||||
#include "gui/osutils/macutils/MacUtils.h"
|
#include "gui/osutils/macutils/MacUtils.h"
|
||||||
#endif
|
#endif
|
||||||
@ -128,7 +127,7 @@ int EntryModel::columnCount(const QModelIndex& parent) const
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 14;
|
return 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant EntryModel::data(const QModelIndex& index, int role) const
|
QVariant EntryModel::data(const QModelIndex& index, int role) const
|
||||||
@ -249,6 +248,12 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
|
|||||||
return entry->resolveMultiplePlaceholders(entry->username());
|
return entry->resolveMultiplePlaceholders(entry->username());
|
||||||
case Password:
|
case Password:
|
||||||
return entry->resolveMultiplePlaceholders(entry->password());
|
return entry->resolveMultiplePlaceholders(entry->password());
|
||||||
|
case PasswordStrength: {
|
||||||
|
if (!entry->password().isEmpty() && !entry->excludeFromReports()) {
|
||||||
|
return entry->passwordHealth()->score();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
case Expires:
|
case Expires:
|
||||||
// There seems to be no better way of expressing 'infinity'
|
// There seems to be no better way of expressing 'infinity'
|
||||||
return entry->timeInfo().expires() ? entry->timeInfo().expiryTime() : QDateTime(QDate(9999, 1, 1));
|
return entry->timeInfo().expires() ? entry->timeInfo().expiryTime() : QDateTime(QDate(9999, 1, 1));
|
||||||
@ -290,6 +295,28 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
|
|||||||
return icons()->icon("chronometer");
|
return icons()->icon("chronometer");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case PasswordStrength:
|
||||||
|
if (!entry->password().isEmpty() && !entry->excludeFromReports()) {
|
||||||
|
StateColorPalette statePalette;
|
||||||
|
QColor color = statePalette.color(StateColorPalette::Error);
|
||||||
|
|
||||||
|
switch (entry->passwordHealth()->quality()) {
|
||||||
|
case PasswordHealth::Quality::Bad:
|
||||||
|
case PasswordHealth::Quality::Poor:
|
||||||
|
color = statePalette.color(StateColorPalette::HealthCritical);
|
||||||
|
break;
|
||||||
|
case PasswordHealth::Quality::Weak:
|
||||||
|
color = statePalette.color(StateColorPalette::HealthBad);
|
||||||
|
break;
|
||||||
|
case PasswordHealth::Quality::Good:
|
||||||
|
case PasswordHealth::Quality::Excellent:
|
||||||
|
color = statePalette.color(StateColorPalette::HealthExcellent);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} else if (role == Qt::FontRole) {
|
} else if (role == Qt::FontRole) {
|
||||||
QFont font;
|
QFont font;
|
||||||
@ -316,9 +343,9 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const
|
|||||||
if (backgroundColor.isValid()) {
|
if (backgroundColor.isValid()) {
|
||||||
return QVariant(backgroundColor);
|
return QVariant(backgroundColor);
|
||||||
}
|
}
|
||||||
} else if (role == Qt::TextAlignmentRole) {
|
} else if (role == Qt::ToolTipRole) {
|
||||||
if (index.column() == Paperclip) {
|
if (index.column() == PasswordStrength && !entry->password().isEmpty() && !entry->excludeFromReports()) {
|
||||||
return Qt::AlignCenter;
|
return entry->passwordHealth()->scoreReason();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -363,6 +390,8 @@ QVariant EntryModel::headerData(int section, Qt::Orientation orientation, int ro
|
|||||||
return icons()->icon("paperclip");
|
return icons()->icon("paperclip");
|
||||||
case Totp:
|
case Totp:
|
||||||
return icons()->icon("chronometer");
|
return icons()->icon("chronometer");
|
||||||
|
case PasswordStrength:
|
||||||
|
return icons()->icon("lock-question");
|
||||||
}
|
}
|
||||||
} else if (role == Qt::ToolTipRole) {
|
} else if (role == Qt::ToolTipRole) {
|
||||||
switch (section) {
|
switch (section) {
|
||||||
@ -374,6 +403,8 @@ QVariant EntryModel::headerData(int section, Qt::Orientation orientation, int ro
|
|||||||
return tr("Username");
|
return tr("Username");
|
||||||
case Password:
|
case Password:
|
||||||
return tr("Password");
|
return tr("Password");
|
||||||
|
case PasswordStrength:
|
||||||
|
return tr("Password Strength");
|
||||||
case Url:
|
case Url:
|
||||||
return tr("URL");
|
return tr("URL");
|
||||||
case Notes:
|
case Notes:
|
||||||
@ -393,7 +424,7 @@ QVariant EntryModel::headerData(int section, Qt::Orientation orientation, int ro
|
|||||||
case Paperclip:
|
case Paperclip:
|
||||||
return tr("Has attachments");
|
return tr("Has attachments");
|
||||||
case Totp:
|
case Totp:
|
||||||
return tr("Has TOTP one-time password");
|
return tr("Has TOTP");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +46,8 @@ public:
|
|||||||
Paperclip = 10,
|
Paperclip = 10,
|
||||||
Attachments = 11,
|
Attachments = 11,
|
||||||
Totp = 12,
|
Totp = 12,
|
||||||
Size = 13
|
Size = 13,
|
||||||
|
PasswordStrength = 14
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit EntryModel(QObject* parent = nullptr);
|
explicit EntryModel(QObject* parent = nullptr);
|
||||||
|
@ -20,12 +20,42 @@
|
|||||||
|
|
||||||
#include <QAccessible>
|
#include <QAccessible>
|
||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
#include <QKeyEvent>
|
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
|
#include <QPainter>
|
||||||
#include <QShortcut>
|
#include <QShortcut>
|
||||||
|
#include <QStyledItemDelegate>
|
||||||
|
|
||||||
#include "gui/SortFilterHideProxyModel.h"
|
#include "gui/SortFilterHideProxyModel.h"
|
||||||
|
|
||||||
|
#define ICON_ONLY_SECTION_SIZE 26
|
||||||
|
|
||||||
|
class PasswordStrengthItemDelegate : public QStyledItemDelegate
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit PasswordStrengthItemDelegate(QObject* parent)
|
||||||
|
: QStyledItemDelegate(parent){};
|
||||||
|
|
||||||
|
void initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const override
|
||||||
|
{
|
||||||
|
QStyledItemDelegate::initStyleOption(option, index);
|
||||||
|
auto value = index.data(Qt::DecorationRole);
|
||||||
|
if (value.isValid() && value.type() == QVariant::Color && option->rect.width() > 0) {
|
||||||
|
// Rebuild the password strength icon to add a dark border
|
||||||
|
QColor pen(Qt::black);
|
||||||
|
if (option->widget) {
|
||||||
|
pen = option->widget->palette().color(QPalette::Shadow);
|
||||||
|
}
|
||||||
|
auto size = option->decorationSize;
|
||||||
|
QImage image(size.width(), size.height(), QImage::Format_ARGB32_Premultiplied);
|
||||||
|
QPainter p(&image);
|
||||||
|
p.setBrush(value.value<QColor>());
|
||||||
|
p.setPen(pen);
|
||||||
|
p.drawRect(0, 0, size.width() - 1, size.height() - 1);
|
||||||
|
option->icon = QIcon(QPixmap::fromImage(image));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
EntryView::EntryView(QWidget* parent)
|
EntryView::EntryView(QWidget* parent)
|
||||||
: QTreeView(parent)
|
: QTreeView(parent)
|
||||||
, m_model(new EntryModel(this))
|
, m_model(new EntryModel(this))
|
||||||
@ -41,6 +71,7 @@ EntryView::EntryView(QWidget* parent)
|
|||||||
// Use Qt::UserRole as sort role, see EntryModel::data()
|
// Use Qt::UserRole as sort role, see EntryModel::data()
|
||||||
m_sortModel->setSortRole(Qt::UserRole);
|
m_sortModel->setSortRole(Qt::UserRole);
|
||||||
QTreeView::setModel(m_sortModel);
|
QTreeView::setModel(m_sortModel);
|
||||||
|
QTreeView::setItemDelegateForColumn(EntryModel::PasswordStrength, new PasswordStrengthItemDelegate(this));
|
||||||
|
|
||||||
setUniformRowHeights(true);
|
setUniformRowHeights(true);
|
||||||
setRootIsDecorated(false);
|
setRootIsDecorated(false);
|
||||||
@ -52,10 +83,10 @@ EntryView::EntryView(QWidget* parent)
|
|||||||
// QAbstractItemView::startDrag() uses this property as the default drag action
|
// QAbstractItemView::startDrag() uses this property as the default drag action
|
||||||
setDefaultDropAction(Qt::MoveAction);
|
setDefaultDropAction(Qt::MoveAction);
|
||||||
|
|
||||||
// clang-format off
|
|
||||||
connect(this, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex)));
|
connect(this, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex)));
|
||||||
connect(selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), SLOT(emitEntrySelectionChanged()));
|
connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, [this] {
|
||||||
// clang-format on
|
emit entrySelectionChanged(currentEntry());
|
||||||
|
});
|
||||||
|
|
||||||
new QShortcut(Qt::CTRL + Qt::Key_F10, this, SLOT(contextMenuShortcutPressed()), nullptr, Qt::WidgetShortcut);
|
new QShortcut(Qt::CTRL + Qt::Key_F10, this, SLOT(contextMenuShortcutPressed()), nullptr, Qt::WidgetShortcut);
|
||||||
|
|
||||||
@ -68,13 +99,11 @@ EntryView::EntryView(QWidget* parent)
|
|||||||
for (int visualIndex = 1; visualIndex < header()->count(); ++visualIndex) {
|
for (int visualIndex = 1; visualIndex < header()->count(); ++visualIndex) {
|
||||||
int logicalIndex = header()->logicalIndex(visualIndex);
|
int logicalIndex = header()->logicalIndex(visualIndex);
|
||||||
QString caption = m_model->headerData(logicalIndex, Qt::Horizontal, Qt::DisplayRole).toString();
|
QString caption = m_model->headerData(logicalIndex, Qt::Horizontal, Qt::DisplayRole).toString();
|
||||||
if (logicalIndex == EntryModel::Paperclip) {
|
if (caption.isEmpty()) {
|
||||||
caption = tr("Has attachments", "Entry attachment icon toggle");
|
caption = m_model->headerData(logicalIndex, Qt::Horizontal, Qt::ToolTipRole).toString();
|
||||||
} else if (logicalIndex == EntryModel::Totp) {
|
|
||||||
caption = tr("Has TOTP", "Entry TOTP icon toggle");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QAction* action = m_headerMenu->addAction(caption);
|
auto action = m_headerMenu->addAction(caption);
|
||||||
action->setCheckable(true);
|
action->setCheckable(true);
|
||||||
action->setData(logicalIndex);
|
action->setData(logicalIndex);
|
||||||
m_columnActions->addAction(action);
|
m_columnActions->addAction(action);
|
||||||
@ -82,7 +111,8 @@ EntryView::EntryView(QWidget* parent)
|
|||||||
connect(m_columnActions, SIGNAL(triggered(QAction*)), this, SLOT(toggleColumnVisibility(QAction*)));
|
connect(m_columnActions, SIGNAL(triggered(QAction*)), this, SLOT(toggleColumnVisibility(QAction*)));
|
||||||
connect(header(), &QHeaderView::sortIndicatorChanged, [this](int index, Qt::SortOrder order) {
|
connect(header(), &QHeaderView::sortIndicatorChanged, [this](int index, Qt::SortOrder order) {
|
||||||
Q_UNUSED(order)
|
Q_UNUSED(order)
|
||||||
header()->setSortIndicatorShown(index != EntryModel::Paperclip && index != EntryModel::Totp);
|
header()->setSortIndicatorShown(index != EntryModel::Paperclip && index != EntryModel::Totp
|
||||||
|
&& index != EntryModel::PasswordStrength);
|
||||||
});
|
});
|
||||||
|
|
||||||
m_headerMenu->addSeparator();
|
m_headerMenu->addSeparator();
|
||||||
@ -101,8 +131,6 @@ EntryView::EntryView(QWidget* parent)
|
|||||||
connect(header(), SIGNAL(sectionMoved(int, int, int)), SIGNAL(viewStateChanged()));
|
connect(header(), SIGNAL(sectionMoved(int, int, int)), SIGNAL(viewStateChanged()));
|
||||||
connect(header(), SIGNAL(sectionResized(int, int, int)), SIGNAL(viewStateChanged()));
|
connect(header(), SIGNAL(sectionResized(int, int, int)), SIGNAL(viewStateChanged()));
|
||||||
connect(header(), SIGNAL(sortIndicatorChanged(int, Qt::SortOrder)), SLOT(sortIndicatorChanged(int, Qt::SortOrder)));
|
connect(header(), SIGNAL(sortIndicatorChanged(int, Qt::SortOrder)), SLOT(sortIndicatorChanged(int, Qt::SortOrder)));
|
||||||
|
|
||||||
// clang-format off
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntryView::contextMenuShortcutPressed()
|
void EntryView::contextMenuShortcutPressed()
|
||||||
@ -130,10 +158,10 @@ void EntryView::sortIndicatorChanged(int logicalIndex, Qt::SortOrder order)
|
|||||||
// do not emit any signals, header()->setSortIndicator recursively calls this
|
// do not emit any signals, header()->setSortIndicator recursively calls this
|
||||||
// function and the signals are emitted in the else part
|
// function and the signals are emitted in the else part
|
||||||
} else {
|
} else {
|
||||||
// call emitEntrySelectionChanged even though the selection did not really change
|
// emit entrySelectionChanged even though the selection did not really change
|
||||||
// this triggers the evaluation of the menu activation and anyway, the position
|
// this triggers the evaluation of the menu activation and anyway, the position
|
||||||
// of the selected entry within the widget did change
|
// of the selected entry within the widget did change
|
||||||
emitEntrySelectionChanged();
|
emit entrySelectionChanged(currentEntry());
|
||||||
emit viewStateChanged();
|
emit viewStateChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -228,11 +256,6 @@ void EntryView::emitEntryActivated(const QModelIndex& index)
|
|||||||
emit entryActivated(entry, static_cast<EntryModel::ModelColumn>(m_sortModel->mapToSource(index).column()));
|
emit entryActivated(entry, static_cast<EntryModel::ModelColumn>(m_sortModel->mapToSource(index).column()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntryView::emitEntrySelectionChanged()
|
|
||||||
{
|
|
||||||
emit entrySelectionChanged(currentEntry());
|
|
||||||
}
|
|
||||||
|
|
||||||
void EntryView::setModel(QAbstractItemModel* model)
|
void EntryView::setModel(QAbstractItemModel* model)
|
||||||
{
|
{
|
||||||
Q_UNUSED(model);
|
Q_UNUSED(model);
|
||||||
@ -391,18 +414,15 @@ void EntryView::fitColumnsToContents()
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark icon-only columns as fixed and resize them to their minimum section size.
|
* Mark icon-only columns as fixed and resize them to icon-only section size
|
||||||
*/
|
*/
|
||||||
void EntryView::resetFixedColumns()
|
void EntryView::resetFixedColumns()
|
||||||
{
|
{
|
||||||
if (!isColumnHidden(EntryModel::Paperclip)) {
|
for (const auto& col : {EntryModel::Paperclip, EntryModel::Totp, EntryModel::PasswordStrength}) {
|
||||||
header()->setSectionResizeMode(EntryModel::Paperclip, QHeaderView::Fixed);
|
if (!isColumnHidden(col)) {
|
||||||
header()->resizeSection(EntryModel::Paperclip, header()->minimumSectionSize());
|
header()->setSectionResizeMode(col, QHeaderView::Fixed);
|
||||||
}
|
header()->resizeSection(col, ICON_ONLY_SECTION_SIZE);
|
||||||
|
}
|
||||||
if (!isColumnHidden(EntryModel::Totp)) {
|
|
||||||
header()->setSectionResizeMode(EntryModel::Totp, QHeaderView::Fixed);
|
|
||||||
header()->resizeSection(EntryModel::Totp, header()->minimumSectionSize());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -431,6 +451,7 @@ void EntryView::resetViewToDefaults()
|
|||||||
header()->hideSection(EntryModel::Accessed);
|
header()->hideSection(EntryModel::Accessed);
|
||||||
header()->hideSection(EntryModel::Attachments);
|
header()->hideSection(EntryModel::Attachments);
|
||||||
header()->hideSection(EntryModel::Size);
|
header()->hideSection(EntryModel::Size);
|
||||||
|
header()->hideSection(EntryModel::PasswordStrength);
|
||||||
|
|
||||||
// Reset column order to logical indices
|
// Reset column order to logical indices
|
||||||
for (int i = 0; i < header()->count(); ++i) {
|
for (int i = 0; i < header()->count(); ++i) {
|
||||||
|
@ -63,7 +63,6 @@ protected:
|
|||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void emitEntryActivated(const QModelIndex& index);
|
void emitEntryActivated(const QModelIndex& index);
|
||||||
void emitEntrySelectionChanged();
|
|
||||||
void showHeaderMenu(const QPoint& position);
|
void showHeaderMenu(const QPoint& position);
|
||||||
void toggleColumnVisibility(QAction* action);
|
void toggleColumnVisibility(QAction* action);
|
||||||
void fitColumnsToWindow();
|
void fitColumnsToWindow();
|
||||||
|
@ -41,14 +41,13 @@ namespace
|
|||||||
QPointer<const Group> group;
|
QPointer<const Group> group;
|
||||||
QPointer<const Entry> entry;
|
QPointer<const Entry> entry;
|
||||||
QSharedPointer<PasswordHealth> health;
|
QSharedPointer<PasswordHealth> health;
|
||||||
bool knownBad = false;
|
bool exclude = false;
|
||||||
|
|
||||||
Item(const Group* g, const Entry* e, QSharedPointer<PasswordHealth> h)
|
Item(const Group* g, const Entry* e, QSharedPointer<PasswordHealth> h)
|
||||||
: group(g)
|
: group(g)
|
||||||
, entry(e)
|
, entry(e)
|
||||||
, health(h)
|
, health(h)
|
||||||
, knownBad(e->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
|
, exclude(e->excludeFromReports())
|
||||||
&& e->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR)
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,7 +120,7 @@ Health::Health(QSharedPointer<Database> db)
|
|||||||
|
|
||||||
// Evaluate this entry
|
// Evaluate this entry
|
||||||
const auto item = QSharedPointer<Item>(new Item(group, entry, m_checker.evaluate(entry)));
|
const auto item = QSharedPointer<Item>(new Item(group, entry, m_checker.evaluate(entry)));
|
||||||
if (item->knownBad) {
|
if (item->exclude) {
|
||||||
m_anyKnownBad = true;
|
m_anyKnownBad = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,7 +139,6 @@ Health::Health(QSharedPointer<Database> db)
|
|||||||
ReportsWidgetHealthcheck::ReportsWidgetHealthcheck(QWidget* parent)
|
ReportsWidgetHealthcheck::ReportsWidgetHealthcheck(QWidget* parent)
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
, m_ui(new Ui::ReportsWidgetHealthcheck())
|
, m_ui(new Ui::ReportsWidgetHealthcheck())
|
||||||
, m_errorIcon(icons()->icon("dialog-error"))
|
|
||||||
, m_referencesModel(new QStandardItemModel(this))
|
, m_referencesModel(new QStandardItemModel(this))
|
||||||
, m_modelProxy(new ReportSortProxyModel(this))
|
, m_modelProxy(new ReportSortProxyModel(this))
|
||||||
{
|
{
|
||||||
@ -258,18 +256,18 @@ void ReportsWidgetHealthcheck::calculateHealth()
|
|||||||
const QScopedPointer<Health> health(AsyncTask::runAndWaitForFuture([this] { return new Health(m_db); }));
|
const QScopedPointer<Health> health(AsyncTask::runAndWaitForFuture([this] { return new Health(m_db); }));
|
||||||
|
|
||||||
// Display entries that are marked as "known bad"?
|
// Display entries that are marked as "known bad"?
|
||||||
const auto showKnownBad = m_ui->showKnownBadCheckBox->isChecked();
|
const auto showExcluded = m_ui->showKnownBadCheckBox->isChecked();
|
||||||
|
|
||||||
// Display the entries
|
// Display the entries
|
||||||
m_rowToEntry.clear();
|
m_rowToEntry.clear();
|
||||||
for (const auto& item : health->items()) {
|
for (const auto& item : health->items()) {
|
||||||
if (item->knownBad && !showKnownBad) {
|
if (item->exclude && !showExcluded) {
|
||||||
// Exclude this entry from the report
|
// Exclude this entry from the report
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show the entry in the report
|
// Show the entry in the report
|
||||||
addHealthRow(item->health, item->group, item->entry, item->knownBad);
|
addHealthRow(item->health, item->group, item->entry, item->exclude);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the table header
|
// Set the table header
|
||||||
@ -330,12 +328,16 @@ void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos)
|
|||||||
connect(edit, SIGNAL(triggered()), SLOT(editFromContextmenu()));
|
connect(edit, SIGNAL(triggered()), SLOT(editFromContextmenu()));
|
||||||
|
|
||||||
// Create the "exclude from reports" menu item
|
// Create the "exclude from reports" menu item
|
||||||
const auto knownbad = 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);
|
||||||
knownbad->setCheckable(true);
|
exclude->setCheckable(true);
|
||||||
knownbad->setChecked(m_contextmenuEntry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
|
exclude->setChecked(m_contextmenuEntry->excludeFromReports());
|
||||||
&& m_contextmenuEntry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR);
|
menu->addAction(exclude);
|
||||||
menu->addAction(knownbad);
|
connect(exclude, &QAction::toggled, exclude, [this](bool state) {
|
||||||
connect(knownbad, SIGNAL(toggled(bool)), SLOT(toggleKnownBad(bool)));
|
if (m_contextmenuEntry) {
|
||||||
|
m_contextmenuEntry->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));
|
||||||
@ -348,17 +350,6 @@ void ReportsWidgetHealthcheck::editFromContextmenu()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReportsWidgetHealthcheck::toggleKnownBad(bool isKnownBad)
|
|
||||||
{
|
|
||||||
if (!m_contextmenuEntry) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_contextmenuEntry->customData()->set(PasswordHealth::OPTION_KNOWN_BAD, isKnownBad ? TRUE_STR : FALSE_STR);
|
|
||||||
|
|
||||||
calculateHealth();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ReportsWidgetHealthcheck::saveSettings()
|
void ReportsWidgetHealthcheck::saveSettings()
|
||||||
{
|
{
|
||||||
// nothing to do - the tab is passive
|
// nothing to do - the tab is passive
|
||||||
|
@ -41,7 +41,7 @@ class ReportsWidgetHealthcheck : public QWidget
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit ReportsWidgetHealthcheck(QWidget* parent = nullptr);
|
explicit ReportsWidgetHealthcheck(QWidget* parent = nullptr);
|
||||||
~ReportsWidgetHealthcheck();
|
~ReportsWidgetHealthcheck() override;
|
||||||
|
|
||||||
void loadSettings(QSharedPointer<Database> db);
|
void loadSettings(QSharedPointer<Database> db);
|
||||||
void saveSettings();
|
void saveSettings();
|
||||||
@ -57,7 +57,6 @@ public slots:
|
|||||||
void emitEntryActivated(const QModelIndex& index);
|
void emitEntryActivated(const QModelIndex& index);
|
||||||
void customMenuRequested(QPoint);
|
void customMenuRequested(QPoint);
|
||||||
void editFromContextmenu();
|
void editFromContextmenu();
|
||||||
void toggleKnownBad(bool);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void addHealthRow(QSharedPointer<PasswordHealth>, const Group*, const Entry*, bool knownBad);
|
void addHealthRow(QSharedPointer<PasswordHealth>, const Group*, const Entry*, bool knownBad);
|
||||||
@ -65,7 +64,6 @@ private:
|
|||||||
QScopedPointer<Ui::ReportsWidgetHealthcheck> m_ui;
|
QScopedPointer<Ui::ReportsWidgetHealthcheck> m_ui;
|
||||||
|
|
||||||
bool m_healthCalculated = false;
|
bool m_healthCalculated = false;
|
||||||
QIcon m_errorIcon;
|
|
||||||
QScopedPointer<QStandardItemModel> m_referencesModel;
|
QScopedPointer<QStandardItemModel> m_referencesModel;
|
||||||
QScopedPointer<QSortFilterProxyModel> m_modelProxy;
|
QScopedPointer<QSortFilterProxyModel> m_modelProxy;
|
||||||
QSharedPointer<Database> m_db;
|
QSharedPointer<Database> m_db;
|
||||||
|
@ -32,20 +32,6 @@
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
/*
|
|
||||||
* Check if an entry has been marked as "known bad password".
|
|
||||||
* These entries are to be excluded from the HIBP report.
|
|
||||||
*
|
|
||||||
* Question to reviewer: Should this be a member function of Entry?
|
|
||||||
* It's duplicated in EditEntryWidget::setForms, EditEntryWidget::updateEntryData,
|
|
||||||
* ReportsWidgetHealthcheck::customMenuRequested, and Health::Item::Item.
|
|
||||||
*/
|
|
||||||
bool isKnownBad(const Entry* entry)
|
|
||||||
{
|
|
||||||
return entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
|
|
||||||
&& entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ReportSortProxyModel : public QSortFilterProxyModel
|
class ReportSortProxyModel : public QSortFilterProxyModel
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@ -153,25 +139,23 @@ void ReportsWidgetHibp::makeHibpTable()
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Display entries that are marked as "known bad"?
|
// Display entries that are marked as "known bad"?
|
||||||
const auto showKnownBad = m_ui->showKnownBadCheckBox->isChecked();
|
const auto showExcluded = m_ui->showKnownBadCheckBox->isChecked();
|
||||||
|
|
||||||
// The colors for table cells
|
// The colors for table cells
|
||||||
const auto red = QBrush("red");
|
const auto red = QBrush("red");
|
||||||
|
|
||||||
// Build the table
|
// Build the table
|
||||||
bool anyKnownBad = false;
|
bool anyExcluded = false;
|
||||||
for (const auto& item : items) {
|
for (const auto& item : items) {
|
||||||
const auto entry = item.first;
|
const auto entry = item.first;
|
||||||
const auto group = entry->group();
|
const auto group = entry->group();
|
||||||
const auto count = item.second;
|
const auto count = item.second;
|
||||||
auto title = entry->title();
|
auto title = entry->title();
|
||||||
|
|
||||||
// If the entry is marked as known bad, hide it unless the
|
// Hide entry if excluded unless explicitly requested
|
||||||
// checkbox is set.
|
if (entry->excludeFromReports()) {
|
||||||
bool knownBad = isKnownBad(entry);
|
anyExcluded = true;
|
||||||
if (knownBad) {
|
if (!showExcluded) {
|
||||||
anyKnownBad = true;
|
|
||||||
if (!showKnownBad) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +167,7 @@ void ReportsWidgetHibp::makeHibpTable()
|
|||||||
<< new QStandardItem(group->iconPixmap(), group->hierarchy().join("/"))
|
<< new QStandardItem(group->iconPixmap(), group->hierarchy().join("/"))
|
||||||
<< new QStandardItem(countToText(count));
|
<< new QStandardItem(countToText(count));
|
||||||
|
|
||||||
if (knownBad) {
|
if (entry->excludeFromReports()) {
|
||||||
row[1]->setToolTip(tr("This entry is being excluded from reports"));
|
row[1]->setToolTip(tr("This entry is being excluded from reports"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,7 +197,7 @@ void ReportsWidgetHibp::makeHibpTable()
|
|||||||
|
|
||||||
// Show the "show known bad entries" checkbox if there's any known
|
// Show the "show known bad entries" checkbox if there's any known
|
||||||
// bad entry in the database.
|
// bad entry in the database.
|
||||||
if (anyKnownBad) {
|
if (anyExcluded) {
|
||||||
m_ui->showKnownBadCheckBox->show();
|
m_ui->showKnownBadCheckBox->show();
|
||||||
} else {
|
} else {
|
||||||
m_ui->showKnownBadCheckBox->hide();
|
m_ui->showKnownBadCheckBox->hide();
|
||||||
@ -331,7 +315,7 @@ void ReportsWidgetHibp::emitEntryActivated(const QModelIndex& index)
|
|||||||
// Found it, invoke entry editor
|
// Found it, invoke entry editor
|
||||||
m_editedEntry = entry;
|
m_editedEntry = entry;
|
||||||
m_editedPassword = entry->password();
|
m_editedPassword = entry->password();
|
||||||
m_editedKnownBad = isKnownBad(entry);
|
m_editedExcluded = entry->excludeFromReports();
|
||||||
emit entryActivated(const_cast<Entry*>(entry));
|
emit entryActivated(const_cast<Entry*>(entry));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -350,7 +334,7 @@ void ReportsWidgetHibp::refreshAfterEdit()
|
|||||||
// No need to re-validate if there was no change that affects
|
// No need to re-validate if there was no change that affects
|
||||||
// the HIBP result (i. e., change to the password or to the
|
// the HIBP result (i. e., change to the password or to the
|
||||||
// "known bad" flag)
|
// "known bad" flag)
|
||||||
if (m_editedEntry->password() == m_editedPassword && isKnownBad(m_editedEntry) == m_editedKnownBad) {
|
if (m_editedEntry->password() == m_editedPassword && m_editedEntry->excludeFromReports() == m_editedExcluded) {
|
||||||
// Don't go through HIBP but still rebuild the table, the user might
|
// Don't go through HIBP but still rebuild the table, the user might
|
||||||
// have edited the entry title.
|
// have edited the entry title.
|
||||||
makeHibpTable();
|
makeHibpTable();
|
||||||
@ -392,12 +376,16 @@ void ReportsWidgetHibp::customMenuRequested(QPoint pos)
|
|||||||
connect(edit, SIGNAL(triggered()), SLOT(editFromContextmenu()));
|
connect(edit, SIGNAL(triggered()), SLOT(editFromContextmenu()));
|
||||||
|
|
||||||
// Create the "exclude from reports" menu item
|
// Create the "exclude from reports" menu item
|
||||||
const auto knownbad = 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);
|
||||||
knownbad->setCheckable(true);
|
exclude->setCheckable(true);
|
||||||
knownbad->setChecked(m_contextmenuEntry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
|
exclude->setChecked(m_contextmenuEntry->excludeFromReports());
|
||||||
&& m_contextmenuEntry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR);
|
menu->addAction(exclude);
|
||||||
menu->addAction(knownbad);
|
connect(exclude, &QAction::toggled, exclude, [this](bool state) {
|
||||||
connect(knownbad, SIGNAL(toggled(bool)), SLOT(toggleKnownBad(bool)));
|
if (m_contextmenuEntry) {
|
||||||
|
m_contextmenuEntry->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));
|
||||||
@ -410,17 +398,6 @@ void ReportsWidgetHibp::editFromContextmenu()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReportsWidgetHibp::toggleKnownBad(bool isKnownBad)
|
|
||||||
{
|
|
||||||
if (!m_contextmenuEntry) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_contextmenuEntry->customData()->set(PasswordHealth::OPTION_KNOWN_BAD, isKnownBad ? TRUE_STR : FALSE_STR);
|
|
||||||
|
|
||||||
makeHibpTable();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ReportsWidgetHibp::saveSettings()
|
void ReportsWidgetHibp::saveSettings()
|
||||||
{
|
{
|
||||||
// nothing to do - the tab is passive
|
// nothing to do - the tab is passive
|
||||||
|
@ -62,7 +62,6 @@ public slots:
|
|||||||
void makeHibpTable();
|
void makeHibpTable();
|
||||||
void customMenuRequested(QPoint);
|
void customMenuRequested(QPoint);
|
||||||
void editFromContextmenu();
|
void editFromContextmenu();
|
||||||
void toggleKnownBad(bool);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void startValidation();
|
void startValidation();
|
||||||
@ -78,7 +77,7 @@ private:
|
|||||||
QList<const Entry*> m_rowToEntry; // List index is table row
|
QList<const Entry*> m_rowToEntry; // List index is table row
|
||||||
QPointer<const Entry> m_editedEntry; // The entry we're currently editing
|
QPointer<const 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_editedKnownBad; // 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
|
Entry* m_contextmenuEntry = nullptr; // The entry that was right-clicked
|
||||||
|
|
||||||
#ifdef WITH_XC_NETWORKING
|
#ifdef WITH_XC_NETWORKING
|
||||||
|
@ -26,8 +26,6 @@
|
|||||||
#include "core/PasswordHealth.h"
|
#include "core/PasswordHealth.h"
|
||||||
#include "gui/Icons.h"
|
#include "gui/Icons.h"
|
||||||
|
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QHash>
|
|
||||||
#include <QStandardItemModel>
|
#include <QStandardItemModel>
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
@ -37,15 +35,15 @@ namespace
|
|||||||
public:
|
public:
|
||||||
// The statistics we collect:
|
// The statistics we collect:
|
||||||
QDateTime modified; // File modification time
|
QDateTime modified; // File modification time
|
||||||
int nGroups = 0; // Number of groups in the database
|
int groupCount = 0; // Number of groups in the database
|
||||||
int nEntries = 0; // Number of entries (across all groups)
|
int entryCount = 0; // Number of entries (across all groups)
|
||||||
int nExpired = 0; // Number of expired entries
|
int expiredEntries = 0; // Number of expired entries
|
||||||
int nPwdsWeak = 0; // Number of weak or poor passwords
|
int excludedEntries = 0; // Number of known bad entries
|
||||||
int nPwdsShort = 0; // Number of passwords 8 characters or less in size
|
int weakPasswords = 0; // Number of weak or poor passwords
|
||||||
int nPwdsUnique = 0; // Number of unique passwords
|
int shortPasswords = 0; // Number of passwords 8 characters or less in size
|
||||||
int nPwdsReused = 0; // Number of non-unique passwords
|
int uniquePasswords = 0; // Number of unique passwords
|
||||||
int nKnownBad = 0; // Number of known bad entries
|
int reusedPasswords = 0; // Number of non-unique passwords
|
||||||
int pwdTotalLen = 0; // Total length of all passwords
|
int totalPasswordLength = 0; // Total length of all passwords
|
||||||
|
|
||||||
// Ctor does all the work
|
// Ctor does all the work
|
||||||
explicit Stats(QSharedPointer<Database> db)
|
explicit Stats(QSharedPointer<Database> db)
|
||||||
@ -58,8 +56,8 @@ namespace
|
|||||||
// Get average password length
|
// Get average password length
|
||||||
int averagePwdLength() const
|
int averagePwdLength() const
|
||||||
{
|
{
|
||||||
const auto nPwds = nPwdsUnique + nPwdsReused;
|
const auto passwords = uniquePasswords + reusedPasswords;
|
||||||
return nPwds == 0 ? 0 : std::round(pwdTotalLen / double(nPwds));
|
return passwords == 0 ? 0 : std::round(totalPasswordLength / double(passwords));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get max number of password reuse (=how many entries
|
// Get max number of password reuse (=how many entries
|
||||||
@ -77,12 +75,12 @@ namespace
|
|||||||
// following returns true.
|
// following returns true.
|
||||||
bool isAnyExpired() const
|
bool isAnyExpired() const
|
||||||
{
|
{
|
||||||
return nExpired > 0;
|
return expiredEntries > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool areTooManyPwdsReused() const
|
bool areTooManyPwdsReused() const
|
||||||
{
|
{
|
||||||
return nPwdsReused > nPwdsUnique / 10;
|
return reusedPasswords > uniquePasswords / 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool arePwdsReusedTooOften() const
|
bool arePwdsReusedTooOften() const
|
||||||
@ -109,7 +107,7 @@ namespace
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
++nGroups;
|
++groupCount;
|
||||||
|
|
||||||
for (const auto* entry : group->entries()) {
|
for (const auto* entry : group->entries()) {
|
||||||
// Don't count anything in the recycle bin
|
// Don't count anything in the recycle bin
|
||||||
@ -117,36 +115,35 @@ namespace
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
++nEntries;
|
++entryCount;
|
||||||
|
|
||||||
if (entry->isExpired()) {
|
if (entry->isExpired()) {
|
||||||
++nExpired;
|
++expiredEntries;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get password statistics
|
// Get password statistics
|
||||||
const auto pwd = entry->password();
|
const auto pwd = entry->password();
|
||||||
if (!pwd.isEmpty()) {
|
if (!pwd.isEmpty()) {
|
||||||
if (!m_passwords.contains(pwd)) {
|
if (!m_passwords.contains(pwd)) {
|
||||||
++nPwdsUnique;
|
++uniquePasswords;
|
||||||
} else {
|
} else {
|
||||||
++nPwdsReused;
|
++reusedPasswords;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pwd.size() < 8) {
|
if (pwd.size() < 8) {
|
||||||
++nPwdsShort;
|
++shortPasswords;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Speed up Zxcvbn process by excluding very long passwords and most passphrases
|
// Speed up Zxcvbn process by excluding very long passwords and most passphrases
|
||||||
if (pwd.size() < 25 && checker.evaluate(entry)->quality() <= PasswordHealth::Quality::Weak) {
|
if (pwd.size() < 25 && checker.evaluate(entry)->quality() <= PasswordHealth::Quality::Weak) {
|
||||||
++nPwdsWeak;
|
++weakPasswords;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD)
|
if (entry->excludeFromReports()) {
|
||||||
&& entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR) {
|
++excludedEntries;
|
||||||
++nKnownBad;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pwdTotalLen += pwd.size();
|
totalPasswordLength += pwd.size();
|
||||||
m_passwords[pwd]++;
|
m_passwords[pwd]++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -220,15 +217,15 @@ void ReportsWidgetStatistics::calculateStats()
|
|||||||
m_db->isModified() ? tr("yes") : tr("no"),
|
m_db->isModified() ? tr("yes") : tr("no"),
|
||||||
m_db->isModified(),
|
m_db->isModified(),
|
||||||
tr("The database was modified, but the changes have not yet been saved to disk."));
|
tr("The database was modified, but the changes have not yet been saved to disk."));
|
||||||
addStatsRow(tr("Number of groups"), QString::number(stats->nGroups));
|
addStatsRow(tr("Number of groups"), QString::number(stats->groupCount));
|
||||||
addStatsRow(tr("Number of entries"), QString::number(stats->nEntries));
|
addStatsRow(tr("Number of entries"), QString::number(stats->entryCount));
|
||||||
addStatsRow(tr("Number of expired entries"),
|
addStatsRow(tr("Number of expired entries"),
|
||||||
QString::number(stats->nExpired),
|
QString::number(stats->expiredEntries),
|
||||||
stats->isAnyExpired(),
|
stats->isAnyExpired(),
|
||||||
tr("The database contains entries that have expired."));
|
tr("The database contains entries that have expired."));
|
||||||
addStatsRow(tr("Unique passwords"), QString::number(stats->nPwdsUnique));
|
addStatsRow(tr("Unique passwords"), QString::number(stats->uniquePasswords));
|
||||||
addStatsRow(tr("Non-unique passwords"),
|
addStatsRow(tr("Non-unique passwords"),
|
||||||
QString::number(stats->nPwdsReused),
|
QString::number(stats->reusedPasswords),
|
||||||
stats->areTooManyPwdsReused(),
|
stats->areTooManyPwdsReused(),
|
||||||
tr("More than 10% of passwords are reused. Use unique passwords when possible."));
|
tr("More than 10% of passwords are reused. Use unique passwords when possible."));
|
||||||
addStatsRow(tr("Maximum password reuse"),
|
addStatsRow(tr("Maximum password reuse"),
|
||||||
@ -236,16 +233,16 @@ void ReportsWidgetStatistics::calculateStats()
|
|||||||
stats->arePwdsReusedTooOften(),
|
stats->arePwdsReusedTooOften(),
|
||||||
tr("Some passwords are used more than three times. Use unique passwords when possible."));
|
tr("Some passwords are used more than three times. Use unique passwords when possible."));
|
||||||
addStatsRow(tr("Number of short passwords"),
|
addStatsRow(tr("Number of short passwords"),
|
||||||
QString::number(stats->nPwdsShort),
|
QString::number(stats->shortPasswords),
|
||||||
stats->nPwdsShort > 0,
|
stats->shortPasswords > 0,
|
||||||
tr("Recommended minimum password length is at least 8 characters."));
|
tr("Recommended minimum password length is at least 8 characters."));
|
||||||
addStatsRow(tr("Number of weak passwords"),
|
addStatsRow(tr("Number of weak passwords"),
|
||||||
QString::number(stats->nPwdsWeak),
|
QString::number(stats->weakPasswords),
|
||||||
stats->nPwdsWeak > 0,
|
stats->weakPasswords > 0,
|
||||||
tr("Recommend using long, randomized passwords with a rating of 'good' or 'excellent'."));
|
tr("Recommend using long, randomized passwords with a rating of 'good' or 'excellent'."));
|
||||||
addStatsRow(tr("Entries excluded from reports"),
|
addStatsRow(tr("Entries excluded from reports"),
|
||||||
QString::number(stats->nKnownBad),
|
QString::number(stats->excludedEntries),
|
||||||
stats->nKnownBad > 0,
|
stats->excludedEntries > 0,
|
||||||
tr("Excluding entries from reports, e. g. because they are known to have a poor password, isn't "
|
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."));
|
"necessarily a problem but you should keep an eye on them."));
|
||||||
addStatsRow(tr("Average password length"),
|
addStatsRow(tr("Average password length"),
|
||||||
|
@ -321,7 +321,7 @@ void TestEntryModel::testProxyModel()
|
|||||||
*/
|
*/
|
||||||
QSignalSpy spyColumnRemove(modelProxy, SIGNAL(columnsAboutToBeRemoved(QModelIndex, int, int)));
|
QSignalSpy spyColumnRemove(modelProxy, SIGNAL(columnsAboutToBeRemoved(QModelIndex, int, int)));
|
||||||
modelProxy->hideColumn(0, true);
|
modelProxy->hideColumn(0, true);
|
||||||
QCOMPARE(modelProxy->columnCount(), 13);
|
QCOMPARE(modelProxy->columnCount(), 14);
|
||||||
QVERIFY(!spyColumnRemove.isEmpty());
|
QVERIFY(!spyColumnRemove.isEmpty());
|
||||||
|
|
||||||
int oldSpyColumnRemoveSize = spyColumnRemove.size();
|
int oldSpyColumnRemoveSize = spyColumnRemove.size();
|
||||||
@ -343,7 +343,7 @@ void TestEntryModel::testProxyModel()
|
|||||||
*/
|
*/
|
||||||
QSignalSpy spyColumnInsert(modelProxy, SIGNAL(columnsAboutToBeInserted(QModelIndex, int, int)));
|
QSignalSpy spyColumnInsert(modelProxy, SIGNAL(columnsAboutToBeInserted(QModelIndex, int, int)));
|
||||||
modelProxy->hideColumn(0, false);
|
modelProxy->hideColumn(0, false);
|
||||||
QCOMPARE(modelProxy->columnCount(), 14);
|
QCOMPARE(modelProxy->columnCount(), 15);
|
||||||
QVERIFY(!spyColumnInsert.isEmpty());
|
QVERIFY(!spyColumnInsert.isEmpty());
|
||||||
|
|
||||||
int oldSpyColumnInsertSize = spyColumnInsert.size();
|
int oldSpyColumnInsertSize = spyColumnInsert.size();
|
||||||
|
@ -461,14 +461,13 @@ void TestGui::testEditEntry()
|
|||||||
|
|
||||||
// Test the "known bad" checkbox
|
// Test the "known bad" checkbox
|
||||||
editEntryWidget->setCurrentPage(1);
|
editEntryWidget->setCurrentPage(1);
|
||||||
auto knownBadCheckBox = editEntryWidget->findChild<QCheckBox*>("knownBadCheckBox");
|
auto excludeReportsCheckBox = editEntryWidget->findChild<QCheckBox*>("excludeReportsCheckBox");
|
||||||
QVERIFY(knownBadCheckBox);
|
QVERIFY(excludeReportsCheckBox);
|
||||||
QCOMPARE(knownBadCheckBox->isChecked(), false);
|
QCOMPARE(excludeReportsCheckBox->isChecked(), false);
|
||||||
knownBadCheckBox->setChecked(true);
|
excludeReportsCheckBox->setChecked(true);
|
||||||
QTest::mouseClick(applyButton, Qt::LeftButton);
|
QTest::mouseClick(applyButton, Qt::LeftButton);
|
||||||
QCOMPARE(entry->historyItems().size(), ++editCount);
|
QCOMPARE(entry->historyItems().size(), ++editCount);
|
||||||
QCOMPARE(entry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD), true);
|
QVERIFY(entry->excludeFromReports());
|
||||||
QCOMPARE(entry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD), TRUE_STR);
|
|
||||||
|
|
||||||
// Test entry colors (simulate choosing a color)
|
// Test entry colors (simulate choosing a color)
|
||||||
editEntryWidget->setCurrentPage(1);
|
editEntryWidget->setCurrentPage(1);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user