From cd642e7fee3f46847cc5020f33058d4bceef6805 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sami=20V=C3=A4nttinen?= <sami.vanttinen@protonmail.com>
Date: Thu, 30 Dec 2021 14:31:06 +0200
Subject: [PATCH] Add support for Browser statistics (#7197)

Co-authored-by: Jonathan White <support@dmapps.us>
---
 share/translations/keepassxc_en.ts            |  86 ++++
 src/CMakeLists.txt                            |   2 +
 src/core/Entry.cpp                            |  20 +
 src/core/Entry.h                              |   1 +
 src/gui/reports/ReportsDialog.cpp             |  24 +-
 src/gui/reports/ReportsDialog.h               |   8 +-
 .../reports/ReportsPageBrowserStatistics.cpp  |  53 +++
 .../reports/ReportsPageBrowserStatistics.h    |  39 ++
 .../ReportsWidgetBrowserStatistics.cpp        | 372 ++++++++++++++++++
 .../reports/ReportsWidgetBrowserStatistics.h  |  71 ++++
 .../reports/ReportsWidgetBrowserStatistics.ui | 100 +++++
 src/gui/styles/StateColorPalette.cpp          |   8 +-
 src/gui/styles/StateColorPalette.h            |   6 +-
 13 files changed, 784 insertions(+), 6 deletions(-)
 create mode 100644 src/gui/reports/ReportsPageBrowserStatistics.cpp
 create mode 100644 src/gui/reports/ReportsPageBrowserStatistics.h
 create mode 100644 src/gui/reports/ReportsWidgetBrowserStatistics.cpp
 create mode 100644 src/gui/reports/ReportsWidgetBrowserStatistics.h
 create mode 100644 src/gui/reports/ReportsWidgetBrowserStatistics.ui

diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts
index c69ebe60e..1318fc3d6 100644
--- a/share/translations/keepassxc_en.ts
+++ b/share/translations/keepassxc_en.ts
@@ -7731,6 +7731,10 @@ Please consider generating a new key file.</source>
             <numerusform></numerusform>
         </translation>
     </message>
+    <message>
+        <source>Browser Statistics</source>
+        <translation type="unfinished"></translation>
+    </message>
 </context>
 <context>
     <name>QtIOCompressor</name>
@@ -7766,6 +7770,88 @@ Please consider generating a new key file.</source>
         <translation>Internal zlib error: </translation>
     </message>
 </context>
+<context>
+    <name>ReportsWidgetBrowserStatistics</name>
+    <message>
+        <source>Exclude expired entries from the report</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Show only entries which have URL set</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Show only entries which have browser settings in custom data</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Double-click entries to edit.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>List of entry URLs</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Entry has no URLs set</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Allowed URLs</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Entry has no Browser Integration settings</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Denied URLs</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source> (Excluded)</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>This entry is being excluded from reports</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Please wait, browser statistics is being calculated…</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>No entries with a URL, or none has browser extension settings saved.</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>URLs</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <source>Title</source>
+        <translation type="unfinished">Title</translation>
+    </message>
+    <message>
+        <source>Path</source>
+        <translation type="unfinished">Path</translation>
+    </message>
+    <message>
+        <source>Edit Entry…</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message numerus="yes">
+        <source>Delete Entry(s)…</source>
+        <translation type="unfinished">
+            <numerusform></numerusform>
+            <numerusform></numerusform>
+        </translation>
+    </message>
+    <message>
+        <source>Exclude from reports</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
 <context>
     <name>ReportsWidgetHealthcheck</name>
     <message>
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 1b08910d7..1cdb0dfc9 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -242,6 +242,8 @@ if(WITH_XC_BROWSER)
     set(keepassxcbrowser_LIB keepassxcbrowser)
     set(keepassx_SOURCES ${keepassx_SOURCES} gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp)
     set(keepassx_SOURCES ${keepassx_SOURCES} gui/entry/EntryURLModel.cpp)
+    set(keepassx_SOURCES ${keepassx_SOURCES} gui/reports/ReportsWidgetBrowserStatistics.cpp)
+    set(keepassx_SOURCES ${keepassx_SOURCES} gui/reports/ReportsPageBrowserStatistics.cpp)
 endif()
 
 add_subdirectory(autotype)
diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp
index d6e072223..980ab163f 100644
--- a/src/core/Entry.cpp
+++ b/src/core/Entry.cpp
@@ -355,6 +355,26 @@ QString Entry::url() const
     return m_attributes->value(EntryAttributes::URLKey);
 }
 
+QStringList Entry::getAllUrls() const
+{
+    QStringList urlList;
+
+    if (!url().isEmpty()) {
+        urlList << url();
+    }
+
+    for (const auto& key : m_attributes->keys()) {
+        if (key.startsWith("KP2A_URL")) {
+            auto additionalUrl = m_attributes->value(key);
+            if (!additionalUrl.isEmpty()) {
+                urlList << additionalUrl;
+            }
+        }
+    }
+
+    return urlList;
+}
+
 QString Entry::webUrl() const
 {
     QString url = resolveMultiplePlaceholders(m_attributes->value(EntryAttributes::URLKey));
diff --git a/src/core/Entry.h b/src/core/Entry.h
index 50c3427ee..6227aa1a9 100644
--- a/src/core/Entry.h
+++ b/src/core/Entry.h
@@ -98,6 +98,7 @@ public:
     const AutoTypeAssociations* autoTypeAssociations() const;
     QString title() const;
     QString url() const;
+    QStringList getAllUrls() const;
     QString webUrl() const;
     QString displayUrl() const;
     QString username() const;
diff --git a/src/gui/reports/ReportsDialog.cpp b/src/gui/reports/ReportsDialog.cpp
index ecd015b89..406237459 100644
--- a/src/gui/reports/ReportsDialog.cpp
+++ b/src/gui/reports/ReportsDialog.cpp
@@ -1,5 +1,5 @@
 /*
- *  Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
+ *  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
@@ -21,6 +21,10 @@
 #include "ReportsPageHealthcheck.h"
 #include "ReportsPageHibp.h"
 #include "ReportsPageStatistics.h"
+#ifdef WITH_XC_BROWSER
+#include "ReportsPageBrowserStatistics.h"
+#include "ReportsWidgetBrowserStatistics.h"
+#endif
 #include "ReportsWidgetHealthcheck.h"
 #include "ReportsWidgetHibp.h"
 
@@ -58,14 +62,20 @@ ReportsDialog::ReportsDialog(QWidget* parent)
     , m_healthPage(new ReportsPageHealthcheck())
     , m_hibpPage(new ReportsPageHibp())
     , m_statPage(new ReportsPageStatistics())
+#ifdef WITH_XC_BROWSER
+    , m_browserStatPage(new ReportsPageBrowserStatistics())
+#endif
     , m_editEntryWidget(new EditEntryWidget(this))
 {
     m_ui->setupUi(this);
 
     connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject()));
+    addPage(m_statPage);
+#ifdef WITH_XC_BROWSER
+    addPage(m_browserStatPage);
+#endif
     addPage(m_healthPage);
     addPage(m_hibpPage);
-    addPage(m_statPage);
 
     m_ui->stackedWidget->setCurrentIndex(0);
 
@@ -77,6 +87,11 @@ ReportsDialog::ReportsDialog(QWidget* parent)
     connect(m_ui->categoryList, SIGNAL(categoryChanged(int)), m_ui->stackedWidget, SLOT(setCurrentIndex(int)));
     connect(m_healthPage->m_healthWidget, SIGNAL(entryActivated(Entry*)), SLOT(entryActivationSignalReceived(Entry*)));
     connect(m_hibpPage->m_hibpWidget, SIGNAL(entryActivated(Entry*)), SLOT(entryActivationSignalReceived(Entry*)));
+#ifdef WITH_XC_BROWSER
+    connect(m_browserStatPage->m_browserWidget,
+            SIGNAL(entryActivated(Entry*)),
+            SLOT(entryActivationSignalReceived(Entry*)));
+#endif
     connect(m_editEntryWidget, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
 }
 
@@ -142,6 +157,11 @@ void ReportsDialog::switchToMainView(bool previousDialogAccepted)
         } else if (m_sender == m_hibpPage->m_hibpWidget) {
             m_hibpPage->m_hibpWidget->refreshAfterEdit();
         }
+#ifdef WITH_XC_BROWSER
+        if (m_sender == m_browserStatPage->m_browserWidget) {
+            m_browserStatPage->m_browserWidget->calculateBrowserStatistics();
+        }
+#endif
     }
 
     // Don't process the same sender twice
diff --git a/src/gui/reports/ReportsDialog.h b/src/gui/reports/ReportsDialog.h
index 99da7f25f..25cb623eb 100644
--- a/src/gui/reports/ReportsDialog.h
+++ b/src/gui/reports/ReportsDialog.h
@@ -1,5 +1,5 @@
 /*
- *  Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
+ *  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
@@ -29,6 +29,9 @@ class QTabWidget;
 class ReportsPageHealthcheck;
 class ReportsPageHibp;
 class ReportsPageStatistics;
+#ifdef WITH_XC_BROWSER
+class ReportsPageBrowserStatistics;
+#endif
 
 namespace Ui
 {
@@ -74,6 +77,9 @@ private:
     const QSharedPointer<ReportsPageHealthcheck> m_healthPage;
     const QSharedPointer<ReportsPageHibp> m_hibpPage;
     const QSharedPointer<ReportsPageStatistics> m_statPage;
+#ifdef WITH_XC_BROWSER
+    const QSharedPointer<ReportsPageBrowserStatistics> m_browserStatPage;
+#endif
     QPointer<EditEntryWidget> m_editEntryWidget;
     QWidget* m_sender = nullptr;
 
diff --git a/src/gui/reports/ReportsPageBrowserStatistics.cpp b/src/gui/reports/ReportsPageBrowserStatistics.cpp
new file mode 100644
index 000000000..97325745c
--- /dev/null
+++ b/src/gui/reports/ReportsPageBrowserStatistics.cpp
@@ -0,0 +1,53 @@
+/*
+ *  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 "ReportsPageBrowserStatistics.h"
+
+#include "ReportsWidgetBrowserStatistics.h"
+#include "gui/Icons.h"
+
+ReportsPageBrowserStatistics::ReportsPageBrowserStatistics()
+    : m_browserWidget(new ReportsWidgetBrowserStatistics())
+{
+}
+
+QString ReportsPageBrowserStatistics::name()
+{
+    return QObject::tr("Browser Statistics");
+}
+
+QIcon ReportsPageBrowserStatistics::icon()
+{
+    return icons()->icon("internet-web-browser");
+}
+
+QWidget* ReportsPageBrowserStatistics::createWidget()
+{
+    return m_browserWidget;
+}
+
+void ReportsPageBrowserStatistics::loadSettings(QWidget* widget, QSharedPointer<Database> db)
+{
+    const auto settingsWidget = reinterpret_cast<ReportsWidgetBrowserStatistics*>(widget);
+    settingsWidget->loadSettings(db);
+}
+
+void ReportsPageBrowserStatistics::saveSettings(QWidget* widget)
+{
+    const auto settingsWidget = reinterpret_cast<ReportsWidgetBrowserStatistics*>(widget);
+    settingsWidget->saveSettings();
+}
diff --git a/src/gui/reports/ReportsPageBrowserStatistics.h b/src/gui/reports/ReportsPageBrowserStatistics.h
new file mode 100644
index 000000000..fb1b20a9f
--- /dev/null
+++ b/src/gui/reports/ReportsPageBrowserStatistics.h
@@ -0,0 +1,39 @@
+/*
+ *  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_REPORTSPAGEBROWSERSTATISTICS_H
+#define KEEPASSXC_REPORTSPAGEBROWSERSTATISTICS_H
+
+#include "ReportsDialog.h"
+
+class ReportsWidgetBrowserStatistics;
+
+class ReportsPageBrowserStatistics : public IReportsPage
+{
+public:
+    ReportsWidgetBrowserStatistics* m_browserWidget;
+
+    ReportsPageBrowserStatistics();
+
+    QString name() override;
+    QIcon icon() override;
+    QWidget* createWidget() override;
+    void loadSettings(QWidget* widget, QSharedPointer<Database> db) override;
+    void saveSettings(QWidget* widget) override;
+};
+
+#endif // KEEPASSXC_REPORTSPAGEBROWSERSTATISTICS_H
diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp
new file mode 100644
index 000000000..1a3ad5d7e
--- /dev/null
+++ b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp
@@ -0,0 +1,372 @@
+/*
+ *  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 "ReportsWidgetBrowserStatistics.h"
+#include "ui_ReportsWidgetBrowserStatistics.h"
+
+#include "browser/BrowserService.h"
+#include "core/AsyncTask.h"
+#include "core/Group.h"
+#include "core/Metadata.h"
+#include "gui/GuiTools.h"
+#include "gui/Icons.h"
+#include "gui/styles/StateColorPalette.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QMenu>
+#include <QShortcut>
+#include <QSortFilterProxyModel>
+#include <QStandardItemModel>
+
+namespace
+{
+    class BrowserStatistics
+    {
+    public:
+        struct Item
+        {
+            QPointer<Group> group;
+            QPointer<Entry> entry;
+            bool hasUrls;
+            bool hasSettings;
+            bool exclude = false;
+
+            Item(Group* g, Entry* e, bool hU, bool hS)
+                : group(g)
+                , entry(e)
+                , hasUrls(hU)
+                , hasSettings(hS)
+                , exclude(e->excludeFromReports())
+            {
+            }
+        };
+
+        explicit BrowserStatistics(QSharedPointer<Database>);
+
+        const QList<QSharedPointer<Item>>& items() const
+        {
+            return m_items;
+        }
+
+    private:
+        QSharedPointer<Database> m_db;
+        QList<QSharedPointer<Item>> m_items;
+    };
+} // namespace
+
+BrowserStatistics::BrowserStatistics(QSharedPointer<Database> db)
+    : m_db(db)
+{
+    for (auto group : db->rootGroup()->groupsRecursive(true)) {
+        // Skip recycle bin
+        if (group->isRecycled()) {
+            continue;
+        }
+
+        for (auto entry : group->entries()) {
+            if (entry->isRecycled()) {
+                continue;
+            }
+
+            auto hasUrls = !entry->getAllUrls().isEmpty();
+            auto hasSettings = entry->customData()->contains(BrowserService::KEEPASSXCBROWSER_NAME);
+
+            const auto item = QSharedPointer<Item>(new Item(group, entry, hasUrls, hasSettings));
+            m_items.append(item);
+        }
+    }
+}
+
+ReportsWidgetBrowserStatistics::ReportsWidgetBrowserStatistics(QWidget* parent)
+    : QWidget(parent)
+    , m_ui(new Ui::ReportsWidgetBrowserStatistics())
+    , m_referencesModel(new QStandardItemModel(this))
+    , m_modelProxy(new QSortFilterProxyModel(this))
+{
+    m_ui->setupUi(this);
+
+    m_modelProxy->setSourceModel(m_referencesModel.data());
+    m_modelProxy->setSortLocaleAware(true);
+    m_ui->browserStatisticsTableView->setModel(m_modelProxy.data());
+    m_ui->browserStatisticsTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
+    m_ui->browserStatisticsTableView->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
+
+    connect(m_ui->browserStatisticsTableView,
+            SIGNAL(customContextMenuRequested(QPoint)),
+            SLOT(customMenuRequested(QPoint)));
+    connect(
+        m_ui->browserStatisticsTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex)));
+    connect(m_ui->showEntriesWithUrlOnlyCheckBox, SIGNAL(stateChanged(int)), this, SLOT(calculateBrowserStatistics()));
+    connect(m_ui->showConnectedOnlyCheckBox, SIGNAL(stateChanged(int)), this, SLOT(calculateBrowserStatistics()));
+    connect(m_ui->excludeExpired, SIGNAL(stateChanged(int)), this, SLOT(calculateBrowserStatistics()));
+
+    new QShortcut(Qt::Key_Delete, this, SLOT(deleteSelectedEntries()));
+}
+
+ReportsWidgetBrowserStatistics::~ReportsWidgetBrowserStatistics()
+{
+}
+
+void ReportsWidgetBrowserStatistics::addStatisticsRow(bool hasUrls,
+                                                      bool hasSettings,
+                                                      Group* group,
+                                                      Entry* entry,
+                                                      bool excluded)
+{
+    StateColorPalette statePalette;
+
+    auto urlList = entry->getAllUrls();
+    auto urlToolTip = hasUrls ? tr("List of entry URLs") : tr("Entry has no URLs set");
+
+    auto browserConfig = getBrowserConfigFromEntry(entry);
+    auto allowedUrlsList = browserConfig["Allow"];
+    auto deniedUrlsList = browserConfig["Deny"];
+
+    auto allowedUrlsToolTip = hasSettings ? tr("Allowed URLs") : tr("Entry has no Browser Integration settings");
+    auto deniedUrlsToolTip = hasSettings ? tr("Denied URLs") : tr("Entry has no Browser Integration settings");
+
+    auto title = entry->title();
+    if (excluded) {
+        title.append(tr(" (Excluded)"));
+    }
+
+    auto row = QList<QStandardItem*>();
+    row << new QStandardItem(Icons::entryIconPixmap(entry), title);
+    row << new QStandardItem(Icons::groupIconPixmap(group), group->hierarchy().join("/"));
+    row << new QStandardItem(urlList.join('\n'));
+    row << new QStandardItem(allowedUrlsList.join('\n'));
+    row << new QStandardItem(deniedUrlsList.join('\n'));
+
+    // Set tooltips
+    row[2]->setToolTip(urlToolTip);
+    row[3]->setToolTip(allowedUrlsToolTip);
+    row[4]->setToolTip(deniedUrlsToolTip);
+    if (excluded) {
+        row[0]->setToolTip(tr("This entry is being excluded from reports"));
+    }
+
+    // Store entry pointer per table row (used in double click handler)
+    m_referencesModel->appendRow(row);
+    m_rowToEntry.append({group, entry});
+}
+
+void ReportsWidgetBrowserStatistics::loadSettings(QSharedPointer<Database> db)
+{
+    m_db = std::move(db);
+    m_statisticsCalculated = false;
+    m_referencesModel->clear();
+    m_rowToEntry.clear();
+
+    auto row = QList<QStandardItem*>();
+    row << new QStandardItem(tr("Please wait, browser statistics is being calculated…"));
+    m_referencesModel->appendRow(row);
+}
+
+void ReportsWidgetBrowserStatistics::showEvent(QShowEvent* event)
+{
+    QWidget::showEvent(event);
+
+    if (!m_statisticsCalculated) {
+        // Perform stats calculation on next event loop to allow widget to appear
+        m_statisticsCalculated = true;
+        QTimer::singleShot(0, this, SLOT(calculateBrowserStatistics()));
+    }
+}
+
+void ReportsWidgetBrowserStatistics::calculateBrowserStatistics()
+{
+    m_referencesModel->clear();
+
+    // Perform the statistics check
+    const QScopedPointer<BrowserStatistics> browserStatistics(
+        AsyncTask::runAndWaitForFuture([this] { return new BrowserStatistics(m_db); }));
+
+    const auto showExcluded = m_ui->showConnectedOnlyCheckBox->isChecked();
+    const auto showEntriesWithUrlOnly = m_ui->showEntriesWithUrlOnlyCheckBox->isChecked();
+    const auto showOnlyEntriesWithSettings = m_ui->showConnectedOnlyCheckBox->isChecked();
+
+    // Display the entries
+    m_rowToEntry.clear();
+    for (const auto& item : browserStatistics->items()) {
+        auto excluded = item->exclude || (item->entry->isExpired() && m_ui->excludeExpired->isChecked());
+        if (excluded && !showExcluded) {
+            // Exclude this entry from the report
+            continue;
+        }
+
+        // Exclude this entry if URL are not set
+        if (showEntriesWithUrlOnly && !item->hasUrls) {
+            continue;
+        }
+
+        // Exclude this entry if it doesn't have any Browser Integration settings
+        if (showOnlyEntriesWithSettings
+            && !item->entry->customData()->contains(BrowserService::KEEPASSXCBROWSER_NAME)) {
+            continue;
+        }
+
+        // Show the entry in the report
+        addStatisticsRow(item->hasUrls, item->hasSettings, item->group, item->entry, item->exclude);
+    }
+
+    // Set the table header
+    if (m_referencesModel->rowCount() == 0) {
+        m_referencesModel->setHorizontalHeaderLabels(
+            QStringList() << tr("No entries with a URL, or none has browser extension settings saved."));
+    } else {
+        m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Title") << tr("Path") << tr("URLs")
+                                                                   << tr("Allowed URLs") << tr("Denied URLs"));
+        m_ui->browserStatisticsTableView->sortByColumn(0, Qt::AscendingOrder);
+    }
+
+    m_ui->browserStatisticsTableView->resizeColumnsToContents();
+}
+
+void ReportsWidgetBrowserStatistics::emitEntryActivated(const QModelIndex& index)
+{
+    if (!index.isValid()) {
+        return;
+    }
+
+    auto mappedIndex = m_modelProxy->mapToSource(index);
+    const auto row = m_rowToEntry[mappedIndex.row()];
+    const auto group = row.first;
+    const auto entry = row.second;
+
+    if (group && entry) {
+        emit entryActivated(const_cast<Entry*>(entry));
+    }
+}
+
+void ReportsWidgetBrowserStatistics::customMenuRequested(QPoint pos)
+{
+    auto selected = m_ui->browserStatisticsTableView->selectionModel()->selectedRows();
+    if (selected.isEmpty()) {
+        return;
+    }
+
+    // Create the context menu
+    const auto menu = new QMenu(this);
+
+    // Create the "edit entry" menu item (only if 1 row is selected)
+    if (selected.size() == 1) {
+        const auto edit = new QAction(icons()->icon("entry-edit"), tr("Edit Entry…"), this);
+        menu->addAction(edit);
+        connect(edit, &QAction::triggered, edit, [this, selected] {
+            auto row = m_modelProxy->mapToSource(selected[0]).row();
+            auto entry = m_rowToEntry[row].second;
+            emit entryActivated(entry);
+        });
+    }
+
+    // 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, &ReportsWidgetBrowserStatistics::deleteSelectedEntries);
+
+    // Create the "exclude from reports" menu item
+    const auto exclude = new QAction(icons()->icon("reports-exclude"), tr("Exclude from reports"), this);
+
+    bool isExcluded = false;
+    for (auto index : selected) {
+        auto row = m_modelProxy->mapToSource(index).row();
+        auto entry = m_rowToEntry[row].second;
+        if (entry && entry->excludeFromReports()) {
+            // If at least one entry is excluded switch to inclusion
+            isExcluded = true;
+            break;
+        }
+    }
+    exclude->setCheckable(true);
+    exclude->setChecked(isExcluded);
+
+    menu->addAction(exclude);
+    connect(exclude, &QAction::toggled, exclude, [this, selected](bool state) {
+        for (auto index : selected) {
+            auto row = m_modelProxy->mapToSource(index).row();
+            auto entry = m_rowToEntry[row].second;
+            if (entry) {
+                entry->setExcludeFromReports(state);
+            }
+        }
+        calculateBrowserStatistics();
+    });
+
+    // Show the context menu
+    menu->popup(m_ui->browserStatisticsTableView->viewport()->mapToGlobal(pos));
+}
+
+void ReportsWidgetBrowserStatistics::saveSettings()
+{
+    // Nothing to do - the tab is passive
+}
+
+void ReportsWidgetBrowserStatistics::deleteSelectedEntries()
+{
+    QList<Entry*> selectedEntries;
+    for (auto index : m_ui->browserStatisticsTableView->selectionModel()->selectedRows()) {
+        auto row = m_modelProxy->mapToSource(index).row();
+        auto entry = m_rowToEntry[row].second;
+        if (entry) {
+            selectedEntries << entry;
+        }
+    }
+
+    bool permanent = !m_db->metadata()->recycleBinEnabled();
+    if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) {
+        GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent);
+    }
+
+    calculateBrowserStatistics();
+}
+
+QMap<QString, QStringList> ReportsWidgetBrowserStatistics::getBrowserConfigFromEntry(Entry* entry) const
+{
+    QMap<QString, QStringList> configList;
+
+    auto config = entry->customData()->value(BrowserService::KEEPASSXCBROWSER_NAME);
+    if (!config.isEmpty()) {
+        QJsonDocument doc = QJsonDocument::fromJson(config.toUtf8());
+        if (!doc.isNull()) {
+            auto jsonObject = doc.object();
+            auto allowedSites = jsonObject["Allow"].toArray();
+            auto deniedSites = jsonObject["Deny"].toArray();
+
+            QStringList allowed;
+            foreach (const auto& value, allowedSites) {
+                auto url = value.toString();
+                if (!url.isEmpty()) {
+                    allowed << url;
+                }
+            }
+
+            QStringList denied;
+            foreach (const auto& value, deniedSites) {
+                auto url = value.toString();
+                if (!url.isEmpty()) {
+                    denied << url;
+                }
+            }
+
+            configList.insert("Allow", allowed);
+            configList.insert("Deny", denied);
+        }
+    }
+
+    return configList;
+}
diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.h b/src/gui/reports/ReportsWidgetBrowserStatistics.h
new file mode 100644
index 000000000..8aa6651ee
--- /dev/null
+++ b/src/gui/reports/ReportsWidgetBrowserStatistics.h
@@ -0,0 +1,71 @@
+/*
+ *  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_REPORTSWIDGETBROWSERSTATISTICS_H
+#define KEEPASSXC_REPORTSWIDGETBROWSERSTATISTICS_H
+
+#include "gui/entry/EntryModel.h"
+#include <QWidget>
+
+class Database;
+class Entry;
+class Group;
+class PasswordHealth;
+class QSortFilterProxyModel;
+class QStandardItemModel;
+
+namespace Ui
+{
+    class ReportsWidgetBrowserStatistics;
+}
+
+class ReportsWidgetBrowserStatistics : public QWidget
+{
+    Q_OBJECT
+public:
+    explicit ReportsWidgetBrowserStatistics(QWidget* parent = nullptr);
+    ~ReportsWidgetBrowserStatistics() override;
+
+    void loadSettings(QSharedPointer<Database> db);
+    void saveSettings();
+
+protected:
+    void showEvent(QShowEvent* event) override;
+
+signals:
+    void entryActivated(Entry*);
+
+public slots:
+    void calculateBrowserStatistics();
+    void emitEntryActivated(const QModelIndex& index);
+    void customMenuRequested(QPoint);
+    void deleteSelectedEntries();
+
+private:
+    void addStatisticsRow(bool hasUrls, bool hasSettings, Group*, Entry*, bool);
+    QMap<QString, QStringList> getBrowserConfigFromEntry(Entry* entry) const;
+
+    QScopedPointer<Ui::ReportsWidgetBrowserStatistics> m_ui;
+
+    bool m_statisticsCalculated = false;
+    QScopedPointer<QStandardItemModel> m_referencesModel;
+    QScopedPointer<QSortFilterProxyModel> m_modelProxy;
+    QSharedPointer<Database> m_db;
+    QList<QPair<Group*, Entry*>> m_rowToEntry;
+};
+
+#endif // KEEPASSXC_REPORTSWIDGETBROWSERSTATISTICS_H
diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.ui b/src/gui/reports/ReportsWidgetBrowserStatistics.ui
new file mode 100644
index 000000000..4236da6e1
--- /dev/null
+++ b/src/gui/reports/ReportsWidgetBrowserStatistics.ui
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ReportsWidgetBrowserStatistics</class>
+ <widget class="QWidget" name="ReportsWidgetBrowserStatistics">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>505</width>
+    <height>379</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,0">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item>
+    <widget class="QTableView" name="browserStatisticsTableView">
+     <property name="contextMenuPolicy">
+      <enum>Qt::CustomContextMenu</enum>
+     </property>
+     <property name="editTriggers">
+      <set>QAbstractItemView::NoEditTriggers</set>
+     </property>
+     <property name="showDropIndicator" stdset="0">
+      <bool>false</bool>
+     </property>
+     <property name="alternatingRowColors">
+      <bool>true</bool>
+     </property>
+     <property name="selectionBehavior">
+      <enum>QAbstractItemView::SelectRows</enum>
+     </property>
+     <property name="textElideMode">
+      <enum>Qt::ElideMiddle</enum>
+     </property>
+     <property name="sortingEnabled">
+      <bool>true</bool>
+     </property>
+     <attribute name="horizontalHeaderStretchLastSection">
+      <bool>true</bool>
+     </attribute>
+     <attribute name="verticalHeaderVisible">
+      <bool>false</bool>
+     </attribute>
+    </widget>
+   </item>
+   <item>
+    <widget class="QCheckBox" name="excludeExpired">
+     <property name="text">
+      <string>Exclude expired entries from the report</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QCheckBox" name="showEntriesWithUrlOnlyCheckBox">
+     <property name="text">
+      <string>Show only entries which have URL set</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QCheckBox" name="showConnectedOnlyCheckBox">
+     <property name="text">
+      <string>Show only entries which have browser settings in custom data</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="tipLabel">
+     <property name="font">
+      <font>
+       <italic>true</italic>
+      </font>
+     </property>
+     <property name="text">
+      <string>Double-click entries to edit.</string>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <tabstops>
+  <tabstop>browserStatisticsTableView</tabstop>
+  <tabstop>excludeExpired</tabstop>
+  <tabstop>showEntriesWithUrlOnlyCheckBox</tabstop>
+  <tabstop>showConnectedOnlyCheckBox</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/gui/styles/StateColorPalette.cpp b/src/gui/styles/StateColorPalette.cpp
index c729e3269..8a2b563da 100644
--- a/src/gui/styles/StateColorPalette.cpp
+++ b/src/gui/styles/StateColorPalette.cpp
@@ -1,5 +1,5 @@
 /*
- *  Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
+ *  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
@@ -40,6 +40,9 @@ void StateColorPalette::initDefaultPaletteLight()
     setColor(ColorRole::HealthWeak, QStringLiteral("#FFD30F"));
     setColor(ColorRole::HealthOk, QStringLiteral("#5EA10E"));
     setColor(ColorRole::HealthExcellent, QStringLiteral("#118f17"));
+
+    setColor(ColorRole::True, QStringLiteral("#5EA10E"));
+    setColor(ColorRole::False, QStringLiteral("#C43F31"));
 }
 
 void StateColorPalette::initDefaultPaletteDark()
@@ -54,4 +57,7 @@ void StateColorPalette::initDefaultPaletteDark()
     setColor(ColorRole::HealthWeak, QStringLiteral("#F0C400"));
     setColor(ColorRole::HealthOk, QStringLiteral("#608A22"));
     setColor(ColorRole::HealthExcellent, QStringLiteral("#1F8023"));
+
+    setColor(ColorRole::True, QStringLiteral("#608A22"));
+    setColor(ColorRole::False, QStringLiteral("#C43F31"));
 }
diff --git a/src/gui/styles/StateColorPalette.h b/src/gui/styles/StateColorPalette.h
index 408fe032a..082f6a893 100644
--- a/src/gui/styles/StateColorPalette.h
+++ b/src/gui/styles/StateColorPalette.h
@@ -1,5 +1,5 @@
 /*
- *  Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
+ *  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
@@ -43,7 +43,9 @@ public:
         HealthPoor,
         HealthWeak,
         HealthOk,
-        HealthExcellent
+        HealthExcellent,
+        True,
+        False
     };
 
     inline void setColor(ColorRole role, const QColor& color)