Download all favicons ()

* 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.
This commit is contained in:
Sami Vänttinen 2019-07-07 22:29:11 +03:00 committed by Jonathan White
parent 65cec901d5
commit 6ae27fa47b
19 changed files with 980 additions and 221 deletions

@ -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)

@ -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);

204
src/core/IconDownloader.cpp Normal file

@ -0,0 +1,204 @@
/*
* Copyright (C) 2019 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 "IconDownloader.h"
#include "core/Config.h"
#include "core/NetworkManager.h"
#include <QHostInfo>
#include <QtNetwork>
#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<QUrl>()) {
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<QHostAddress> 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);
}
}

63
src/core/IconDownloader.h Normal file

@ -0,0 +1,63 @@
/*
* Copyright (C) 2019 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_ICONDOWNLOADER_H
#define KEEPASSXC_ICONDOWNLOADER_H
#include <QImage>
#include <QObject>
#include <QTimer>
#include <QUrl>
#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<QUrl> m_urlsToTry;
QByteArray m_bytesReceived;
QNetworkReply* m_reply;
QTimer m_timeout;
int m_redirects;
};
#endif // KEEPASSXC_ICONDOWNLOADER_H

@ -0,0 +1,33 @@
/*
* Copyright (C) 2019 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 "config-keepassx.h"
#ifdef WITH_XC_NETWORKING
#include "NetworkManager.h"
#include <QCoreApplication>
QNetworkAccessManager* g_netMgr = nullptr;
QNetworkAccessManager* getNetMgr()
{
if (!g_netMgr) {
g_netMgr = new QNetworkAccessManager(QCoreApplication::instance());
}
return g_netMgr;
}
#endif

34
src/core/NetworkManager.h Normal file

@ -0,0 +1,34 @@
/*
* Copyright (C) 2019 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_NETWORKMANAGER_H
#define KEEPASSXC_NETWORKMANAGER_H
#include "config-keepassx.h"
#include <QtGlobal>
#ifdef WITH_XC_NETWORKING
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
QNetworkAccessManager* getNetMgr();
#else
Q_STATIC_ASSERT_X(false, "Qt Networking used when WITH_XC_NETWORKING is disabled!");
#endif
#endif // KEEPASSXC_NETWORKMANAGER_H

@ -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());

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>684</width>
<height>1030</height>
<height>860</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
@ -329,6 +329,55 @@
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,1">
<item>
<widget class="QLabel" name="faviconTimeoutLabel">
<property name="text">
<string>Favicon download timeout:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="faviconTimeoutSpinBox">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="suffix">
<string comment="Seconds"> sec</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>60</number>
</property>
<property name="value">
<number>10</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>

@ -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 <sys/vfs.h>
#endif
@ -650,6 +654,41 @@ void DatabaseWidget::openUrl()
}
}
void DatabaseWidget::downloadSelectedFavicons()
{
#ifdef WITH_XC_NETWORKING
QList<Entry*> 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<Entry*>& 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()) {

@ -25,12 +25,11 @@
#include <QTimer>
#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 @@ signals:
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:
void setClipboardTextAndMinimize(const QString& text);
void processAutoOpen();
bool confirmDeleteEntries(QList<Entry*> entries, bool permanent);
void performIconDownloads(const QList<Entry*>& entries, bool force = false);
QSharedPointer<Database> m_db;

@ -28,11 +28,8 @@
#include "core/Tools.h"
#include "gui/IconModels.h"
#include "gui/MessageBox.h"
#ifdef WITH_XC_NETWORKING
#include <QHostInfo>
#include <QNetworkAccessManager>
#include <QtNetwork>
#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<QUrl>())
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<QHostAddress> hostAddressess = QHostInfo::fromName(fullyQualifiedDomain).addresses();
for (auto addr : hostAddressess) {
if (addr.toString() == fullyQualifiedDomain) {
hostIsIp = true;
}
if (!m_url.isEmpty()) {
m_downloader->setUrl(m_url);
m_downloader->download();
}
// 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"));
}
// 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);
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"));
}
}
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);
}
} 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"));
if (!m_db) {
return;
}
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;
}
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);
}
}
}

@ -25,6 +25,7 @@
#include <QWidget>
#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 @@ signals:
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:
QSharedPointer<Database> m_db;
QUuid m_currentUuid;
ApplyIconToOptions m_applyIconTo;
#ifdef WITH_XC_NETWORKING
QUrl m_url;
QUrl m_fetchUrl;
QList<QUrl> 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<IconDownloader> m_downloader;
QString m_url;
#endif
Q_DISABLE_COPY(EditWidgetIcons)
};

@ -0,0 +1,198 @@
/*
* Copyright (C) 2019 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 "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 <QMutexLocker>
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>& database,
const QList<Entry*>& 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<QStandardItem*>()
<< 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<IconDownloader*>(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();
}

@ -0,0 +1,69 @@
/*
* Copyright (C) 2019 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 KEEPASSX_ICONDOWNLOADERDIALOG_H
#define KEEPASSX_ICONDOWNLOADERDIALOG_H
#include <QDialog>
#include <QMutex>
#include <QStandardItemModel>
#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>& database, const QList<Entry*>& 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<Ui::IconDownloaderDialog> m_ui;
QStandardItemModel* m_dataModel;
QSharedPointer<Database> m_db;
QMultiMap<QString, Entry*> m_urlToEntries;
QList<IconDownloader*> m_activeDownloaders;
QMutex m_mutex;
Q_DISABLE_COPY(IconDownloaderDialog)
};
#endif // KEEPASSX_ICONDOWNLOADERDIALOG_H

@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>IconDownloaderDialog</class>
<widget class="QDialog" name="IconDownloaderDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>453</width>
<height>339</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Download Favicons</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="progressLabel">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Downloading favicon 0/0...</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancelButton">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="fallbackLabel">
<property name="font">
<font>
<weight>75</weight>
<italic>false</italic>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Having trouble downloading icons?
You can enable the DuckDuckGo website icon service in the security section of the application settings.</string>
</property>
<property name="scaledContents">
<bool>false</bool>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QTableView" name="tableView">
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAsNeeded</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContents</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="textElideMode">
<enum>Qt::ElideNone</enum>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
<attribute name="horizontalHeaderMinimumSectionSize">
<number>20</number>
</attribute>
<attribute name="horizontalHeaderDefaultSectionSize">
<number>20</number>
</attribute>
<attribute name="horizontalHeaderShowSortIndicator" stdset="0">
<bool>true</bool>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<widget class="QPushButton" name="closeButton">
<property name="text">
<string>Close</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources/>
<connections/>
</ui>

@ -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());

@ -283,6 +283,8 @@
<addaction name="menuEntryTotp"/>
<addaction name="actionEntryOpenUrl"/>
<addaction name="actionEntryAutoType"/>
<addaction name="separator"/>
<addaction name="actionEntryDownloadIcon"/>
</widget>
<widget class="QMenu" name="menuGroups">
<property name="title">
@ -294,6 +296,8 @@
<addaction name="actionGroupDelete"/>
<addaction name="actionGroupEmptyRecycleBin"/>
<addaction name="separator"/>
<addaction name="actionGroupDownloadFavicons"/>
<addaction name="separator"/>
<addaction name="actionGroupSortAsc"/>
<addaction name="actionGroupSortDesc"/>
</widget>
@ -467,6 +471,14 @@
<string>&amp;Delete group</string>
</property>
</action>
<action name="actionGroupDownloadFavicons">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Downlo&amp;ad all favicons</string>
</property>
</action>
<action name="actionGroupSortAsc">
<property name="enabled">
<bool>false</bool>
@ -570,6 +582,11 @@
<string>Perform &amp;Auto-Type</string>
</property>
</action>
<action name="actionEntryDownloadIcon">
<property name="text">
<string>Download favicon</string>
</property>
</action>
<action name="actionEntryOpenUrl">
<property name="enabled">
<bool>false</bool>

@ -16,18 +16,21 @@
*/
#include "UpdateChecker.h"
#include "config-keepassx.h"
#include "core/Clock.h"
#include "core/Config.h"
#include "core/NetworkManager.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QtNetwork>
#include <QRegularExpression>
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);

@ -20,7 +20,6 @@
#include <QObject>
#include <QString>
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;