diff options
Diffstat (limited to 'src/core/IconDownloader.cpp')
-rw-r--r-- | src/core/IconDownloader.cpp | 204 |
1 files changed, 204 insertions, 0 deletions
diff --git a/src/core/IconDownloader.cpp b/src/core/IconDownloader.cpp new file mode 100644 index 000000000..36047ce2a --- /dev/null +++ b/src/core/IconDownloader.cpp @@ -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); + } +} |