diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f142f36800..48b606211d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -96,6 +96,8 @@ set(keepassx_SOURCES gui/EditWidgetProperties.cpp gui/FileDialog.cpp gui/Font.cpp + gui/IconDownloader.cpp + gui/IconDownloaderDialog.cpp gui/IconModels.cpp gui/KeePass1OpenWidget.cpp gui/KMessageWidget.cpp diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp index 2461230c86..958174ee27 100644 --- a/src/gui/ApplicationSettingsWidget.cpp +++ b/src/gui/ApplicationSettingsWidget.cpp @@ -84,6 +84,8 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent) connect(m_generalUi->toolbarHideCheckBox, SIGNAL(toggled(bool)), SLOT(toolbarSettingsToggled(bool))); connect(m_generalUi->rememberLastDatabasesCheckBox, SIGNAL(toggled(bool)), SLOT(rememberDatabasesToggled(bool))); + connect(m_generalUi->downloadFaviconCheckBox, SIGNAL(toggled(bool)), + m_generalUi->downloadFaviconSpinBox, SLOT(setEnabled(bool))); connect(m_secUi->clearClipboardCheckBox, SIGNAL(toggled(bool)), m_secUi->clearClipboardSpinBox, SLOT(setEnabled(bool))); connect(m_secUi->lockDatabaseIdleCheckBox, SIGNAL(toggled(bool)), @@ -152,6 +154,8 @@ 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->downloadFaviconCheckBox->setChecked(config()->get("DownloadFavicon").toBool()); + m_generalUi->downloadFaviconSpinBox->setValue(config()->get("DownloadFaviconTimeout").toInt()); m_generalUi->languageComboBox->clear(); QList> languages = Translator::availableLanguages(); @@ -252,6 +256,8 @@ 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("DownloadFavicon", m_generalUi->downloadFaviconCheckBox->isChecked()); + config()->set("DownloadFaviconTimeout", m_generalUi->downloadFaviconSpinBox->value()); config()->set("GUI/Language", m_generalUi->languageComboBox->itemData(currentLangIndex).toString()); diff --git a/src/gui/ApplicationSettingsWidgetGeneral.ui b/src/gui/ApplicationSettingsWidgetGeneral.ui index 9f03bbb509..aace35acba 100644 --- a/src/gui/ApplicationSettingsWidgetGeneral.ui +++ b/src/gui/ApplicationSettingsWidgetGeneral.ui @@ -525,6 +525,50 @@ + + + + Timeouts + + + + Qt::AlignLeft|Qt::AlignTop + + + + + Favicon download timeout + + + + + + + false + + + + 0 + 0 + + + + sec + + + 1 + + + 60 + + + 10 + + + + + + diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 0d200848d1..a97db8ba4c 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -61,6 +61,10 @@ #include "keeshare/KeeShare.h" #include "touchid/TouchID.h" +#ifdef WITH_XC_NETWORKING +#include "gui/IconDownloaderDialog.h" +#endif + #ifdef Q_OS_LINUX #include #endif @@ -89,6 +93,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent) , m_groupView(new GroupView(m_db.data(), m_mainSplitter)) , m_saveAttempts(0) , m_fileWatcher(new DelayingFileWatcher(this)) + , m_iconDownloaderState(IconDownloaderState::Idle) { m_messageWidget->setHidden(true); @@ -650,6 +655,55 @@ void DatabaseWidget::openUrl() } } +void DatabaseWidget::downloadSelectedFavicons() +{ +#ifdef WITH_XC_NETWORKING + const QModelIndexList selected = m_entryView->selectionModel()->selectedRows(); + if (selected.isEmpty()) { + return; + } + + QList selectedEntries; + for (const QModelIndex& index : selected) { + selectedEntries.append(m_entryView->entryFromIndex(index)); + } + + if (selectedEntries.isEmpty()) { + return; + } + + if (m_iconDownloaderState == IconDownloaderState::Downloading) { + return; + } + m_iconDownloaderState = IconDownloaderState::Downloading; + + auto* iconDownloader = new IconDownloaderDialog(this); + iconDownloader->downloadFavicons(m_db, selectedEntries); + m_iconDownloaderState = IconDownloaderState::Idle; +#endif +} + +void DatabaseWidget::downloadAllFavicons() +{ +#ifdef WITH_XC_NETWORKING + if (m_iconDownloaderState == IconDownloaderState::Downloading) { + return; + } + m_iconDownloaderState = IconDownloaderState::Downloading; + + auto* iconDownloader = new IconDownloaderDialog(this); + + Group* currentGroup = m_groupView->currentGroup(); + Q_ASSERT(currentGroup); + if (currentGroup) { + auto entries = currentGroup->entries(); + iconDownloader->downloadFavicons(m_db, entries); + } + + m_iconDownloaderState = IconDownloaderState::Idle; +#endif +} + void DatabaseWidget::openUrlForEntry(Entry* entry) { Q_ASSERT(entry); @@ -1489,6 +1543,16 @@ bool DatabaseWidget::currentEntryHasTotp() return currentEntry->hasTotp(); } +bool DatabaseWidget::currentEntryHasIconSet() +{ + Entry* currentEntry = m_entryView->currentEntry(); + Q_ASSERT(currentEntry); + if (!currentEntry) { + return false; + } + return !currentEntry->iconUuid().isNull(); +} + bool DatabaseWidget::currentEntryHasNotes() { Entry* currentEntry = m_entryView->currentEntry(); diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index fb9cf817e4..d65b5a445b 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -28,7 +28,6 @@ #include "gui/MessageWidget.h" #include "gui/csvImport/CsvImportWizard.h" #include "gui/entry/EntryModel.h" - #include "config-keepassx.h" class DatabaseOpenWidget; @@ -107,6 +106,7 @@ class DatabaseWidget : public QStackedWidget bool currentEntryHasUrl(); bool currentEntryHasNotes(); bool currentEntryHasTotp(); + bool currentEntryHasIconSet(); void blockAutoReload(bool block = true); @@ -164,6 +164,8 @@ public slots: void setupTotp(); void performAutoType(); void openUrl(); + void downloadSelectedFavicons(); + void downloadAllFavicons(); void openUrlForEntry(Entry* entry); void createGroup(); void deleteGroup(); @@ -221,6 +223,12 @@ private slots: void restoreGroupEntryFocus(const QUuid& groupUuid, const QUuid& EntryUuid); private: + enum IconDownloaderState + { + Idle, + Downloading + }; + int addChildWidget(QWidget* w); void setClipboardTextAndMinimize(const QString& text); void setIconFromParent(); @@ -263,6 +271,8 @@ private slots: // Autoreload QPointer m_fileWatcher; bool m_blockAutoSave; + + IconDownloaderState m_iconDownloaderState; }; #endif // KEEPASSX_DATABASEWIDGET_H diff --git a/src/gui/EditWidgetIcons.cpp b/src/gui/EditWidgetIcons.cpp index dcc5160a32..07f4dd1c95 100644 --- a/src/gui/EditWidgetIcons.cpp +++ b/src/gui/EditWidgetIcons.cpp @@ -26,15 +26,10 @@ #include "core/Group.h" #include "core/Metadata.h" #include "core/Tools.h" +#include "gui/IconDownloader.h" #include "gui/IconModels.h" #include "gui/MessageBox.h" -#ifdef WITH_XC_NETWORKING -#include -#include -#include -#endif - IconStruct::IconStruct() : uuid(QUuid()) , number(0) @@ -45,12 +40,11 @@ EditWidgetIcons::EditWidgetIcons(QWidget* parent) : QWidget(parent) , m_ui(new Ui::EditWidgetIcons()) , m_db(nullptr) -#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); @@ -155,178 +149,44 @@ void EditWidgetIcons::setUrl(const QString& url) #endif } -#ifdef WITH_XC_NETWORKING -namespace -{ - // Try to get the 2nd level domain of the host part of a QUrl. For example, - // "foo.bar.example.com" would become "example.com", and "foo.bar.example.co.uk" - // would become "example.co.uk". - QString getSecondLevelDomain(const QUrl& url) - { - QString fqdn = url.host(); - fqdn.truncate(fqdn.length() - url.topLevelDomain().length()); - QStringList parts = fqdn.split('.'); - QString newdom = parts.takeLast() + url.topLevelDomain(); - return newdom; - } - - QUrl convertVariantToUrl(const QVariant& var) - { - QUrl url; - if (var.canConvert()) - url = var.toUrl(); - return url; - } - - QUrl getRedirectTarget(QNetworkReply* reply) - { - QVariant var = reply->attribute(QNetworkRequest::RedirectionTargetAttribute); - QUrl url = convertVariantToUrl(var); - return url; - } -} // namespace -#endif - void EditWidgetIcons::downloadFavicon() { #ifdef WITH_XC_NETWORKING - m_ui->faviconButton->setDisabled(true); - - m_redirects = 0; - m_urlsToTry.clear(); - - QString fullyQualifiedDomain = m_url.host(); - - // Determine if host portion of URL is an IP address by resolving it and - // searching for a match with the returned address(es). - bool hostIsIp = false; - QList hostAddressess = QHostInfo::fromName(fullyQualifiedDomain).addresses(); - for (auto addr : hostAddressess) { - if (addr.toString() == fullyQualifiedDomain) { - hostIsIp = true; - } - } - - // Determine the second-level domain, if available - QString secondLevelDomain; - if (!hostIsIp) { - secondLevelDomain = getSecondLevelDomain(m_url); - } - - // Start with the "fallback" url (if enabled) to try to get the best favicon - if (config()->get("security/IconDownloadFallback", false).toBool()) { - QUrl fallbackUrl = QUrl("https://icons.duckduckgo.com"); - fallbackUrl.setPath("/ip3/" + QUrl::toPercentEncoding(fullyQualifiedDomain) + ".ico"); - m_urlsToTry.append(fallbackUrl); - - // Also try a direct pull of the second-level domain (if possible) - if (!hostIsIp && fullyQualifiedDomain != secondLevelDomain) { - fallbackUrl.setPath("/ip3/" + QUrl::toPercentEncoding(secondLevelDomain) + ".ico"); - m_urlsToTry.append(fallbackUrl); - } - } - - // Add a direct pull of the website's own favicon.ico file - m_urlsToTry.append(QUrl(m_url.scheme() + "://" + fullyQualifiedDomain + "/favicon.ico")); - - // Also try a direct pull of the second-level domain (if possible) - if (!hostIsIp && fullyQualifiedDomain != secondLevelDomain) { - m_urlsToTry.append(QUrl(m_url.scheme() + "://" + secondLevelDomain + "/favicon.ico")); + connect(m_downloader.data(), SIGNAL(iconReceived(const QImage&, Entry*)), this, SLOT(iconReceived(const QImage&, Entry*))); + Entry* entry = m_db->rootGroup()->findEntryByUuid(m_currentUuid); + if (entry) { + m_downloader->downloadFavicon(entry); } - - // 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() -{ -#ifdef WITH_XC_NETWORKING - m_bytesReceived += m_reply->readAll(); #endif } -void EditWidgetIcons::fetchFinished() +void EditWidgetIcons::iconReceived(const QImage& icon, Entry* entry) { #ifdef WITH_XC_NETWORKING - QImage image; - bool fallbackEnabled = config()->get("security/IconDownloadFallback", false).toBool(); - bool error = (m_reply->error() != QNetworkReply::NoError); - QUrl redirectTarget = getRedirectTarget(m_reply); - - m_reply->deleteLater(); - m_reply = nullptr; - - if (!error) { - if (redirectTarget.isValid()) { - // Redirected, we need to follow it, or fall through if we have - // done too many redirects already. - if (m_redirects < 5) { - m_redirects++; - if (redirectTarget.isRelative()) - redirectTarget = m_fetchUrl.resolved(redirectTarget); - startFetchFavicon(redirectTarget); - return; - } - } else { - // No redirect, and we theoretically have some icon data now. - image.loadFromData(m_bytesReceived); - } - } - - if (!image.isNull()) { - if (!addCustomIcon(image)) { - emit messageEditEntry(tr("Custom icon already exists"), MessageWidget::Information); - } else if (!isVisible()) { - // Show confirmation message if triggered from Entry tab download button - emit messageEditEntry(tr("Custom icon successfully downloaded"), MessageWidget::Positive); - } - } else if (!m_urlsToTry.empty()) { - m_redirects = 0; - startFetchFavicon(m_urlsToTry.takeFirst()); + Q_UNUSED(entry); + if (icon.isNull()) { + emit messageEditEntry(tr("Unable to fetch favicon."), 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("Custom icon already exists"), MessageWidget::Information); + } +#else + Q_UNUSED(icon); + Q_UNUSED(entry); #endif } void EditWidgetIcons::abortRequests() { #ifdef WITH_XC_NETWORKING - if (m_reply) { - m_reply->abort(); + if (m_downloader) { + m_downloader->abortRequest(); } #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) { diff --git a/src/gui/EditWidgetIcons.h b/src/gui/EditWidgetIcons.h index dcff02f565..285bb6c63f 100644 --- a/src/gui/EditWidgetIcons.h +++ b/src/gui/EditWidgetIcons.h @@ -25,15 +25,15 @@ #include "config-keepassx.h" #include "core/Global.h" +#include "core/Entry.h" #include "gui/MessageWidget.h" +#ifdef WITH_XC_NETWORKING +#include "gui/IconDownloader.h" +#endif class Database; class DefaultIconModel; class CustomIconModel; -#ifdef WITH_XC_NETWORKING -class QNetworkAccessManager; -class QNetworkReply; -#endif namespace Ui { @@ -74,9 +74,7 @@ public slots: private slots: void downloadFavicon(); - void startFetchFavicon(const QUrl& url); - void fetchFinished(); - void fetchReadyRead(); + void iconReceived(const QImage& icon, Entry* entry); void addCustomIconFromFile(); bool addCustomIcon(const QImage& icon); void removeCustomIcon(); @@ -89,17 +87,12 @@ private slots: const QScopedPointer m_ui; QSharedPointer m_db; QUuid m_currentUuid; + DefaultIconModel* const m_defaultIconModel; + CustomIconModel* const m_customIconModel; #ifdef WITH_XC_NETWORKING + QScopedPointer m_downloader; QUrl m_url; - QUrl m_fetchUrl; - QList m_urlsToTry; - QByteArray m_bytesReceived; - QNetworkAccessManager* m_netMgr; - QNetworkReply* m_reply; - int m_redirects; #endif - DefaultIconModel* const m_defaultIconModel; - CustomIconModel* const m_customIconModel; Q_DISABLE_COPY(EditWidgetIcons) }; diff --git a/src/gui/IconDownloader.cpp b/src/gui/IconDownloader.cpp new file mode 100644 index 0000000000..818c7c626f --- /dev/null +++ b/src/gui/IconDownloader.cpp @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "IconDownloader.h" +#include "core/Config.h" + +#ifdef WITH_XC_NETWORKING +#include +#include +#include +#endif + +IconDownloader::IconDownloader(QObject* parent) + : QObject(parent) +#ifdef WITH_XC_NETWORKING + , m_netMgr(new QNetworkAccessManager(this)) + , m_reply(nullptr) +#endif +{ +} + +IconDownloader::~IconDownloader() +{ +} + +#ifdef WITH_XC_NETWORKING +namespace +{ + // Try to get the 2nd level domain of the host part of a QUrl. For example, + // "foo.bar.example.com" would become "example.com", and "foo.bar.example.co.uk" + // would become "example.co.uk". + QString getSecondLevelDomain(const QUrl& url) + { + QString fqdn = url.host(); + fqdn.truncate(fqdn.length() - url.topLevelDomain().length()); + QStringList parts = fqdn.split('.'); + QString newdom = parts.takeLast() + url.topLevelDomain(); + return newdom; + } + + QUrl convertVariantToUrl(const QVariant& var) + { + QUrl url; + if (var.canConvert()) + url = var.toUrl(); + return url; + } + + QUrl getRedirectTarget(QNetworkReply* reply) + { + QVariant var = reply->attribute(QNetworkRequest::RedirectionTargetAttribute); + QUrl url = convertVariantToUrl(var); + return url; + } +} // namespace +#endif + +void IconDownloader::downloadFavicon(Entry* entry) +{ +#ifdef WITH_XC_NETWORKING + m_entry = entry; + m_url = entry->url(); + m_redirects = 0; + m_urlsToTry.clear(); + + if (m_url.scheme().isEmpty()) { + m_url.setUrl(QString("https://%1").arg(m_url.toString())); + } + + QString fullyQualifiedDomain = m_url.host(); + + // Determine if host portion of URL is an IP address by resolving it and + // searching for a match with the returned address(es). + bool hostIsIp = false; + QList hostAddressess = QHostInfo::fromName(fullyQualifiedDomain).addresses(); + for (auto addr : hostAddressess) { + if (addr.toString() == fullyQualifiedDomain) { + hostIsIp = true; + } + } + + // Determine the second-level domain, if available + QString secondLevelDomain; + if (!hostIsIp) { + secondLevelDomain = getSecondLevelDomain(m_url); + } + + // Start with the "fallback" url (if enabled) to try to get the best favicon + if (config()->get("security/IconDownloadFallback", false).toBool()) { + QUrl fallbackUrl = QUrl("https://icons.duckduckgo.com"); + fallbackUrl.setPath("/ip3/" + QUrl::toPercentEncoding(fullyQualifiedDomain) + ".ico"); + m_urlsToTry.append(fallbackUrl); + + // Also try a direct pull of the second-level domain (if possible) + if (!hostIsIp && fullyQualifiedDomain != secondLevelDomain) { + fallbackUrl.setPath("/ip3/" + QUrl::toPercentEncoding(secondLevelDomain) + ".ico"); + m_urlsToTry.append(fallbackUrl); + } + } + + // Add a direct pull of the website's own favicon.ico file + m_urlsToTry.append(QUrl(m_url.scheme() + "://" + fullyQualifiedDomain + "/favicon.ico")); + + // Also try a direct pull of the second-level domain (if possible) + if (!hostIsIp && fullyQualifiedDomain != secondLevelDomain) { + m_urlsToTry.append(QUrl(m_url.scheme() + "://" + secondLevelDomain + "/favicon.ico")); + } + + // 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()); +#else + Q_UNUSED(entry); +#endif +} + +void IconDownloader::fetchReadyRead() +{ +#ifdef WITH_XC_NETWORKING + m_bytesReceived += m_reply->readAll(); +#endif +} + +void IconDownloader::fetchFinished() +{ +#ifdef WITH_XC_NETWORKING + QImage image; + bool fallbackEnabled = config()->get("security/IconDownloadFallback", false).toBool(); + bool error = (m_reply->error() != QNetworkReply::NoError); + if (m_reply->error() == QNetworkReply::HostNotFoundError || m_reply->error() == QNetworkReply::TimeoutError) { + emit iconReceived(image, m_entry); + return; + } + QUrl redirectTarget = getRedirectTarget(m_reply); + + m_reply->deleteLater(); + m_reply = nullptr; + + if (!error) { + if (redirectTarget.isValid()) { + // Redirected, we need to follow it, or fall through if we have + // done too many redirects already. + if (m_redirects < 5) { + m_redirects++; + if (redirectTarget.isRelative()) + redirectTarget = m_fetchUrl.resolved(redirectTarget); + startFetchFavicon(redirectTarget); + return; + } + } else { + // No redirect, and we theoretically have some icon data now. + image.loadFromData(m_bytesReceived); + } + } + + if (!image.isNull()) { + emit iconReceived(image, m_entry); + return; + } else if (!m_urlsToTry.empty()) { + m_redirects = 0; + startFetchFavicon(m_urlsToTry.takeFirst()); + return; + } else { + if (!fallbackEnabled) { + emit fallbackNotEnabled(); + } else { + emit iconError(m_entry); + } + } + emit iconReceived(image, m_entry); +#endif +} + +void IconDownloader::abortRequest() +{ +#ifdef WITH_XC_NETWORKING + if (m_reply) { + m_reply->abort(); + } +#endif +} + +void IconDownloader::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, &IconDownloader::fetchFinished); + connect(m_reply, &QIODevice::readyRead, this, &IconDownloader::fetchReadyRead); +#else + Q_UNUSED(url); +#endif +} diff --git a/src/gui/IconDownloader.h b/src/gui/IconDownloader.h new file mode 100644 index 0000000000..6b071a5f74 --- /dev/null +++ b/src/gui/IconDownloader.h @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSX_ICONDOWNLOADER_H +#define KEEPASSX_ICONDOWNLOADER_H + +#include +#include +#include + +#include "config-keepassx.h" +#include "core/Entry.h" +#include "core/Global.h" + +#ifdef WITH_XC_NETWORKING +class QNetworkAccessManager; +class QNetworkReply; +#endif + +class IconDownloader : public QObject +{ + Q_OBJECT + +public: + explicit IconDownloader(QObject* parent = nullptr); + ~IconDownloader(); + + void downloadFavicon(Entry* entry); + +public slots: + void startFetchFavicon(const QUrl& url); + void abortRequest(); + +signals: + void iconReceived(const QImage&, Entry*); + void iconError(Entry*); + void fallbackNotEnabled(); + +private slots: + void fetchFinished(); + void fetchReadyRead(); + +private: +#ifdef WITH_XC_NETWORKING + QUrl m_url; + Entry* m_entry; + QUrl m_fetchUrl; + QList m_urlsToTry; + QByteArray m_bytesReceived; + QNetworkAccessManager* m_netMgr; + QNetworkReply* m_reply; + int m_redirects; +#endif +}; + +#endif // KEEPASSX_ICONDOWNLOADER_H \ No newline at end of file diff --git a/src/gui/IconDownloaderDialog.cpp b/src/gui/IconDownloaderDialog.cpp new file mode 100644 index 0000000000..2b84a52f8d --- /dev/null +++ b/src/gui/IconDownloaderDialog.cpp @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "IconDownloaderDialog.h" + +#include +#include +#include + +#include "core/AsyncTask.h" +#include "core/Config.h" +#include "core/Group.h" +#include "core/Metadata.h" +#include "core/Tools.h" +#include "gui/IconModels.h" +#include "ui_IconDownloaderDialog.h" + +#ifdef WITH_XC_NETWORKING +#include +#include +#include +#endif + +#ifdef Q_OS_MACOS +#include "gui/macutils/MacUtils.h" +#endif + +IconDownloaderDialog::IconDownloaderDialog(QWidget* parent) + : QDialog(parent) + , m_ui(new Ui::IconDownloaderDialog()) + , m_dataModel(new QStandardItemModel(this)) + , m_db(nullptr) + , m_customIconModel(new CustomIconModel(this)) + , m_parent(parent) +{ + m_ui->setupUi(this); + setWindowFlags(Qt::Window); + setAttribute(Qt::WA_DeleteOnClose); + m_ui->tableView->setModel(m_dataModel); + m_ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + m_ui->progressBar->setMinimumHeight(20); + m_dataModel->clear(); + m_dataModel->setHorizontalHeaderLabels({"URL", "Status"}); + + connect(this, SIGNAL(entryUpdated()), parent, SIGNAL(databaseModified())); +} + +IconDownloaderDialog::~IconDownloaderDialog() +{ + m_futureList.waitForFinished(); +} + +void IconDownloaderDialog::raiseWindow() +{ +#ifdef Q_OS_MACOS + macUtils()->raiseOwnWindow(); + Tools::wait(500); +#endif + show(); + activateWindow(); + raise(); +} + +void IconDownloaderDialog::downloadFavicon(const QSharedPointer& database, Entry* entry) +{ + m_db = database; +#ifdef WITH_XC_NETWORKING + QScopedPointer downloader(new IconDownloader()); + connect(downloader.data(), SIGNAL(iconError(Entry*)), this, SLOT(iconError(Entry*))); + connect(downloader.data(), SIGNAL(fallbackNotEnabled()), this, SLOT(fallbackNotEnabled())); + connect(downloader.data(), SIGNAL(iconReceived(const QImage&, Entry*)), + this, SLOT(iconReceived(const QImage&, Entry*))); + connect(m_ui->abortButton, SIGNAL(clicked()), downloader.data(), SLOT(abortRequest())); + connect(m_parent, SIGNAL(databaseLocked()), downloader.data(), SLOT(abortRequest())); + connect(m_parent, SIGNAL(databaseLocked()), this, SLOT(close())); + + m_mutex.lock(); + m_dataModel->appendRow(QList() << new QStandardItem(entry->url()) + << new QStandardItem(tr("Loading"))); + m_mutex.unlock(); + + QTimer timer; + connect(&timer, SIGNAL(timeout()), downloader.data(), SLOT(abortRequest())); + if (config()->get("DownloadFavicon", false).toBool()) { + timer.start(config()->get("DownloadFaviconTimeout").toInt() * 1000); + } + + downloader->downloadFavicon(entry); + + QEventLoop loop; + connect(downloader.data(), SIGNAL(iconReceived(const QImage&, Entry*)), &loop, SLOT(quit())); + loop.exec(); + timer.stop(); +#endif +} + +void IconDownloaderDialog::downloadFavicons(const QSharedPointer& database, const QList& entries) +{ + if (entries.count() == 1) { + connect(this, SIGNAL(messageEditEntry(QString, MessageWidget::MessageType)), + m_parent, SLOT(showMessage(QString, MessageWidget::MessageType))); + downloadFavicon(database, entries.first()); + return; + } + + raiseWindow(); + auto result = AsyncTask::runAndWaitForFuture( + [&]() { return downloadAllFavicons(database, entries); }); + Q_UNUSED(result); +} + +bool IconDownloaderDialog::downloadAllFavicons(const QSharedPointer& database, const QList& entries) +{ + m_db = database; + + // Set progress bar + int maximum = 0; + for (const auto& e : entries) { + if (!e->url().isEmpty() && e->iconUuid().isNull()) { + ++maximum; + } + } + m_ui->progressBar->setMinimum(0); + m_ui->progressBar->setMaximum(maximum); + + for (const auto& e : entries) { + if (!e->url().isEmpty() && e->iconUuid().isNull()) { + QFuture fut = QtConcurrent::run(this, &IconDownloaderDialog::downloadFavicon, database, e); + m_futureList.addFuture(fut); + } + } + + m_futureList.waitForFinished(); + m_ui->abortButton->setEnabled(false); + return true; +} + +void IconDownloaderDialog::iconReceived(const QImage& icon, Entry* entry) +{ + m_mutex.lock(); + m_ui->progressBar->setValue(m_ui->progressBar->value() + 1); + m_ui->label->setText(tr("Downloading favicon %1 of %n…", 0, + m_ui->progressBar->maximum()).arg(m_ui->progressBar->value())); + m_mutex.unlock(); + + if (icon.isNull() || !entry) { + updateTable(entry, tr("Error")); + emit messageEditEntry(tr("Unable to fetch favicon."), MessageWidget::Error); + return; + } + + if (!addCustomIcon(icon, entry)) { + updateTable(entry, tr("Custom icon already exists")); + return; + } + + updateTable(entry, tr("Ok")); +} + +bool IconDownloaderDialog::addCustomIcon(const QImage& icon, Entry* entry) +{ + bool added = false; + 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); + m_customIconModel->setIcons(m_db->metadata()->customIconsScaledPixmaps(), + m_db->metadata()->customIconsOrder()); + added = true; + } else { + emit messageEditEntry(tr("Custom icon already exists"), MessageWidget::Information); + } + + if (entry) { + entry->setIcon(uuid); + } + + emit entryUpdated(); + } + + return added; +} + +void IconDownloaderDialog::fallbackNotEnabled() +{ +#ifdef Q_OS_MACOS + const QString settingsPath = tr("Preferences -> Security"); +#else + const QString settingsPath = tr("Tools -> Settings -> Security"); +#endif + + emit messageEditEntry( + tr("Unable to fetch favicon.") + "\n" + + tr("You can enable the DuckDuckGo website icon service under %1").arg(settingsPath), + MessageWidget::Error); +} + +void IconDownloaderDialog::iconError(Entry* entry) +{ + updateTable(entry, tr("Unable to fetch favicon.")); + emit messageEditEntry(tr("Unable to fetch favicon."), MessageWidget::Error); +} + +void IconDownloaderDialog::updateTable(Entry* entry, const QString& message) +{ + if (!entry) { + return; + } + + QMutexLocker locker(&m_mutex); + for (int i = 0; i < m_dataModel->rowCount(); ++i) { + if (m_dataModel->item(i, 0)->text() == entry->url()) { + m_dataModel->item(i, 1)->setText(message); + } + } +} + +void IconDownloaderDialog::closeEvent(QCloseEvent* event) +{ + emit m_ui->abortButton->clicked(); + m_ui->abortButton->setEnabled(false); + m_futureList.waitForFinished(); + event->accept(); +} diff --git a/src/gui/IconDownloaderDialog.h b/src/gui/IconDownloaderDialog.h new file mode 100644 index 0000000000..c7300317e7 --- /dev/null +++ b/src/gui/IconDownloaderDialog.h @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSX_ICONDOWNLOADERDIALOG_H +#define KEEPASSX_ICONDOWNLOADERDIALOG_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config-keepassx.h" +#include "core/Entry.h" +#include "core/Global.h" +#include "gui/MessageWidget.h" +#ifdef WITH_XC_NETWORKING +#include "gui/IconDownloader.h" +#endif + +class Database; +class CustomIconModel; + +namespace Ui +{ + class IconDownloaderDialog; +} + +class IconDownloaderDialog : public QDialog +{ + Q_OBJECT + +public: + explicit IconDownloaderDialog(QWidget* parent = nullptr); + ~IconDownloaderDialog() override; + + void raiseWindow(); + void downloadFavicon(const QSharedPointer& database, Entry* entry); + void downloadFavicons(const QSharedPointer& database, const QList& entries); + +signals: + void messageEditEntry(QString, MessageWidget::MessageType); + void entryUpdated(); + +private slots: + void iconReceived(const QImage& icon, Entry* entry); + bool addCustomIcon(const QImage& icon, Entry* entry); + void fallbackNotEnabled(); + void iconError(Entry* entry); + +protected: + bool downloadAllFavicons(const QSharedPointer& database, const QList& entries); + void updateTable(Entry* entry, const QString& message); + void closeEvent(QCloseEvent* event) override; + +private: + QScopedPointer m_ui; + QStandardItemModel* m_dataModel; + QSharedPointer m_db; + QUuid m_currentUuid; + CustomIconModel* const m_customIconModel; + QWidget* m_parent; + QFutureSynchronizer m_futureList; + QMutex m_mutex; + + Q_DISABLE_COPY(IconDownloaderDialog) +}; + +#endif // KEEPASSX_ICONDOWNLOADERDIALOG_H diff --git a/src/gui/IconDownloaderDialog.ui b/src/gui/IconDownloaderDialog.ui new file mode 100644 index 0000000000..eb1ece20a8 --- /dev/null +++ b/src/gui/IconDownloaderDialog.ui @@ -0,0 +1,107 @@ + + + IconDownloaderDialog + + + + 0 + 0 + 641 + 452 + + + + + 0 + 0 + + + + Download all favicons + + + + + + + + + + Downloading favicon 0/0... + + + + + + + QLayout::SetDefaultConstraint + + + + + 0 + + + + + + + + 0 + 0 + + + + + 100 + 16777215 + + + + Abort + + + + + + + + + Qt::ScrollBarAsNeeded + + + QAbstractScrollArea::AdjustToContents + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + QAbstractItemView::SelectColumns + + + 500 + + + 20 + + + true + + + false + + + + + + + + + + + + + diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index e5f5ea6132..f0799ab094 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -230,6 +230,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); @@ -245,6 +246,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); @@ -353,11 +355,13 @@ 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())); m_actionMultiplexer.connect(m_ui->actionGroupDelete, SIGNAL(triggered()), SLOT(deleteGroup())); m_actionMultiplexer.connect(m_ui->actionGroupEmptyRecycleBin, SIGNAL(triggered()), SLOT(emptyRecycleBin())); + m_actionMultiplexer.connect(m_ui->actionDownloadAllFavicons, SIGNAL(triggered()), SLOT(downloadAllFavicons())); connect(m_ui->actionSettings, SIGNAL(toggled(bool)), SLOT(switchToSettings(bool))); connect(m_ui->actionPasswordGenerator, SIGNAL(toggled(bool)), SLOT(switchToPasswordGen(bool))); @@ -565,11 +569,14 @@ 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->currentEntryHasIconSet() && dbWidget->currentEntryHasUrl())); m_ui->actionGroupNew->setEnabled(groupSelected); m_ui->actionGroupEdit->setEnabled(groupSelected); m_ui->actionGroupDelete->setEnabled(groupSelected && dbWidget->canDeleteCurrentGroup()); m_ui->actionGroupEmptyRecycleBin->setVisible(recycleBinSelected); m_ui->actionGroupEmptyRecycleBin->setEnabled(recycleBinSelected); + m_ui->actionDownloadAllFavicons->setEnabled(groupSelected); m_ui->actionChangeMasterKey->setEnabled(true); m_ui->actionChangeDatabaseSettings->setEnabled(true); m_ui->actionDatabaseSave->setEnabled(m_ui->tabWidget->canSave()); diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index 004518eec9..63a13b6fad 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -270,6 +270,8 @@ + + @@ -280,6 +282,8 @@ + + @@ -451,6 +455,14 @@ &Delete group + + + false + + + Downlo&ad all favicons + + false @@ -538,6 +550,11 @@ Perform &Auto-Type + + + Download favicon + + false