From 6ae27fa47b070902eabb44ebab68d471eb68d0e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sami=20V=C3=A4nttinen?= Date: Sun, 7 Jul 2019 22:29:11 +0300 Subject: [PATCH] Download all favicons (#3169) * Selecting one or more entries to download icons always forces the download (ie, if a new URL exists the new icon will be downloaded and set) * Instead of downloading for each entry, the web url's are scraped from the provided entries and only those urls are downloaded. The icon is set for all entries that share a URL. This is useful if a group contains many entries that point to the same url, only 1 download call will occur. * The icon download dialog displays whether you are doing one entry, many entries, or an entire group. It is also modal so you have to dismiss it to use KeePassXC again. * Moved DuckDuckGo fallback notice into the download dialog. --- src/CMakeLists.txt | 13 +- src/core/Config.cpp | 1 + src/core/IconDownloader.cpp | 204 +++++++++++++++ src/core/IconDownloader.h | 63 +++++ src/core/NetworkManager.cpp | 33 +++ src/core/NetworkManager.h | 34 +++ src/gui/ApplicationSettingsWidget.cpp | 4 + src/gui/ApplicationSettingsWidgetGeneral.ui | 51 +++- src/gui/DatabaseWidget.cpp | 41 +++ src/gui/DatabaseWidget.h | 7 +- src/gui/EditWidgetIcons.cpp | 263 +++++--------------- src/gui/EditWidgetIcons.h | 21 +- src/gui/IconDownloaderDialog.cpp | 198 +++++++++++++++ src/gui/IconDownloaderDialog.h | 69 +++++ src/gui/IconDownloaderDialog.ui | 154 ++++++++++++ src/gui/MainWindow.cpp | 17 ++ src/gui/MainWindow.ui | 17 ++ src/updatecheck/UpdateChecker.cpp | 11 +- src/updatecheck/UpdateChecker.h | 2 - 19 files changed, 981 insertions(+), 222 deletions(-) create mode 100644 src/core/IconDownloader.cpp create mode 100644 src/core/IconDownloader.h create mode 100644 src/core/NetworkManager.cpp create mode 100644 src/core/NetworkManager.h create mode 100644 src/gui/IconDownloaderDialog.cpp create mode 100644 src/gui/IconDownloaderDialog.h create mode 100644 src/gui/IconDownloaderDialog.ui diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9b24873359..966ed88859 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -27,6 +27,9 @@ set(keepassx_SOURCES core/Alloc.cpp core/AutoTypeAssociations.cpp core/AutoTypeMatch.cpp + core/Base32.cpp + core/Bootstrap.cpp + core/Clock.cpp core/Compare.cpp core/Config.cpp core/CsvParser.cpp @@ -39,7 +42,6 @@ set(keepassx_SOURCES core/EntrySearcher.cpp core/FilePath.cpp core/FileWatcher.cpp - core/Bootstrap.cpp core/Group.cpp core/HibpOffline.cpp core/InactivityTimer.cpp @@ -52,10 +54,8 @@ set(keepassx_SOURCES core/ScreenLockListenerPrivate.cpp core/TimeDelta.cpp core/TimeInfo.cpp - core/Clock.cpp core/Tools.cpp core/Translator.cpp - core/Base32.cpp cli/Utils.cpp cli/TextStream.cpp crypto/Crypto.cpp @@ -264,7 +264,12 @@ else() endif() if(WITH_XC_NETWORKING) - list(APPEND keepassx_SOURCES updatecheck/UpdateChecker.cpp gui/UpdateCheckDialog.cpp) + list(APPEND keepassx_SOURCES + core/IconDownloader.cpp + core/NetworkManager.cpp + gui/UpdateCheckDialog.cpp + gui/IconDownloaderDialog.cpp + updatecheck/UpdateChecker.cpp) endif() if(WITH_XC_TOUCHID) diff --git a/src/core/Config.cpp b/src/core/Config.cpp index 3922d3e27e..741a10d75f 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -194,6 +194,7 @@ void Config::init(const QString& fileName) m_defaults.insert("AutoTypeStartDelay", 500); m_defaults.insert("UseGroupIconOnEntryCreation", true); m_defaults.insert("IgnoreGroupExpansion", true); + m_defaults.insert("FaviconDownloadTimeout", 10); m_defaults.insert("security/clearclipboard", true); m_defaults.insert("security/clearclipboardtimeout", 10); m_defaults.insert("security/lockdatabaseidle", false); diff --git a/src/core/IconDownloader.cpp b/src/core/IconDownloader.cpp new file mode 100644 index 0000000000..36047ce2a7 --- /dev/null +++ b/src/core/IconDownloader.cpp @@ -0,0 +1,204 @@ +/* + * 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 "IconDownloader.h" +#include "core/Config.h" +#include "core/NetworkManager.h" + +#include +#include + +#define MAX_REDIRECTS 5 + +IconDownloader::IconDownloader(QObject* parent) + : QObject(parent) + , m_reply(nullptr) + , m_redirects(0) +{ + m_timeout.setSingleShot(true); + connect(&m_timeout, SIGNAL(timeout()), SLOT(abortDownload())); +} + +IconDownloader::~IconDownloader() +{ + abortDownload(); +} + +namespace +{ + // Try to get the 2nd level domain of the host part of a QUrl. For example, + // "foo.bar.example.com" would become "example.com", and "foo.bar.example.co.uk" + // would become "example.co.uk". + QString getSecondLevelDomain(const QUrl& url) + { + QString fqdn = url.host(); + fqdn.truncate(fqdn.length() - url.topLevelDomain().length()); + QStringList parts = fqdn.split('.'); + QString newdom = parts.takeLast() + url.topLevelDomain(); + return newdom; + } + + QUrl convertVariantToUrl(const QVariant& var) + { + QUrl url; + if (var.canConvert()) { + url = var.toUrl(); + } + return url; + } + + QUrl getRedirectTarget(QNetworkReply* reply) + { + QVariant var = reply->attribute(QNetworkRequest::RedirectionTargetAttribute); + QUrl url = convertVariantToUrl(var); + return url; + } +} // namespace + +void IconDownloader::setUrl(const QString& entryUrl) +{ + m_url = entryUrl; + QUrl url(m_url); + if (!url.isValid()) { + return; + } + + m_redirects = 0; + m_urlsToTry.clear(); + + if (url.scheme().isEmpty()) { + url.setUrl(QString("https://%1").arg(url.toString())); + } + + QString fullyQualifiedDomain = url.host(); + + // Determine if host portion of URL is an IP address by resolving it and + // searching for a match with the returned address(es). + bool hostIsIp = false; + QList hostAddressess = QHostInfo::fromName(fullyQualifiedDomain).addresses(); + for (auto addr : hostAddressess) { + if (addr.toString() == fullyQualifiedDomain) { + hostIsIp = true; + } + } + + // Determine the second-level domain, if available + QString secondLevelDomain; + if (!hostIsIp) { + secondLevelDomain = getSecondLevelDomain(m_url); + } + + // Start with the "fallback" url (if enabled) to try to get the best favicon + if (config()->get("security/IconDownloadFallback", false).toBool()) { + QUrl fallbackUrl = QUrl("https://icons.duckduckgo.com"); + fallbackUrl.setPath("/ip3/" + QUrl::toPercentEncoding(fullyQualifiedDomain) + ".ico"); + m_urlsToTry.append(fallbackUrl); + + // Also try a direct pull of the second-level domain (if possible) + if (!hostIsIp && fullyQualifiedDomain != secondLevelDomain) { + fallbackUrl.setPath("/ip3/" + QUrl::toPercentEncoding(secondLevelDomain) + ".ico"); + m_urlsToTry.append(fallbackUrl); + } + } + + // Add a direct pull of the website's own favicon.ico file + m_urlsToTry.append(QUrl(url.scheme() + "://" + fullyQualifiedDomain + "/favicon.ico")); + + // Also try a direct pull of the second-level domain (if possible) + if (!hostIsIp && fullyQualifiedDomain != secondLevelDomain) { + m_urlsToTry.append(QUrl(url.scheme() + "://" + secondLevelDomain + "/favicon.ico")); + } +} + +void IconDownloader::download() +{ + if (!m_timeout.isActive()) { + int timeout = config()->get("FaviconDownloadTimeout", 10).toInt(); + m_timeout.start(timeout * 1000); + + // Use the first URL to start the download process + // If a favicon is not found, the next URL will be tried + fetchFavicon(m_urlsToTry.takeFirst()); + } +} + +void IconDownloader::abortDownload() +{ + if (m_reply) { + m_reply->abort(); + } +} + +void IconDownloader::fetchFavicon(const QUrl& url) +{ + m_bytesReceived.clear(); + m_fetchUrl = url; + + QNetworkRequest request(url); + m_reply = getNetMgr()->get(request); + + connect(m_reply, &QNetworkReply::finished, this, &IconDownloader::fetchFinished); + connect(m_reply, &QIODevice::readyRead, this, &IconDownloader::fetchReadyRead); +} + +void IconDownloader::fetchReadyRead() +{ + m_bytesReceived += m_reply->readAll(); +} + +void IconDownloader::fetchFinished() +{ + QImage image; + QString url = m_url; + + bool error = (m_reply->error() != QNetworkReply::NoError); + QUrl redirectTarget = getRedirectTarget(m_reply); + + m_reply->deleteLater(); + m_reply = nullptr; + + if (!error) { + if (redirectTarget.isValid()) { + // Redirected, we need to follow it, or fall through if we have + // done too many redirects already. + if (m_redirects < MAX_REDIRECTS) { + m_redirects++; + if (redirectTarget.isRelative()) { + redirectTarget = m_fetchUrl.resolved(redirectTarget); + } + m_urlsToTry.prepend(redirectTarget); + } + } else { + // No redirect, and we theoretically have some icon data now. + image.loadFromData(m_bytesReceived); + } + } + + if (!image.isNull()) { + // Valid icon received + m_timeout.stop(); + emit finished(url, image); + } else if (!m_urlsToTry.empty()) { + // Try the next url + m_redirects = 0; + fetchFavicon(m_urlsToTry.takeFirst()); + } else { + // No icon found + m_timeout.stop(); + emit finished(url, image); + } +} diff --git a/src/core/IconDownloader.h b/src/core/IconDownloader.h new file mode 100644 index 0000000000..e2b8c4f2df --- /dev/null +++ b/src/core/IconDownloader.h @@ -0,0 +1,63 @@ +/* + * 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 . + */ + +#ifndef KEEPASSXC_ICONDOWNLOADER_H +#define KEEPASSXC_ICONDOWNLOADER_H + +#include +#include +#include +#include + +#include "core/Global.h" + +class QNetworkReply; + +class IconDownloader : public QObject +{ + Q_OBJECT + +public: + explicit IconDownloader(QObject* parent = nullptr); + ~IconDownloader() override; + + void setUrl(const QString& entryUrl); + void download(); + +signals: + void finished(const QString& entryUrl, const QImage& image); + +public slots: + void abortDownload(); + +private slots: + void fetchFinished(); + void fetchReadyRead(); + +private: + void fetchFavicon(const QUrl& url); + + QString m_url; + QUrl m_fetchUrl; + QList m_urlsToTry; + QByteArray m_bytesReceived; + QNetworkReply* m_reply; + QTimer m_timeout; + int m_redirects; +}; + +#endif // KEEPASSXC_ICONDOWNLOADER_H \ No newline at end of file diff --git a/src/core/NetworkManager.cpp b/src/core/NetworkManager.cpp new file mode 100644 index 0000000000..52b54609fe --- /dev/null +++ b/src/core/NetworkManager.cpp @@ -0,0 +1,33 @@ +/* + * 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 "config-keepassx.h" + +#ifdef WITH_XC_NETWORKING +#include "NetworkManager.h" + +#include + +QNetworkAccessManager* g_netMgr = nullptr; +QNetworkAccessManager* getNetMgr() +{ + if (!g_netMgr) { + g_netMgr = new QNetworkAccessManager(QCoreApplication::instance()); + } + return g_netMgr; +} +#endif diff --git a/src/core/NetworkManager.h b/src/core/NetworkManager.h new file mode 100644 index 0000000000..5616218749 --- /dev/null +++ b/src/core/NetworkManager.h @@ -0,0 +1,34 @@ +/* + * 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 . + */ + +#ifndef KEEPASSXC_NETWORKMANAGER_H +#define KEEPASSXC_NETWORKMANAGER_H + +#include "config-keepassx.h" +#include + +#ifdef WITH_XC_NETWORKING +#include +#include +#include + +QNetworkAccessManager* getNetMgr(); +#else +Q_STATIC_ASSERT_X(false, "Qt Networking used when WITH_XC_NETWORKING is disabled!"); +#endif + +#endif // KEEPASSXC_NETWORKMANAGER_H diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp index 832e73fa40..89c585fcb0 100644 --- a/src/gui/ApplicationSettingsWidget.cpp +++ b/src/gui/ApplicationSettingsWidget.cpp @@ -103,6 +103,8 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent) #ifndef WITH_XC_NETWORKING m_secUi->privacy->setVisible(false); + m_generalUi->faviconTimeoutLabel->setVisible(false); + m_generalUi->faviconTimeoutSpinBox->setVisible(false); #endif #ifndef WITH_XC_TOUCHID @@ -156,6 +158,7 @@ void ApplicationSettingsWidget::loadSettings() m_generalUi->autoTypeEntryTitleMatchCheckBox->setChecked(config()->get("AutoTypeEntryTitleMatch").toBool()); m_generalUi->autoTypeEntryURLMatchCheckBox->setChecked(config()->get("AutoTypeEntryURLMatch").toBool()); m_generalUi->ignoreGroupExpansionCheckBox->setChecked(config()->get("IgnoreGroupExpansion").toBool()); + m_generalUi->faviconTimeoutSpinBox->setValue(config()->get("FaviconDownloadTimeout").toInt()); if (!m_generalUi->hideWindowOnCopyCheckBox->isChecked()) { hideWindowOnCopyCheckBoxToggled(false); @@ -264,6 +267,7 @@ void ApplicationSettingsWidget::saveSettings() config()->set("AutoTypeEntryTitleMatch", m_generalUi->autoTypeEntryTitleMatchCheckBox->isChecked()); config()->set("AutoTypeEntryURLMatch", m_generalUi->autoTypeEntryURLMatchCheckBox->isChecked()); int currentLangIndex = m_generalUi->languageComboBox->currentIndex(); + config()->set("FaviconDownloadTimeout", m_generalUi->faviconTimeoutSpinBox->value()); config()->set("GUI/Language", m_generalUi->languageComboBox->itemData(currentLangIndex).toString()); diff --git a/src/gui/ApplicationSettingsWidgetGeneral.ui b/src/gui/ApplicationSettingsWidgetGeneral.ui index d1fb19c758..311b408b89 100644 --- a/src/gui/ApplicationSettingsWidgetGeneral.ui +++ b/src/gui/ApplicationSettingsWidgetGeneral.ui @@ -7,7 +7,7 @@ 0 0 684 - 1030 + 860 @@ -329,6 +329,55 @@ + + + + + + Favicon download timeout: + + + + + + + true + + + + 0 + 0 + + + + sec + + + 1 + + + 60 + + + 10 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 7d8c958025..4effa99f99 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -62,6 +62,10 @@ #include "keeshare/KeeShare.h" #include "touchid/TouchID.h" +#ifdef WITH_XC_NETWORKING +#include "gui/IconDownloaderDialog.h" +#endif + #ifdef Q_OS_LINUX #include #endif @@ -650,6 +654,41 @@ void DatabaseWidget::openUrl() } } +void DatabaseWidget::downloadSelectedFavicons() +{ +#ifdef WITH_XC_NETWORKING + QList selectedEntries; + for (const auto& index : m_entryView->selectionModel()->selectedRows()) { + selectedEntries.append(m_entryView->entryFromIndex(index)); + } + + // Force download even if icon already exists + performIconDownloads(selectedEntries, true); +#endif +} + +void DatabaseWidget::downloadAllFavicons() +{ +#ifdef WITH_XC_NETWORKING + auto currentGroup = m_groupView->currentGroup(); + if (currentGroup) { + performIconDownloads(currentGroup->entries()); + } +#endif +} + +void DatabaseWidget::performIconDownloads(const QList& entries, bool force) +{ +#ifdef WITH_XC_NETWORKING + auto* iconDownloaderDialog = new IconDownloaderDialog(this); + connect(this, SIGNAL(databaseLockRequested()), iconDownloaderDialog, SLOT(close())); + iconDownloaderDialog->downloadFavicons(m_db, entries, force); +#else + Q_UNUSED(entries); + Q_UNUSED(force); +#endif +} + void DatabaseWidget::openUrlForEntry(Entry* entry) { Q_ASSERT(entry); @@ -1275,6 +1314,8 @@ bool DatabaseWidget::lock() return true; } + emit databaseLockRequested(); + clipboard()->clearCopiedText(); if (isEditWidgetModified()) { diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 7da6b7a40d..aeb6a02e1b 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -25,12 +25,11 @@ #include #include "DatabaseOpenDialog.h" +#include "config-keepassx.h" #include "gui/MessageWidget.h" #include "gui/csvImport/CsvImportWizard.h" #include "gui/entry/EntryModel.h" -#include "config-keepassx.h" - class DatabaseOpenWidget; class KeePass1OpenWidget; class OpVaultOpenWidget; @@ -124,6 +123,7 @@ class DatabaseWidget : public QStackedWidget void databaseModified(); void databaseSaved(); void databaseUnlocked(); + void databaseLockRequested(); void databaseLocked(); // Emitted in replaceDatabase, may be caused by lock, reload, unlock, load. @@ -169,6 +169,8 @@ public slots: void setupTotp(); void performAutoType(); void openUrl(); + void downloadSelectedFavicons(); + void downloadAllFavicons(); void openUrlForEntry(Entry* entry); void createGroup(); void deleteGroup(); @@ -233,6 +235,7 @@ private slots: void setClipboardTextAndMinimize(const QString& text); void processAutoOpen(); bool confirmDeleteEntries(QList entries, bool permanent); + void performIconDownloads(const QList& entries, bool force = false); QSharedPointer m_db; diff --git a/src/gui/EditWidgetIcons.cpp b/src/gui/EditWidgetIcons.cpp index fadfb1a1c0..83bc0fc350 100644 --- a/src/gui/EditWidgetIcons.cpp +++ b/src/gui/EditWidgetIcons.cpp @@ -28,11 +28,8 @@ #include "core/Tools.h" #include "gui/IconModels.h" #include "gui/MessageBox.h" - #ifdef WITH_XC_NETWORKING -#include -#include -#include +#include "core/IconDownloader.h" #endif IconStruct::IconStruct() @@ -46,12 +43,11 @@ EditWidgetIcons::EditWidgetIcons(QWidget* parent) , m_ui(new Ui::EditWidgetIcons()) , m_db(nullptr) , m_applyIconTo(ApplyIconToOptions::THIS_ONLY) -#ifdef WITH_XC_NETWORKING - , m_netMgr(new QNetworkAccessManager(this)) - , m_reply(nullptr) -#endif , m_defaultIconModel(new DefaultIconModel(this)) , m_customIconModel(new CustomIconModel(this)) +#ifdef WITH_XC_NETWORKING + , m_downloader(new IconDownloader()) +#endif { m_ui->setupUi(this); @@ -75,6 +71,11 @@ EditWidgetIcons::EditWidgetIcons(QWidget* parent) this, SIGNAL(widgetUpdated())); connect(m_ui->customIconsView->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SIGNAL(widgetUpdated())); +#ifdef WITH_XC_NETWORKING + connect(m_downloader.data(), + SIGNAL(finished(const QString&, const QImage&)), + SLOT(iconReceived(const QString&, const QImage&))); +#endif // clang-format on m_ui->faviconButton->setVisible(false); @@ -177,7 +178,7 @@ QMenu* EditWidgetIcons::createApplyIconToMenu() void EditWidgetIcons::setUrl(const QString& url) { #ifdef WITH_XC_NETWORKING - m_url = QUrl(url); + m_url = url; m_ui->faviconButton->setVisible(!url.isEmpty()); #else Q_UNUSED(url); @@ -185,223 +186,95 @@ void EditWidgetIcons::setUrl(const QString& url) #endif } -#ifdef WITH_XC_NETWORKING -namespace -{ - // Try to get the 2nd level domain of the host part of a QUrl. For example, - // "foo.bar.example.com" would become "example.com", and "foo.bar.example.co.uk" - // would become "example.co.uk". - QString getSecondLevelDomain(const QUrl& url) - { - QString fqdn = url.host(); - fqdn.truncate(fqdn.length() - url.topLevelDomain().length()); - QStringList parts = fqdn.split('.'); - QString newdom = parts.takeLast() + url.topLevelDomain(); - return newdom; - } - - QUrl convertVariantToUrl(const QVariant& var) - { - QUrl url; - if (var.canConvert()) - url = var.toUrl(); - return url; - } - - QUrl getRedirectTarget(QNetworkReply* reply) - { - QVariant var = reply->attribute(QNetworkRequest::RedirectionTargetAttribute); - QUrl url = convertVariantToUrl(var); - return url; - } -} // namespace -#endif - void EditWidgetIcons::downloadFavicon() { #ifdef WITH_XC_NETWORKING - m_ui->faviconButton->setDisabled(true); - - m_redirects = 0; - m_urlsToTry.clear(); - - QString fullyQualifiedDomain = m_url.host(); - - // Determine if host portion of URL is an IP address by resolving it and - // searching for a match with the returned address(es). - bool hostIsIp = false; - QList hostAddressess = QHostInfo::fromName(fullyQualifiedDomain).addresses(); - for (auto addr : hostAddressess) { - if (addr.toString() == fullyQualifiedDomain) { - hostIsIp = true; - } - } - - // Determine the second-level domain, if available - QString secondLevelDomain; - if (!hostIsIp) { - secondLevelDomain = getSecondLevelDomain(m_url); - } - - // Start with the "fallback" url (if enabled) to try to get the best favicon - if (config()->get("security/IconDownloadFallback", false).toBool()) { - QUrl fallbackUrl = QUrl("https://icons.duckduckgo.com"); - fallbackUrl.setPath("/ip3/" + QUrl::toPercentEncoding(fullyQualifiedDomain) + ".ico"); - m_urlsToTry.append(fallbackUrl); - - // Also try a direct pull of the second-level domain (if possible) - if (!hostIsIp && fullyQualifiedDomain != secondLevelDomain) { - fallbackUrl.setPath("/ip3/" + QUrl::toPercentEncoding(secondLevelDomain) + ".ico"); - m_urlsToTry.append(fallbackUrl); - } - } - - // Add a direct pull of the website's own favicon.ico file - m_urlsToTry.append(QUrl(m_url.scheme() + "://" + fullyQualifiedDomain + "/favicon.ico")); - - // Also try a direct pull of the second-level domain (if possible) - if (!hostIsIp && fullyQualifiedDomain != secondLevelDomain) { - m_urlsToTry.append(QUrl(m_url.scheme() + "://" + secondLevelDomain + "/favicon.ico")); + if (!m_url.isEmpty()) { + m_downloader->setUrl(m_url); + m_downloader->download(); } - - // Use the first URL to start the download process - // If a favicon is not found, the next URL will be tried - startFetchFavicon(m_urlsToTry.takeFirst()); #endif } -void EditWidgetIcons::fetchReadyRead() +void EditWidgetIcons::iconReceived(const QString& url, const QImage& icon) { #ifdef WITH_XC_NETWORKING - m_bytesReceived += m_reply->readAll(); -#endif -} - -void EditWidgetIcons::fetchFinished() -{ -#ifdef WITH_XC_NETWORKING - QImage image; - bool fallbackEnabled = config()->get("security/IconDownloadFallback", false).toBool(); - bool error = (m_reply->error() != QNetworkReply::NoError); - QUrl redirectTarget = getRedirectTarget(m_reply); - - m_reply->deleteLater(); - m_reply = nullptr; - - if (!error) { - if (redirectTarget.isValid()) { - // Redirected, we need to follow it, or fall through if we have - // done too many redirects already. - if (m_redirects < 5) { - m_redirects++; - if (redirectTarget.isRelative()) - redirectTarget = m_fetchUrl.resolved(redirectTarget); - startFetchFavicon(redirectTarget); - return; - } - } else { - // No redirect, and we theoretically have some icon data now. - image.loadFromData(m_bytesReceived); - } - } - - if (!image.isNull()) { - if (!addCustomIcon(image)) { - emit messageEditEntry(tr("Custom icon already exists"), MessageWidget::Information); - } else if (!isVisible()) { - // Show confirmation message if triggered from Entry tab download button - emit messageEditEntry(tr("Custom icon successfully downloaded"), MessageWidget::Positive); + Q_UNUSED(url); + if (icon.isNull()) { + QString message(tr("Unable to fetch favicon.")); + if (!config()->get("security/IconDownloadFallback", false).toBool()) { + message.append("\n").append( + tr("You can enable the DuckDuckGo website icon service under Tools -> Settings -> Security")); } - } else if (!m_urlsToTry.empty()) { - m_redirects = 0; - startFetchFavicon(m_urlsToTry.takeFirst()); + emit messageEditEntry(message, MessageWidget::Error); return; - } else { - if (!fallbackEnabled) { - emit messageEditEntry( - tr("Unable to fetch favicon.") + "\n" - + tr("You can enable the DuckDuckGo website icon service under Tools -> Settings -> Security"), - MessageWidget::Error); - } else { - emit messageEditEntry(tr("Unable to fetch favicon."), MessageWidget::Error); - } } - m_ui->faviconButton->setDisabled(false); + if (!addCustomIcon(icon)) { + emit messageEditEntry(tr("Existing icon selected."), MessageWidget::Information); + } +#else + Q_UNUSED(url); + Q_UNUSED(icon); #endif } void EditWidgetIcons::abortRequests() { #ifdef WITH_XC_NETWORKING - if (m_reply) { - m_reply->abort(); + if (m_downloader) { + m_downloader->abortDownload(); } #endif } -void EditWidgetIcons::startFetchFavicon(const QUrl& url) -{ -#ifdef WITH_XC_NETWORKING - m_bytesReceived.clear(); - - m_fetchUrl = url; - - QNetworkRequest request(url); - - m_reply = m_netMgr->get(request); - connect(m_reply, &QNetworkReply::finished, this, &EditWidgetIcons::fetchFinished); - connect(m_reply, &QIODevice::readyRead, this, &EditWidgetIcons::fetchReadyRead); -#else - Q_UNUSED(url); -#endif -} - void EditWidgetIcons::addCustomIconFromFile() { - if (m_db) { - QString filter = QString("%1 (%2);;%3 (*)").arg(tr("Images"), Tools::imageReaderFilter(), tr("All files")); - - auto filenames = QFileDialog::getOpenFileNames(this, tr("Select Image(s)"), "", filter); - if (!filenames.empty()) { - QStringList errornames; - int numexisting = 0; - for (const auto& filename : filenames) { - if (!filename.isEmpty()) { - auto icon = QImage(filename); - if (icon.isNull()) { - errornames << filename; - } else if (!addCustomIcon(icon)) { - // Icon already exists in database - ++numexisting; - } + if (!m_db) { + return; + } + + QString filter = QString("%1 (%2);;%3 (*)").arg(tr("Images"), Tools::imageReaderFilter(), tr("All files")); + + auto filenames = QFileDialog::getOpenFileNames(this, tr("Select Image(s)"), "", filter); + if (!filenames.empty()) { + QStringList errornames; + int numexisting = 0; + for (const auto& filename : filenames) { + if (!filename.isEmpty()) { + auto icon = QImage(filename); + if (icon.isNull()) { + errornames << filename; + } else if (!addCustomIcon(icon)) { + // Icon already exists in database + ++numexisting; } } + } - int numloaded = filenames.size() - errornames.size() - numexisting; - QString msg; + int numloaded = filenames.size() - errornames.size() - numexisting; + QString msg; - if (numloaded > 0) { - msg = tr("Successfully loaded %1 of %n icon(s)", "", filenames.size()).arg(numloaded); - } else { - msg = tr("No icons were loaded"); - } + if (numloaded > 0) { + msg = tr("Successfully loaded %1 of %n icon(s)", "", filenames.size()).arg(numloaded); + } else { + msg = tr("No icons were loaded"); + } - if (numexisting > 0) { - msg += "\n" + tr("%n icon(s) already exist in the database", "", numexisting); - } + if (numexisting > 0) { + msg += "\n" + tr("%n icon(s) already exist in the database", "", numexisting); + } - if (!errornames.empty()) { - // Show the first 8 icons that failed to load - errornames = errornames.mid(0, 8); - emit messageEditEntry(msg + "\n" + tr("The following icon(s) failed:", "", errornames.size()) + "\n" - + errornames.join("\n"), - MessageWidget::Error); - } else if (numloaded > 0) { - emit messageEditEntry(msg, MessageWidget::Positive); - } else { - emit messageEditEntry(msg, MessageWidget::Information); - } + if (!errornames.empty()) { + // Show the first 8 icons that failed to load + errornames = errornames.mid(0, 8); + emit messageEditEntry(msg + "\n" + tr("The following icon(s) failed:", "", errornames.size()) + "\n" + + errornames.join("\n"), + MessageWidget::Error); + } else if (numloaded > 0) { + emit messageEditEntry(msg, MessageWidget::Positive); + } else { + emit messageEditEntry(msg, MessageWidget::Information); } } } diff --git a/src/gui/EditWidgetIcons.h b/src/gui/EditWidgetIcons.h index e7c1b3a16d..2a95445f97 100644 --- a/src/gui/EditWidgetIcons.h +++ b/src/gui/EditWidgetIcons.h @@ -25,6 +25,7 @@ #include #include "config-keepassx.h" +#include "core/Entry.h" #include "core/Global.h" #include "gui/MessageWidget.h" @@ -32,8 +33,7 @@ class Database; class DefaultIconModel; class CustomIconModel; #ifdef WITH_XC_NETWORKING -class QNetworkAccessManager; -class QNetworkReply; +class IconDownloader; #endif namespace Ui @@ -87,9 +87,7 @@ public slots: private slots: void downloadFavicon(); - void startFetchFavicon(const QUrl& url); - void fetchFinished(); - void fetchReadyRead(); + void iconReceived(const QString& url, const QImage& icon); void addCustomIconFromFile(); bool addCustomIcon(const QImage& icon); void removeCustomIcon(); @@ -106,17 +104,12 @@ private slots: QSharedPointer m_db; QUuid m_currentUuid; ApplyIconToOptions m_applyIconTo; -#ifdef WITH_XC_NETWORKING - QUrl m_url; - QUrl m_fetchUrl; - QList m_urlsToTry; - QByteArray m_bytesReceived; - QNetworkAccessManager* m_netMgr; - QNetworkReply* m_reply; - int m_redirects; -#endif DefaultIconModel* const m_defaultIconModel; CustomIconModel* const m_customIconModel; +#ifdef WITH_XC_NETWORKING + QScopedPointer m_downloader; + QString m_url; +#endif Q_DISABLE_COPY(EditWidgetIcons) }; diff --git a/src/gui/IconDownloaderDialog.cpp b/src/gui/IconDownloaderDialog.cpp new file mode 100644 index 0000000000..ebe6980a2d --- /dev/null +++ b/src/gui/IconDownloaderDialog.cpp @@ -0,0 +1,198 @@ +/* + * 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 "IconDownloaderDialog.h" +#include "ui_IconDownloaderDialog.h" + +#include "core/AsyncTask.h" +#include "core/Config.h" +#include "core/Entry.h" +#include "core/Global.h" +#include "core/Group.h" +#include "core/IconDownloader.h" +#include "core/Metadata.h" +#include "core/Tools.h" +#include "gui/IconModels.h" +#ifdef Q_OS_MACOS +#include "gui/macutils/MacUtils.h" +#endif + +#include + +IconDownloaderDialog::IconDownloaderDialog(QWidget* parent) + : QDialog(parent) + , m_ui(new Ui::IconDownloaderDialog()) + , m_dataModel(new QStandardItemModel(this)) +{ + setWindowFlags(Qt::Window); + setAttribute(Qt::WA_DeleteOnClose); + + m_ui->setupUi(this); + showFallbackMessage(false); + + m_dataModel->clear(); + m_dataModel->setHorizontalHeaderLabels({tr("URL"), tr("Status")}); + + m_ui->tableView->setModel(m_dataModel); + m_ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + + connect(m_ui->cancelButton, SIGNAL(clicked()), SLOT(abortDownloads())); + connect(m_ui->closeButton, SIGNAL(clicked()), SLOT(close())); +} + +IconDownloaderDialog::~IconDownloaderDialog() +{ + abortDownloads(); +} + +void IconDownloaderDialog::downloadFavicons(const QSharedPointer& database, + const QList& entries, + bool force) +{ + m_db = database; + m_urlToEntries.clear(); + abortDownloads(); + for (const auto& e : entries) { + // Only consider entries with a valid URL and without a custom icon + auto webUrl = e->webUrl(); + if (!webUrl.isEmpty() && (force || e->iconUuid().isNull())) { + m_urlToEntries.insert(webUrl, e); + } + } + + if (m_urlToEntries.count() > 0) { +#ifdef Q_OS_MACOS + macUtils()->raiseOwnWindow(); + Tools::wait(100); +#endif + showFallbackMessage(false); + m_ui->progressLabel->setText(tr("Please wait, processing entry list...")); + open(); + QApplication::processEvents(); + + for (const auto& url : m_urlToEntries.uniqueKeys()) { + m_dataModel->appendRow(QList() + << new QStandardItem(url) << new QStandardItem(tr("Downloading..."))); + m_activeDownloaders.append(createDownloader(url)); + } + + // Setup the dialog + updateProgressBar(); + updateCancelButton(); + QApplication::processEvents(); + + // Start the downloads + for (auto downloader : m_activeDownloaders) { + downloader->download(); + } + } +} + +IconDownloader* IconDownloaderDialog::createDownloader(const QString& url) +{ + auto downloader = new IconDownloader(); + connect(downloader, + SIGNAL(finished(const QString&, const QImage&)), + this, + SLOT(downloadFinished(const QString&, const QImage&))); + + downloader->setUrl(url); + return downloader; +} + +void IconDownloaderDialog::downloadFinished(const QString& url, const QImage& icon) +{ + // Prevent re-entrance from multiple calls finishing at the same time + QMutexLocker locker(&m_mutex); + + // Cleanup the icon downloader that sent this signal + auto downloader = qobject_cast(sender()); + if (downloader) { + downloader->deleteLater(); + m_activeDownloaders.removeAll(downloader); + } + + updateProgressBar(); + updateCancelButton(); + + if (m_db && !icon.isNull()) { + // Don't add an icon larger than 128x128, but retain original size if smaller + auto scaledicon = icon; + if (icon.width() > 128 || icon.height() > 128) { + scaledicon = icon.scaled(128, 128); + } + + QUuid uuid = m_db->metadata()->findCustomIcon(scaledicon); + if (uuid.isNull()) { + uuid = QUuid::createUuid(); + m_db->metadata()->addCustomIcon(uuid, scaledicon); + updateTable(url, tr("Ok")); + } else { + updateTable(url, tr("Already Exists")); + } + + // Set the icon on all the entries associated with this url + for (const auto entry : m_urlToEntries.values(url)) { + entry->setIcon(uuid); + } + } else { + showFallbackMessage(true); + updateTable(url, tr("Download Failed")); + return; + } +} + +void IconDownloaderDialog::showFallbackMessage(bool state) +{ + // Show fallback message if the option is not active + bool show = state && !config()->get("security/IconDownloadFallback").toBool(); + m_ui->fallbackLabel->setVisible(show); +} + +void IconDownloaderDialog::updateProgressBar() +{ + int total = m_urlToEntries.uniqueKeys().count(); + int value = total - m_activeDownloaders.count(); + m_ui->progressBar->setValue(value); + m_ui->progressBar->setMaximum(total); + m_ui->progressLabel->setText( + tr("Downloading favicons (%1/%2)...").arg(QString::number(value), QString::number(total))); +} + +void IconDownloaderDialog::updateCancelButton() +{ + m_ui->cancelButton->setEnabled(!m_activeDownloaders.isEmpty()); +} + +void IconDownloaderDialog::updateTable(const QString& url, const QString& message) +{ + for (int i = 0; i < m_dataModel->rowCount(); ++i) { + if (m_dataModel->item(i, 0)->text() == url) { + m_dataModel->item(i, 1)->setText(message); + } + } +} + +void IconDownloaderDialog::abortDownloads() +{ + for (auto* downloader : m_activeDownloaders) { + delete downloader; + } + m_activeDownloaders.clear(); + updateProgressBar(); + updateCancelButton(); +} diff --git a/src/gui/IconDownloaderDialog.h b/src/gui/IconDownloaderDialog.h new file mode 100644 index 0000000000..955e85a98a --- /dev/null +++ b/src/gui/IconDownloaderDialog.h @@ -0,0 +1,69 @@ +/* + * 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 . + */ + +#ifndef KEEPASSX_ICONDOWNLOADERDIALOG_H +#define KEEPASSX_ICONDOWNLOADERDIALOG_H + +#include +#include +#include + +#include "gui/MessageWidget.h" + +class Database; +class Entry; +class CustomIconModel; +class IconDownloader; + +namespace Ui +{ + class IconDownloaderDialog; +} + +class IconDownloaderDialog : public QDialog +{ + Q_OBJECT + +public: + explicit IconDownloaderDialog(QWidget* parent = nullptr); + ~IconDownloaderDialog() override; + + void downloadFavicons(const QSharedPointer& database, const QList& entries, bool force = false); + +private slots: + void downloadFinished(const QString& url, const QImage& icon); + void abortDownloads(); + +private: + IconDownloader* createDownloader(const QString& url); + + void showFallbackMessage(bool state); + void updateTable(const QString& url, const QString& message); + void updateProgressBar(); + void updateCancelButton(); + + QScopedPointer m_ui; + QStandardItemModel* m_dataModel; + QSharedPointer m_db; + QMultiMap m_urlToEntries; + QList m_activeDownloaders; + QMutex m_mutex; + + Q_DISABLE_COPY(IconDownloaderDialog) +}; + +#endif // KEEPASSX_ICONDOWNLOADERDIALOG_H diff --git a/src/gui/IconDownloaderDialog.ui b/src/gui/IconDownloaderDialog.ui new file mode 100644 index 0000000000..a657f7acbe --- /dev/null +++ b/src/gui/IconDownloaderDialog.ui @@ -0,0 +1,154 @@ + + + IconDownloaderDialog + + + + 0 + 0 + 453 + 339 + + + + + 0 + 0 + + + + Download Favicons + + + + + + + 75 + true + + + + Downloading favicon 0/0... + + + + + + + QLayout::SetDefaultConstraint + + + + + 0 + + + + + + + + 0 + 0 + + + + + 100 + 16777215 + + + + Cancel + + + + + + + + + + 75 + false + true + + + + Having trouble downloading icons? +You can enable the DuckDuckGo website icon service in the security section of the application settings. + + + false + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + Qt::ScrollBarAsNeeded + + + Qt::ScrollBarAlwaysOff + + + QAbstractScrollArea::AdjustToContents + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + QAbstractItemView::SelectRows + + + Qt::ElideNone + + + true + + + false + + + 20 + + + 20 + + + true + + + true + + + false + + + + + + + Close + + + true + + + + + + + + + diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index c0a4a04004..ad00a66211 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -246,6 +246,7 @@ MainWindow::MainWindow() m_ui->actionEntryDelete->setShortcut(Qt::Key_Delete); m_ui->actionEntryClone->setShortcut(Qt::CTRL + Qt::Key_K); m_ui->actionEntryTotp->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_T); + m_ui->actionEntryDownloadIcon->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_D); m_ui->actionEntryCopyTotp->setShortcut(Qt::CTRL + Qt::Key_T); m_ui->actionEntryCopyUsername->setShortcut(Qt::CTRL + Qt::Key_B); m_ui->actionEntryCopyPassword->setShortcut(Qt::CTRL + Qt::Key_C); @@ -261,6 +262,7 @@ MainWindow::MainWindow() m_ui->actionEntryDelete->setShortcutVisibleInContextMenu(true); m_ui->actionEntryClone->setShortcutVisibleInContextMenu(true); m_ui->actionEntryTotp->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryDownloadIcon->setShortcutVisibleInContextMenu(true); m_ui->actionEntryCopyTotp->setShortcutVisibleInContextMenu(true); m_ui->actionEntryCopyUsername->setShortcutVisibleInContextMenu(true); m_ui->actionEntryCopyPassword->setShortcutVisibleInContextMenu(true); @@ -304,11 +306,13 @@ MainWindow::MainWindow() m_ui->actionEntryCopyUsername->setIcon(filePath()->icon("actions", "username-copy")); m_ui->actionEntryCopyPassword->setIcon(filePath()->icon("actions", "password-copy")); m_ui->actionEntryCopyURL->setIcon(filePath()->icon("actions", "url-copy")); + m_ui->actionEntryDownloadIcon->setIcon(filePath()->icon("actions", "favicon-download")); m_ui->actionGroupNew->setIcon(filePath()->icon("actions", "group-new")); m_ui->actionGroupEdit->setIcon(filePath()->icon("actions", "group-edit")); m_ui->actionGroupDelete->setIcon(filePath()->icon("actions", "group-delete")); m_ui->actionGroupEmptyRecycleBin->setIcon(filePath()->icon("actions", "group-empty-trash")); + m_ui->actionGroupDownloadFavicons->setIcon(filePath()->icon("actions", "favicon-download")); m_ui->actionSettings->setIcon(filePath()->icon("actions", "configure")); m_ui->actionPasswordGenerator->setIcon(filePath()->icon("actions", "password-generator")); @@ -376,6 +380,7 @@ MainWindow::MainWindow() m_actionMultiplexer.connect(m_ui->actionEntryCopyNotes, SIGNAL(triggered()), SLOT(copyNotes())); m_actionMultiplexer.connect(m_ui->actionEntryAutoType, SIGNAL(triggered()), SLOT(performAutoType())); m_actionMultiplexer.connect(m_ui->actionEntryOpenUrl, SIGNAL(triggered()), SLOT(openUrl())); + m_actionMultiplexer.connect(m_ui->actionEntryDownloadIcon, SIGNAL(triggered()), SLOT(downloadSelectedFavicons())); m_actionMultiplexer.connect(m_ui->actionGroupNew, SIGNAL(triggered()), SLOT(createGroup())); m_actionMultiplexer.connect(m_ui->actionGroupEdit, SIGNAL(triggered()), SLOT(switchToGroupEdit())); @@ -383,6 +388,7 @@ MainWindow::MainWindow() m_actionMultiplexer.connect(m_ui->actionGroupEmptyRecycleBin, SIGNAL(triggered()), SLOT(emptyRecycleBin())); m_actionMultiplexer.connect(m_ui->actionGroupSortAsc, SIGNAL(triggered()), SLOT(sortGroupsAsc())); m_actionMultiplexer.connect(m_ui->actionGroupSortDesc, SIGNAL(triggered()), SLOT(sortGroupsDesc())); + m_actionMultiplexer.connect(m_ui->actionGroupDownloadFavicons, SIGNAL(triggered()), SLOT(downloadAllFavicons())); connect(m_ui->actionSettings, SIGNAL(toggled(bool)), SLOT(switchToSettings(bool))); connect(m_ui->actionPasswordGenerator, SIGNAL(toggled(bool)), SLOT(switchToPasswordGen(bool))); @@ -419,6 +425,11 @@ MainWindow::MainWindow() m_ui->actionCheckForUpdates->setVisible(false); #endif +#ifndef WITH_XC_NETWORKING + m_ui->actionGroupDownloadFavicons->setVisible(false); + m_ui->actionEntryDownloadIcon->setVisible(false); +#endif + // clang-format off connect(m_ui->tabWidget, SIGNAL(messageGlobal(QString,MessageWidget::MessageType)), @@ -577,6 +588,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) bool entriesSelected = dbWidget->numberOfSelectedEntries() > 0 && hasFocus; bool groupSelected = dbWidget->isGroupSelected(); bool currentGroupHasChildren = dbWidget->currentGroup()->hasChildren(); + bool currentGroupHasEntries = !dbWidget->currentGroup()->entries().isEmpty(); bool recycleBinSelected = dbWidget->isRecycleBinSelected(); m_ui->actionEntryNew->setEnabled(true); @@ -596,6 +608,8 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionEntryCopyTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); m_ui->actionEntrySetupTotp->setEnabled(singleEntrySelected); m_ui->actionEntryTotpQRCode->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); + m_ui->actionEntryDownloadIcon->setEnabled((entriesSelected && !singleEntrySelected) + || (singleEntrySelected && dbWidget->currentEntryHasUrl())); m_ui->actionGroupNew->setEnabled(groupSelected); m_ui->actionGroupEdit->setEnabled(groupSelected); m_ui->actionGroupDelete->setEnabled(groupSelected && dbWidget->canDeleteCurrentGroup()); @@ -603,6 +617,9 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionGroupSortDesc->setEnabled(groupSelected && currentGroupHasChildren); m_ui->actionGroupEmptyRecycleBin->setVisible(recycleBinSelected); m_ui->actionGroupEmptyRecycleBin->setEnabled(recycleBinSelected); + m_ui->actionGroupDownloadFavicons->setVisible(!recycleBinSelected); + m_ui->actionGroupDownloadFavicons->setEnabled(groupSelected && currentGroupHasEntries + && !recycleBinSelected); m_ui->actionChangeMasterKey->setEnabled(true); m_ui->actionChangeDatabaseSettings->setEnabled(true); m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave()); diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index de2867406a..807cdcf6e4 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -283,6 +283,8 @@ + + @@ -294,6 +296,8 @@ + + @@ -467,6 +471,14 @@ &Delete group + + + false + + + Downlo&ad all favicons + + false @@ -570,6 +582,11 @@ Perform &Auto-Type + + + Download favicon + + false diff --git a/src/updatecheck/UpdateChecker.cpp b/src/updatecheck/UpdateChecker.cpp index 1453129074..c36879707d 100644 --- a/src/updatecheck/UpdateChecker.cpp +++ b/src/updatecheck/UpdateChecker.cpp @@ -16,18 +16,21 @@ */ #include "UpdateChecker.h" + #include "config-keepassx.h" #include "core/Clock.h" #include "core/Config.h" +#include "core/NetworkManager.h" + +#include +#include #include -#include -#include +#include UpdateChecker* UpdateChecker::m_instance(nullptr); UpdateChecker::UpdateChecker(QObject* parent) : QObject(parent) - , m_netMgr(new QNetworkAccessManager(this)) , m_reply(nullptr) , m_isManuallyRequested(false) { @@ -56,7 +59,7 @@ void UpdateChecker::checkForUpdates(bool manuallyRequested) QNetworkRequest request(apiUrl); request.setRawHeader("Accept", "application/json"); - m_reply = m_netMgr->get(request); + m_reply = getNetMgr()->get(request); connect(m_reply, &QNetworkReply::finished, this, &UpdateChecker::fetchFinished); connect(m_reply, &QIODevice::readyRead, this, &UpdateChecker::fetchReadyRead); diff --git a/src/updatecheck/UpdateChecker.h b/src/updatecheck/UpdateChecker.h index 64430bda3b..9e804b2740 100644 --- a/src/updatecheck/UpdateChecker.h +++ b/src/updatecheck/UpdateChecker.h @@ -20,7 +20,6 @@ #include #include -class QNetworkAccessManager; class QNetworkReply; class UpdateChecker : public QObject @@ -42,7 +41,6 @@ private slots: void fetchReadyRead(); private: - QNetworkAccessManager* m_netMgr; QNetworkReply* m_reply; QByteArray m_bytesReceived; bool m_isManuallyRequested;