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.
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

View file

@ -27,6 +27,9 @@ set(keepassx_SOURCES
core/Alloc.cpp core/Alloc.cpp
core/AutoTypeAssociations.cpp core/AutoTypeAssociations.cpp
core/AutoTypeMatch.cpp core/AutoTypeMatch.cpp
core/Base32.cpp
core/Bootstrap.cpp
core/Clock.cpp
core/Compare.cpp core/Compare.cpp
core/Config.cpp core/Config.cpp
core/CsvParser.cpp core/CsvParser.cpp
@ -39,7 +42,6 @@ set(keepassx_SOURCES
core/EntrySearcher.cpp core/EntrySearcher.cpp
core/FilePath.cpp core/FilePath.cpp
core/FileWatcher.cpp core/FileWatcher.cpp
core/Bootstrap.cpp
core/Group.cpp core/Group.cpp
core/HibpOffline.cpp core/HibpOffline.cpp
core/InactivityTimer.cpp core/InactivityTimer.cpp
@ -52,10 +54,8 @@ set(keepassx_SOURCES
core/ScreenLockListenerPrivate.cpp core/ScreenLockListenerPrivate.cpp
core/TimeDelta.cpp core/TimeDelta.cpp
core/TimeInfo.cpp core/TimeInfo.cpp
core/Clock.cpp
core/Tools.cpp core/Tools.cpp
core/Translator.cpp core/Translator.cpp
core/Base32.cpp
cli/Utils.cpp cli/Utils.cpp
cli/TextStream.cpp cli/TextStream.cpp
crypto/Crypto.cpp crypto/Crypto.cpp
@ -264,7 +264,12 @@ else()
endif() endif()
if(WITH_XC_NETWORKING) 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() endif()
if(WITH_XC_TOUCHID) if(WITH_XC_TOUCHID)

View file

@ -194,6 +194,7 @@ void Config::init(const QString& fileName)
m_defaults.insert("AutoTypeStartDelay", 500); m_defaults.insert("AutoTypeStartDelay", 500);
m_defaults.insert("UseGroupIconOnEntryCreation", true); m_defaults.insert("UseGroupIconOnEntryCreation", true);
m_defaults.insert("IgnoreGroupExpansion", true); m_defaults.insert("IgnoreGroupExpansion", true);
m_defaults.insert("FaviconDownloadTimeout", 10);
m_defaults.insert("security/clearclipboard", true); m_defaults.insert("security/clearclipboard", true);
m_defaults.insert("security/clearclipboardtimeout", 10); m_defaults.insert("security/clearclipboardtimeout", 10);
m_defaults.insert("security/lockdatabaseidle", false); m_defaults.insert("security/lockdatabaseidle", false);

204
src/core/IconDownloader.cpp Normal file
View 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
View 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

View file

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

View file

@ -103,6 +103,8 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent)
#ifndef WITH_XC_NETWORKING #ifndef WITH_XC_NETWORKING
m_secUi->privacy->setVisible(false); m_secUi->privacy->setVisible(false);
m_generalUi->faviconTimeoutLabel->setVisible(false);
m_generalUi->faviconTimeoutSpinBox->setVisible(false);
#endif #endif
#ifndef WITH_XC_TOUCHID #ifndef WITH_XC_TOUCHID
@ -156,6 +158,7 @@ void ApplicationSettingsWidget::loadSettings()
m_generalUi->autoTypeEntryTitleMatchCheckBox->setChecked(config()->get("AutoTypeEntryTitleMatch").toBool()); m_generalUi->autoTypeEntryTitleMatchCheckBox->setChecked(config()->get("AutoTypeEntryTitleMatch").toBool());
m_generalUi->autoTypeEntryURLMatchCheckBox->setChecked(config()->get("AutoTypeEntryURLMatch").toBool()); m_generalUi->autoTypeEntryURLMatchCheckBox->setChecked(config()->get("AutoTypeEntryURLMatch").toBool());
m_generalUi->ignoreGroupExpansionCheckBox->setChecked(config()->get("IgnoreGroupExpansion").toBool()); m_generalUi->ignoreGroupExpansionCheckBox->setChecked(config()->get("IgnoreGroupExpansion").toBool());
m_generalUi->faviconTimeoutSpinBox->setValue(config()->get("FaviconDownloadTimeout").toInt());
if (!m_generalUi->hideWindowOnCopyCheckBox->isChecked()) { if (!m_generalUi->hideWindowOnCopyCheckBox->isChecked()) {
hideWindowOnCopyCheckBoxToggled(false); hideWindowOnCopyCheckBoxToggled(false);
@ -264,6 +267,7 @@ void ApplicationSettingsWidget::saveSettings()
config()->set("AutoTypeEntryTitleMatch", m_generalUi->autoTypeEntryTitleMatchCheckBox->isChecked()); config()->set("AutoTypeEntryTitleMatch", m_generalUi->autoTypeEntryTitleMatchCheckBox->isChecked());
config()->set("AutoTypeEntryURLMatch", m_generalUi->autoTypeEntryURLMatchCheckBox->isChecked()); config()->set("AutoTypeEntryURLMatch", m_generalUi->autoTypeEntryURLMatchCheckBox->isChecked());
int currentLangIndex = m_generalUi->languageComboBox->currentIndex(); int currentLangIndex = m_generalUi->languageComboBox->currentIndex();
config()->set("FaviconDownloadTimeout", m_generalUi->faviconTimeoutSpinBox->value());
config()->set("GUI/Language", m_generalUi->languageComboBox->itemData(currentLangIndex).toString()); config()->set("GUI/Language", m_generalUi->languageComboBox->itemData(currentLangIndex).toString());

View file

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>684</width> <width>684</width>
<height>1030</height> <height>860</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_3"> <layout class="QVBoxLayout" name="verticalLayout_3">
@ -329,6 +329,55 @@
</item> </item>
</layout> </layout>
</item> </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> </layout>
</widget> </widget>
</item> </item>

View file

@ -62,6 +62,10 @@
#include "keeshare/KeeShare.h" #include "keeshare/KeeShare.h"
#include "touchid/TouchID.h" #include "touchid/TouchID.h"
#ifdef WITH_XC_NETWORKING
#include "gui/IconDownloaderDialog.h"
#endif
#ifdef Q_OS_LINUX #ifdef Q_OS_LINUX
#include <sys/vfs.h> #include <sys/vfs.h>
#endif #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) void DatabaseWidget::openUrlForEntry(Entry* entry)
{ {
Q_ASSERT(entry); Q_ASSERT(entry);
@ -1275,6 +1314,8 @@ bool DatabaseWidget::lock()
return true; return true;
} }
emit databaseLockRequested();
clipboard()->clearCopiedText(); clipboard()->clearCopiedText();
if (isEditWidgetModified()) { if (isEditWidgetModified()) {

View file

@ -25,12 +25,11 @@
#include <QTimer> #include <QTimer>
#include "DatabaseOpenDialog.h" #include "DatabaseOpenDialog.h"
#include "config-keepassx.h"
#include "gui/MessageWidget.h" #include "gui/MessageWidget.h"
#include "gui/csvImport/CsvImportWizard.h" #include "gui/csvImport/CsvImportWizard.h"
#include "gui/entry/EntryModel.h" #include "gui/entry/EntryModel.h"
#include "config-keepassx.h"
class DatabaseOpenWidget; class DatabaseOpenWidget;
class KeePass1OpenWidget; class KeePass1OpenWidget;
class OpVaultOpenWidget; class OpVaultOpenWidget;
@ -124,6 +123,7 @@ signals:
void databaseModified(); void databaseModified();
void databaseSaved(); void databaseSaved();
void databaseUnlocked(); void databaseUnlocked();
void databaseLockRequested();
void databaseLocked(); void databaseLocked();
// Emitted in replaceDatabase, may be caused by lock, reload, unlock, load. // Emitted in replaceDatabase, may be caused by lock, reload, unlock, load.
@ -169,6 +169,8 @@ public slots:
void setupTotp(); void setupTotp();
void performAutoType(); void performAutoType();
void openUrl(); void openUrl();
void downloadSelectedFavicons();
void downloadAllFavicons();
void openUrlForEntry(Entry* entry); void openUrlForEntry(Entry* entry);
void createGroup(); void createGroup();
void deleteGroup(); void deleteGroup();
@ -233,6 +235,7 @@ private:
void setClipboardTextAndMinimize(const QString& text); void setClipboardTextAndMinimize(const QString& text);
void processAutoOpen(); void processAutoOpen();
bool confirmDeleteEntries(QList<Entry*> entries, bool permanent); bool confirmDeleteEntries(QList<Entry*> entries, bool permanent);
void performIconDownloads(const QList<Entry*>& entries, bool force = false);
QSharedPointer<Database> m_db; QSharedPointer<Database> m_db;

View file

@ -28,11 +28,8 @@
#include "core/Tools.h" #include "core/Tools.h"
#include "gui/IconModels.h" #include "gui/IconModels.h"
#include "gui/MessageBox.h" #include "gui/MessageBox.h"
#ifdef WITH_XC_NETWORKING #ifdef WITH_XC_NETWORKING
#include <QHostInfo> #include "core/IconDownloader.h"
#include <QNetworkAccessManager>
#include <QtNetwork>
#endif #endif
IconStruct::IconStruct() IconStruct::IconStruct()
@ -46,12 +43,11 @@ EditWidgetIcons::EditWidgetIcons(QWidget* parent)
, m_ui(new Ui::EditWidgetIcons()) , m_ui(new Ui::EditWidgetIcons())
, m_db(nullptr) , m_db(nullptr)
, m_applyIconTo(ApplyIconToOptions::THIS_ONLY) , m_applyIconTo(ApplyIconToOptions::THIS_ONLY)
#ifdef WITH_XC_NETWORKING
, m_netMgr(new QNetworkAccessManager(this))
, m_reply(nullptr)
#endif
, m_defaultIconModel(new DefaultIconModel(this)) , m_defaultIconModel(new DefaultIconModel(this))
, m_customIconModel(new CustomIconModel(this)) , m_customIconModel(new CustomIconModel(this))
#ifdef WITH_XC_NETWORKING
, m_downloader(new IconDownloader())
#endif
{ {
m_ui->setupUi(this); m_ui->setupUi(this);
@ -75,6 +71,11 @@ EditWidgetIcons::EditWidgetIcons(QWidget* parent)
this, SIGNAL(widgetUpdated())); this, SIGNAL(widgetUpdated()));
connect(m_ui->customIconsView->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), connect(m_ui->customIconsView->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
this, SIGNAL(widgetUpdated())); 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 // clang-format on
m_ui->faviconButton->setVisible(false); m_ui->faviconButton->setVisible(false);
@ -177,7 +178,7 @@ QMenu* EditWidgetIcons::createApplyIconToMenu()
void EditWidgetIcons::setUrl(const QString& url) void EditWidgetIcons::setUrl(const QString& url)
{ {
#ifdef WITH_XC_NETWORKING #ifdef WITH_XC_NETWORKING
m_url = QUrl(url); m_url = url;
m_ui->faviconButton->setVisible(!url.isEmpty()); m_ui->faviconButton->setVisible(!url.isEmpty());
#else #else
Q_UNUSED(url); Q_UNUSED(url);
@ -185,181 +186,54 @@ void EditWidgetIcons::setUrl(const QString& url)
#endif #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() void EditWidgetIcons::downloadFavicon()
{ {
#ifdef WITH_XC_NETWORKING #ifdef WITH_XC_NETWORKING
m_ui->faviconButton->setDisabled(true); if (!m_url.isEmpty()) {
m_downloader->setUrl(m_url);
m_redirects = 0; m_downloader->download();
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;
} }
}
// 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 #endif
} }
void EditWidgetIcons::fetchReadyRead() void EditWidgetIcons::iconReceived(const QString& url, const QImage& icon)
{ {
#ifdef WITH_XC_NETWORKING #ifdef WITH_XC_NETWORKING
m_bytesReceived += m_reply->readAll(); Q_UNUSED(url);
#endif 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"));
} }
emit messageEditEntry(message, MessageWidget::Error);
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; return;
} }
} else {
// No redirect, and we theoretically have some icon data now.
image.loadFromData(m_bytesReceived);
}
}
if (!image.isNull()) { if (!addCustomIcon(icon)) {
if (!addCustomIcon(image)) { emit messageEditEntry(tr("Existing icon selected."), MessageWidget::Information);
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()) { #else
m_redirects = 0; Q_UNUSED(url);
startFetchFavicon(m_urlsToTry.takeFirst()); Q_UNUSED(icon);
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);
#endif #endif
} }
void EditWidgetIcons::abortRequests() void EditWidgetIcons::abortRequests()
{ {
#ifdef WITH_XC_NETWORKING #ifdef WITH_XC_NETWORKING
if (m_reply) { if (m_downloader) {
m_reply->abort(); m_downloader->abortDownload();
} }
#endif #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() void EditWidgetIcons::addCustomIconFromFile()
{ {
if (m_db) { if (!m_db) {
return;
}
QString filter = QString("%1 (%2);;%3 (*)").arg(tr("Images"), Tools::imageReaderFilter(), tr("All files")); QString filter = QString("%1 (%2);;%3 (*)").arg(tr("Images"), Tools::imageReaderFilter(), tr("All files"));
auto filenames = QFileDialog::getOpenFileNames(this, tr("Select Image(s)"), "", filter); auto filenames = QFileDialog::getOpenFileNames(this, tr("Select Image(s)"), "", filter);
@ -404,7 +278,6 @@ void EditWidgetIcons::addCustomIconFromFile()
} }
} }
} }
}
bool EditWidgetIcons::addCustomIcon(const QImage& icon) bool EditWidgetIcons::addCustomIcon(const QImage& icon)
{ {

View file

@ -25,6 +25,7 @@
#include <QWidget> #include <QWidget>
#include "config-keepassx.h" #include "config-keepassx.h"
#include "core/Entry.h"
#include "core/Global.h" #include "core/Global.h"
#include "gui/MessageWidget.h" #include "gui/MessageWidget.h"
@ -32,8 +33,7 @@ class Database;
class DefaultIconModel; class DefaultIconModel;
class CustomIconModel; class CustomIconModel;
#ifdef WITH_XC_NETWORKING #ifdef WITH_XC_NETWORKING
class QNetworkAccessManager; class IconDownloader;
class QNetworkReply;
#endif #endif
namespace Ui namespace Ui
@ -87,9 +87,7 @@ signals:
private slots: private slots:
void downloadFavicon(); void downloadFavicon();
void startFetchFavicon(const QUrl& url); void iconReceived(const QString& url, const QImage& icon);
void fetchFinished();
void fetchReadyRead();
void addCustomIconFromFile(); void addCustomIconFromFile();
bool addCustomIcon(const QImage& icon); bool addCustomIcon(const QImage& icon);
void removeCustomIcon(); void removeCustomIcon();
@ -106,17 +104,12 @@ private:
QSharedPointer<Database> m_db; QSharedPointer<Database> m_db;
QUuid m_currentUuid; QUuid m_currentUuid;
ApplyIconToOptions m_applyIconTo; 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; DefaultIconModel* const m_defaultIconModel;
CustomIconModel* const m_customIconModel; CustomIconModel* const m_customIconModel;
#ifdef WITH_XC_NETWORKING
QScopedPointer<IconDownloader> m_downloader;
QString m_url;
#endif
Q_DISABLE_COPY(EditWidgetIcons) Q_DISABLE_COPY(EditWidgetIcons)
}; };

View file

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

View file

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

View file

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

View file

@ -246,6 +246,7 @@ MainWindow::MainWindow()
m_ui->actionEntryDelete->setShortcut(Qt::Key_Delete); m_ui->actionEntryDelete->setShortcut(Qt::Key_Delete);
m_ui->actionEntryClone->setShortcut(Qt::CTRL + Qt::Key_K); m_ui->actionEntryClone->setShortcut(Qt::CTRL + Qt::Key_K);
m_ui->actionEntryTotp->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_T); 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->actionEntryCopyTotp->setShortcut(Qt::CTRL + Qt::Key_T);
m_ui->actionEntryCopyUsername->setShortcut(Qt::CTRL + Qt::Key_B); m_ui->actionEntryCopyUsername->setShortcut(Qt::CTRL + Qt::Key_B);
m_ui->actionEntryCopyPassword->setShortcut(Qt::CTRL + Qt::Key_C); m_ui->actionEntryCopyPassword->setShortcut(Qt::CTRL + Qt::Key_C);
@ -261,6 +262,7 @@ MainWindow::MainWindow()
m_ui->actionEntryDelete->setShortcutVisibleInContextMenu(true); m_ui->actionEntryDelete->setShortcutVisibleInContextMenu(true);
m_ui->actionEntryClone->setShortcutVisibleInContextMenu(true); m_ui->actionEntryClone->setShortcutVisibleInContextMenu(true);
m_ui->actionEntryTotp->setShortcutVisibleInContextMenu(true); m_ui->actionEntryTotp->setShortcutVisibleInContextMenu(true);
m_ui->actionEntryDownloadIcon->setShortcutVisibleInContextMenu(true);
m_ui->actionEntryCopyTotp->setShortcutVisibleInContextMenu(true); m_ui->actionEntryCopyTotp->setShortcutVisibleInContextMenu(true);
m_ui->actionEntryCopyUsername->setShortcutVisibleInContextMenu(true); m_ui->actionEntryCopyUsername->setShortcutVisibleInContextMenu(true);
m_ui->actionEntryCopyPassword->setShortcutVisibleInContextMenu(true); m_ui->actionEntryCopyPassword->setShortcutVisibleInContextMenu(true);
@ -304,11 +306,13 @@ MainWindow::MainWindow()
m_ui->actionEntryCopyUsername->setIcon(filePath()->icon("actions", "username-copy")); m_ui->actionEntryCopyUsername->setIcon(filePath()->icon("actions", "username-copy"));
m_ui->actionEntryCopyPassword->setIcon(filePath()->icon("actions", "password-copy")); m_ui->actionEntryCopyPassword->setIcon(filePath()->icon("actions", "password-copy"));
m_ui->actionEntryCopyURL->setIcon(filePath()->icon("actions", "url-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->actionGroupNew->setIcon(filePath()->icon("actions", "group-new"));
m_ui->actionGroupEdit->setIcon(filePath()->icon("actions", "group-edit")); m_ui->actionGroupEdit->setIcon(filePath()->icon("actions", "group-edit"));
m_ui->actionGroupDelete->setIcon(filePath()->icon("actions", "group-delete")); m_ui->actionGroupDelete->setIcon(filePath()->icon("actions", "group-delete"));
m_ui->actionGroupEmptyRecycleBin->setIcon(filePath()->icon("actions", "group-empty-trash")); 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->actionSettings->setIcon(filePath()->icon("actions", "configure"));
m_ui->actionPasswordGenerator->setIcon(filePath()->icon("actions", "password-generator")); 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->actionEntryCopyNotes, SIGNAL(triggered()), SLOT(copyNotes()));
m_actionMultiplexer.connect(m_ui->actionEntryAutoType, SIGNAL(triggered()), SLOT(performAutoType())); 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->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->actionGroupNew, SIGNAL(triggered()), SLOT(createGroup()));
m_actionMultiplexer.connect(m_ui->actionGroupEdit, SIGNAL(triggered()), SLOT(switchToGroupEdit())); 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->actionGroupEmptyRecycleBin, SIGNAL(triggered()), SLOT(emptyRecycleBin()));
m_actionMultiplexer.connect(m_ui->actionGroupSortAsc, SIGNAL(triggered()), SLOT(sortGroupsAsc())); 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->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->actionSettings, SIGNAL(toggled(bool)), SLOT(switchToSettings(bool)));
connect(m_ui->actionPasswordGenerator, SIGNAL(toggled(bool)), SLOT(switchToPasswordGen(bool))); connect(m_ui->actionPasswordGenerator, SIGNAL(toggled(bool)), SLOT(switchToPasswordGen(bool)));
@ -419,6 +425,11 @@ MainWindow::MainWindow()
m_ui->actionCheckForUpdates->setVisible(false); m_ui->actionCheckForUpdates->setVisible(false);
#endif #endif
#ifndef WITH_XC_NETWORKING
m_ui->actionGroupDownloadFavicons->setVisible(false);
m_ui->actionEntryDownloadIcon->setVisible(false);
#endif
// clang-format off // clang-format off
connect(m_ui->tabWidget, connect(m_ui->tabWidget,
SIGNAL(messageGlobal(QString,MessageWidget::MessageType)), SIGNAL(messageGlobal(QString,MessageWidget::MessageType)),
@ -577,6 +588,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
bool entriesSelected = dbWidget->numberOfSelectedEntries() > 0 && hasFocus; bool entriesSelected = dbWidget->numberOfSelectedEntries() > 0 && hasFocus;
bool groupSelected = dbWidget->isGroupSelected(); bool groupSelected = dbWidget->isGroupSelected();
bool currentGroupHasChildren = dbWidget->currentGroup()->hasChildren(); bool currentGroupHasChildren = dbWidget->currentGroup()->hasChildren();
bool currentGroupHasEntries = !dbWidget->currentGroup()->entries().isEmpty();
bool recycleBinSelected = dbWidget->isRecycleBinSelected(); bool recycleBinSelected = dbWidget->isRecycleBinSelected();
m_ui->actionEntryNew->setEnabled(true); m_ui->actionEntryNew->setEnabled(true);
@ -596,6 +608,8 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
m_ui->actionEntryCopyTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); m_ui->actionEntryCopyTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp());
m_ui->actionEntrySetupTotp->setEnabled(singleEntrySelected); m_ui->actionEntrySetupTotp->setEnabled(singleEntrySelected);
m_ui->actionEntryTotpQRCode->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); m_ui->actionEntryTotpQRCode->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp());
m_ui->actionEntryDownloadIcon->setEnabled((entriesSelected && !singleEntrySelected)
|| (singleEntrySelected && dbWidget->currentEntryHasUrl()));
m_ui->actionGroupNew->setEnabled(groupSelected); m_ui->actionGroupNew->setEnabled(groupSelected);
m_ui->actionGroupEdit->setEnabled(groupSelected); m_ui->actionGroupEdit->setEnabled(groupSelected);
m_ui->actionGroupDelete->setEnabled(groupSelected && dbWidget->canDeleteCurrentGroup()); 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->actionGroupSortDesc->setEnabled(groupSelected && currentGroupHasChildren);
m_ui->actionGroupEmptyRecycleBin->setVisible(recycleBinSelected); m_ui->actionGroupEmptyRecycleBin->setVisible(recycleBinSelected);
m_ui->actionGroupEmptyRecycleBin->setEnabled(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->actionChangeMasterKey->setEnabled(true);
m_ui->actionChangeDatabaseSettings->setEnabled(true); m_ui->actionChangeDatabaseSettings->setEnabled(true);
m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave()); m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave());

View file

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

View file

@ -16,18 +16,21 @@
*/ */
#include "UpdateChecker.h" #include "UpdateChecker.h"
#include "config-keepassx.h" #include "config-keepassx.h"
#include "core/Clock.h" #include "core/Clock.h"
#include "core/Config.h" #include "core/Config.h"
#include "core/NetworkManager.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QNetworkAccessManager> #include <QRegularExpression>
#include <QtNetwork>
UpdateChecker* UpdateChecker::m_instance(nullptr); UpdateChecker* UpdateChecker::m_instance(nullptr);
UpdateChecker::UpdateChecker(QObject* parent) UpdateChecker::UpdateChecker(QObject* parent)
: QObject(parent) : QObject(parent)
, m_netMgr(new QNetworkAccessManager(this))
, m_reply(nullptr) , m_reply(nullptr)
, m_isManuallyRequested(false) , m_isManuallyRequested(false)
{ {
@ -56,7 +59,7 @@ void UpdateChecker::checkForUpdates(bool manuallyRequested)
QNetworkRequest request(apiUrl); QNetworkRequest request(apiUrl);
request.setRawHeader("Accept", "application/json"); 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, &QNetworkReply::finished, this, &UpdateChecker::fetchFinished);
connect(m_reply, &QIODevice::readyRead, this, &UpdateChecker::fetchReadyRead); connect(m_reply, &QIODevice::readyRead, this, &UpdateChecker::fetchReadyRead);

View file

@ -20,7 +20,6 @@
#include <QObject> #include <QObject>
#include <QString> #include <QString>
class QNetworkAccessManager;
class QNetworkReply; class QNetworkReply;
class UpdateChecker : public QObject class UpdateChecker : public QObject
@ -42,7 +41,6 @@ private slots:
void fetchReadyRead(); void fetchReadyRead();
private: private:
QNetworkAccessManager* m_netMgr;
QNetworkReply* m_reply; QNetworkReply* m_reply;
QByteArray m_bytesReceived; QByteArray m_bytesReceived;
bool m_isManuallyRequested; bool m_isManuallyRequested;