/* * Copyright (C) 2019 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 or (at your option) * version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ReportsWidgetHealthcheck.h" #include "ui_ReportsWidgetHealthcheck.h" #include "core/AsyncTask.h" #include "core/Database.h" #include "core/Global.h" #include "core/Group.h" #include "core/PasswordHealth.h" #include "core/Resources.h" #include "gui/styles/StateColorPalette.h" #include #include #include #include namespace { class Health { public: struct Item { QPointer group; QPointer entry; QSharedPointer health; bool knownBad = false; Item(const Group* g, const Entry* e, QSharedPointer h) : group(g) , entry(e) , health(h) , knownBad(e->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD) && e->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR) { } bool operator<(const Item& rhs) const { return health->score() < rhs.health->score(); } }; explicit Health(QSharedPointer); const QList>& items() const { return m_items; } bool anyKnownBad() const { return m_anyKnownBad; } private: QSharedPointer m_db; HealthChecker m_checker; QList> m_items; bool m_anyKnownBad = false; }; } // namespace Health::Health(QSharedPointer db) : m_db(db) , m_checker(db) { for (const auto* group : db->rootGroup()->groupsRecursive(true)) { // Skip recycle bin if (group->isRecycled()) { continue; } for (const auto* entry : group->entries()) { if (entry->isRecycled()) { continue; } // Skip entries with empty password if (entry->password().isEmpty()) { continue; } // Evaluate this entry const auto item = QSharedPointer(new Item(group, entry, m_checker.evaluate(entry))); if (item->knownBad) { m_anyKnownBad = true; } // Add entry if its password isn't at least "good" if (item->health->quality() < PasswordHealth::Quality::Good) { m_items.append(item); } } } // Sort the result so that the worst passwords (least score) // are at the top std::sort(m_items.begin(), m_items.end(), [](QSharedPointer x, QSharedPointer y) { return *x < *y; }); } ReportsWidgetHealthcheck::ReportsWidgetHealthcheck(QWidget* parent) : QWidget(parent) , m_ui(new Ui::ReportsWidgetHealthcheck()) , m_errorIcon(Resources::instance()->icon("dialog-error")) , m_referencesModel(new QStandardItemModel(this)) , m_modelProxy(new QSortFilterProxyModel(this)) { m_ui->setupUi(this); m_modelProxy->setSourceModel(m_referencesModel.data()); m_ui->healthcheckTableView->setModel(m_modelProxy.data()); m_ui->healthcheckTableView->setSelectionMode(QAbstractItemView::NoSelection); m_ui->healthcheckTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); m_ui->healthcheckTableView->setSortingEnabled(true); connect(m_ui->healthcheckTableView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customMenuRequested(QPoint))); connect(m_ui->healthcheckTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex))); connect(m_ui->showKnownBadCheckBox, SIGNAL(stateChanged(int)), this, SLOT(calculateHealth())); } ReportsWidgetHealthcheck::~ReportsWidgetHealthcheck() { } void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer health, const Group* group, const Entry* entry, bool knownBad) { QString descr, tip; QColor qualityColor; StateColorPalette statePalette; const auto quality = health->quality(); switch (quality) { case PasswordHealth::Quality::Bad: descr = tr("Bad", "Password quality"); tip = tr("Bad — password must be changed"); qualityColor = statePalette.color(StateColorPalette::HealthCritical); break; case PasswordHealth::Quality::Poor: descr = tr("Poor", "Password quality"); tip = tr("Poor — password should be changed"); qualityColor = statePalette.color(StateColorPalette::HealthBad); break; case PasswordHealth::Quality::Weak: descr = tr("Weak", "Password quality"); tip = tr("Weak — consider changing the password"); qualityColor = statePalette.color(StateColorPalette::HealthWeak); break; case PasswordHealth::Quality::Good: case PasswordHealth::Quality::Excellent: qualityColor = statePalette.color(StateColorPalette::HealthOk); break; } auto title = entry->title(); if (knownBad) { title.append(tr(" (Excluded)")); } auto row = QList(); row << new QStandardItem(descr); row << new QStandardItem(entry->iconPixmap(), title); row << new QStandardItem(group->iconPixmap(), group->hierarchy().join("/")); row << new QStandardItem(QString::number(health->score())); row << new QStandardItem(health->scoreReason()); // Set background color of first column according to password quality. // Set the same as foreground color so the description is usually // invisible, it's just for screen readers etc. QBrush brush(qualityColor); row[0]->setForeground(brush); row[0]->setBackground(brush); // Set tooltips row[0]->setToolTip(tip); if (knownBad) { row[1]->setToolTip(tr("This entry is being excluded from reports")); } row[4]->setToolTip(health->scoreDetails()); // Store entry pointer per table row (used in double click handler) m_referencesModel->appendRow(row); m_rowToEntry.append({group, entry}); } void ReportsWidgetHealthcheck::loadSettings(QSharedPointer db) { m_db = std::move(db); m_healthCalculated = false; m_referencesModel->clear(); m_rowToEntry.clear(); auto row = QList(); row << new QStandardItem(tr("Please wait, health data is being calculated...")); m_referencesModel->appendRow(row); } void ReportsWidgetHealthcheck::showEvent(QShowEvent* event) { QWidget::showEvent(event); if (!m_healthCalculated) { // Perform stats calculation on next event loop to allow widget to appear m_healthCalculated = true; QTimer::singleShot(0, this, SLOT(calculateHealth())); } } void ReportsWidgetHealthcheck::calculateHealth() { m_referencesModel->clear(); // Perform the health check const QScopedPointer health(AsyncTask::runAndWaitForFuture([this] { return new Health(m_db); })); // Display entries that are marked as "known bad"? const auto showKnownBad = m_ui->showKnownBadCheckBox->isChecked(); // Display the entries m_rowToEntry.clear(); for (const auto& item : health->items()) { if (item->knownBad && !showKnownBad) { // Exclude this entry from the report continue; } // Show the entry in the report addHealthRow(item->health, item->group, item->entry, item->knownBad); } // Set the table header if (m_referencesModel->rowCount() == 0) { m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Congratulations, everything is healthy!")); } else { m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("") << tr("Title") << tr("Path") << tr("Score") << tr("Reason")); } m_ui->healthcheckTableView->resizeRowsToContents(); // Show the "show known bad entries" checkbox if there's any known // bad entry in the database. if (health->anyKnownBad()) { m_ui->showKnownBadCheckBox->show(); } else { m_ui->showKnownBadCheckBox->hide(); } } void ReportsWidgetHealthcheck::emitEntryActivated(const QModelIndex& index) { if (!index.isValid()) { return; } auto mappedIndex = m_modelProxy->mapToSource(index); const auto row = m_rowToEntry[mappedIndex.row()]; const auto group = row.first; const auto entry = row.second; if (group && entry) { emit entryActivated(const_cast(entry)); } } void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos) { // Find which entry has been clicked const auto index = m_ui->healthcheckTableView->indexAt(pos); if (!index.isValid()) { return; } auto mappedIndex = m_modelProxy->mapToSource(index); m_contextmenuEntry = const_cast(m_rowToEntry[mappedIndex.row()].second); if (!m_contextmenuEntry) { return; } // Create the context menu const auto menu = new QMenu(this); // Create the "edit entry" menu item const auto edit = new QAction(Resources::instance()->icon("entry-edit"), tr("Edit Entry..."), this); menu->addAction(edit); connect(edit, SIGNAL(triggered()), SLOT(editFromContextmenu())); // Create the "exclude from reports" menu item const auto knownbad = new QAction(Resources::instance()->icon("reports-exclude"), tr("Exclude from reports"), this); knownbad->setCheckable(true); knownbad->setChecked(m_contextmenuEntry->customData()->contains(PasswordHealth::OPTION_KNOWN_BAD) && m_contextmenuEntry->customData()->value(PasswordHealth::OPTION_KNOWN_BAD) == TRUE_STR); menu->addAction(knownbad); connect(knownbad, SIGNAL(toggled(bool)), SLOT(toggleKnownBad(bool))); // Show the context menu menu->popup(m_ui->healthcheckTableView->viewport()->mapToGlobal(pos)); } void ReportsWidgetHealthcheck::editFromContextmenu() { if (m_contextmenuEntry) { emit entryActivated(m_contextmenuEntry); } } 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() { // nothing to do - the tab is passive }