diff --git a/autotests/gravatarcachetest.cpp b/autotests/gravatarcachetest.cpp index d2a4239..309eef1 100644 --- a/autotests/gravatarcachetest.cpp +++ b/autotests/gravatarcachetest.cpp @@ -1,99 +1,143 @@ /* Copyright (c) 2015-2017 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "gravatarcachetest.h" #include "../src/misc/gravatarcache.h" +#include "../src/misc/hash.h" +#include #include #include #include using namespace Gravatar; +Q_DECLARE_METATYPE(Gravatar::Hash) + GravatarCacheTest::GravatarCacheTest(QObject *parent) : QObject(parent) { } GravatarCacheTest::~GravatarCacheTest() { } void GravatarCacheTest::initTestCase() { QStandardPaths::setTestModeEnabled(true); } void GravatarCacheTest::shouldHaveDefaultValue() { Gravatar::GravatarCache gravatarCache; QCOMPARE(gravatarCache.maximumSize(), 20); } void GravatarCacheTest::shouldChangeCacheValue() { Gravatar::GravatarCache gravatarCache; int val = 30; gravatarCache.setMaximumSize(val); QCOMPARE(gravatarCache.maximumSize(), val); val = 50; gravatarCache.setMaximumSize(val); QCOMPARE(gravatarCache.maximumSize(), val); } void GravatarCacheTest::testLookup() { + Hash hash(QCryptographicHash::hash(QByteArray("test@example.com"), QCryptographicHash::Md5), Hash::Md5); { GravatarCache cache; cache.clearAllCache(); bool found = false; - const auto result = cache.loadGravatarPixmap(QStringLiteral("fa1afe1"), found); + const auto result = cache.loadGravatarPixmap(hash, found); QVERIFY(!found); QVERIFY(result.isNull()); } QPixmap px(42, 42); px.fill(Qt::blue); { GravatarCache cache; - cache.saveGravatarPixmap(QStringLiteral("fa1afe1"), px); + cache.saveGravatarPixmap(hash, px); // in-memory cache lookup bool found = false; - const auto result = cache.loadGravatarPixmap(QStringLiteral("fa1afe1"), found); + const auto result = cache.loadGravatarPixmap(hash, found); QVERIFY(found); QVERIFY(!result.isNull()); QCOMPARE(result.size(), QSize(42, 42)); } { // disk lookup GravatarCache cache; bool found = false; - const auto result = cache.loadGravatarPixmap(QStringLiteral("fa1afe1"), found); + const auto result = cache.loadGravatarPixmap(hash, found); QVERIFY(found); QVERIFY(!result.isNull()); QCOMPARE(result.size(), QSize(42, 42)); } } +void GravatarCacheTest::testMissing_data() +{ + QTest::addColumn("hash"); + QTest::newRow("md5") << Hash(QCryptographicHash::hash(QByteArray("testMD5@example.com"), QCryptographicHash::Md5), Hash::Md5); + QTest::newRow("Sha256") << Hash(QCryptographicHash::hash(QByteArray("testSHA256@example.com"), QCryptographicHash::Sha256), Hash::Sha256); +} + +void GravatarCacheTest::testMissing() +{ + QFETCH(Hash, hash); + { + GravatarCache cache; + cache.clearAllCache(); + bool found = false; + const auto result = cache.loadGravatarPixmap(hash, found); + QVERIFY(!found); + QVERIFY(result.isNull()); + } + + { + // store miss and verify in memory + GravatarCache cache; + cache.saveMissingGravatar(hash); + bool found = false; + const auto result = cache.loadGravatarPixmap(hash, found); + QVERIFY(found); + QVERIFY(result.isNull()); + } + + { + // verify miss in disk storage + GravatarCache cache; + bool found = false; + const auto result = cache.loadGravatarPixmap(hash, found); + QVERIFY(found); + QVERIFY(result.isNull()); + } +} + QTEST_MAIN(GravatarCacheTest) diff --git a/autotests/gravatarcachetest.h b/autotests/gravatarcachetest.h index 27581cb..c06bc64 100644 --- a/autotests/gravatarcachetest.h +++ b/autotests/gravatarcachetest.h @@ -1,38 +1,40 @@ /* Copyright (c) 2015-2017 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef GRAVATARCACHETEST_H #define GRAVATARCACHETEST_H #include class GravatarCacheTest : public QObject { Q_OBJECT public: explicit GravatarCacheTest(QObject *parent = nullptr); ~GravatarCacheTest(); private Q_SLOTS: void initTestCase(); void shouldHaveDefaultValue(); void shouldChangeCacheValue(); void testLookup(); + void testMissing(); + void testMissing_data(); }; #endif // GRAVATARCACHETEST_H diff --git a/autotests/gravatarresolvurljobtest.cpp b/autotests/gravatarresolvurljobtest.cpp index 89edf41..a3f1d0f 100644 --- a/autotests/gravatarresolvurljobtest.cpp +++ b/autotests/gravatarresolvurljobtest.cpp @@ -1,174 +1,175 @@ /* Copyright (c) 2015-2017 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "gravatarresolvurljobtest.h" #include "../src/job/gravatarresolvurljob.h" +#include "../src/misc/hash.h" #include GravatarResolvUrlJobTest::GravatarResolvUrlJobTest(QObject *parent) : QObject(parent) { } GravatarResolvUrlJobTest::~GravatarResolvUrlJobTest() { } void GravatarResolvUrlJobTest::shouldHaveDefaultValue() { Gravatar::GravatarResolvUrlJob job; QVERIFY(job.email().isEmpty()); QCOMPARE(job.size(), 80); QCOMPARE(job.hasGravatar(), false); QCOMPARE(job.pixmap().isNull(), true); QCOMPARE(job.useDefaultPixmap(), false); QCOMPARE(job.useLibravatar(), false); QCOMPARE(job.fallbackGravatar(), true); } void GravatarResolvUrlJobTest::shouldChangeValue() { Gravatar::GravatarResolvUrlJob job; bool useDefaultPixmap = true; job.setUseDefaultPixmap(useDefaultPixmap); QCOMPARE(job.useDefaultPixmap(), useDefaultPixmap); useDefaultPixmap = false; job.setUseDefaultPixmap(useDefaultPixmap); QCOMPARE(job.useDefaultPixmap(), useDefaultPixmap); bool useLibravatar = true; job.setUseLibravatar(useLibravatar); QCOMPARE(job.useLibravatar(), useLibravatar); useLibravatar = false; job.setUseLibravatar(useLibravatar); QCOMPARE(job.useLibravatar(), useLibravatar); bool fallBackGravatar = false; job.setFallbackGravatar(fallBackGravatar); QCOMPARE(job.fallbackGravatar(), fallBackGravatar); fallBackGravatar = true; job.setFallbackGravatar(fallBackGravatar); QCOMPARE(job.fallbackGravatar(), fallBackGravatar); } void GravatarResolvUrlJobTest::shouldChangeSize() { Gravatar::GravatarResolvUrlJob job; int size = 50; job.setSize(size); QCOMPARE(job.size(), size); size = 0; job.setSize(size); QCOMPARE(job.size(), 80); size = 10; job.setSize(size); QCOMPARE(job.size(), size); size = 2048; job.setSize(size); QCOMPARE(job.size(), size); size = 4096; job.setSize(size); QCOMPARE(job.size(), 2048); } void GravatarResolvUrlJobTest::shouldAddSizeInUrl() { Gravatar::GravatarResolvUrlJob job; job.setEmail(QStringLiteral("foo@kde.org")); job.setSize(1024); job.setUseLibravatar(false); QUrl url = job.generateGravatarUrl(job.useLibravatar()); QCOMPARE(url, QUrl(QStringLiteral("https://secure.gravatar.com:443/avatar/89b4e14cf2fc6d426275c019c6dc9de6?d=404&s=1024"))); job.setUseLibravatar(true); url = job.generateGravatarUrl(job.useLibravatar()); QCOMPARE(url, QUrl(QStringLiteral("https://seccdn.libravatar.org:443/avatar/2726400c3a33ce56c0ff632cbc0474f766d3b36e68819c601fb02954c1681d85?d=404&s=1024"))); } void GravatarResolvUrlJobTest::shouldUseDefaultPixmap() { Gravatar::GravatarResolvUrlJob job; job.setEmail(QStringLiteral("foo@kde.org")); job.setSize(1024); job.setUseDefaultPixmap(true); QUrl url = job.generateGravatarUrl(job.useLibravatar()); QCOMPARE(url, QUrl(QStringLiteral("https://secure.gravatar.com:443/avatar/89b4e14cf2fc6d426275c019c6dc9de6?s=1024"))); } void GravatarResolvUrlJobTest::shouldUseHttps() { Gravatar::GravatarResolvUrlJob job; job.setEmail(QStringLiteral("foo@kde.org")); job.setSize(1024); job.setUseLibravatar(false); QUrl url = job.generateGravatarUrl(job.useLibravatar()); QCOMPARE(url, QUrl(QStringLiteral("https://secure.gravatar.com:443/avatar/89b4e14cf2fc6d426275c019c6dc9de6?d=404&s=1024"))); job.setUseLibravatar(true); url = job.generateGravatarUrl(job.useLibravatar()); QCOMPARE(url, QUrl(QStringLiteral("https://seccdn.libravatar.org:443/avatar/2726400c3a33ce56c0ff632cbc0474f766d3b36e68819c601fb02954c1681d85?d=404&s=1024"))); } void GravatarResolvUrlJobTest::shouldNotStart() { Gravatar::GravatarResolvUrlJob job; QVERIFY(!job.canStart()); job.setEmail(QStringLiteral("foo")); QVERIFY(!job.canStart()); job.setEmail(QStringLiteral(" ")); QVERIFY(!job.canStart()); job.setEmail(QStringLiteral("foo@kde.org")); QVERIFY(job.canStart()); } void GravatarResolvUrlJobTest::shouldGenerateGravatarUrl_data() { QTest::addColumn("input"); QTest::addColumn("calculedhash"); QTest::addColumn("output"); QTest::addColumn("uselibravatar"); QTest::newRow("empty") << QString() << QString() << QUrl() << false; QTest::newRow("no domain") << QStringLiteral("foo") << QString() << QUrl() << false; QTest::newRow("validemail") << QStringLiteral("foo@kde.org") << QStringLiteral("89b4e14cf2fc6d426275c019c6dc9de6") << QUrl(QStringLiteral("https://secure.gravatar.com:443/avatar/89b4e14cf2fc6d426275c019c6dc9de6?d=404")) << false; QTest::newRow("validemaillibravatar") << QStringLiteral("foo@kde.org") << QStringLiteral("2726400c3a33ce56c0ff632cbc0474f766d3b36e68819c601fb02954c1681d85") << QUrl(QStringLiteral("https://seccdn.libravatar.org:443/avatar/2726400c3a33ce56c0ff632cbc0474f766d3b36e68819c601fb02954c1681d85?d=404")) << true; } void GravatarResolvUrlJobTest::shouldGenerateGravatarUrl() { QFETCH(QString, input); QFETCH(QString, calculedhash); QFETCH(QUrl, output); QFETCH(bool, uselibravatar); Gravatar::GravatarResolvUrlJob job; job.setEmail(input); job.setUseLibravatar(uselibravatar); QUrl url = job.generateGravatarUrl(job.useLibravatar()); - QCOMPARE(calculedhash, job.calculatedHash()); + QCOMPARE(calculedhash, job.calculatedHash().hexString()); QCOMPARE(url, output); } QTEST_MAIN(GravatarResolvUrlJobTest) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4d4b01e..58f2616 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,99 +1,100 @@ add_definitions(-DTRANSLATION_DOMAIN=\"libgravatar\") set(gravatarlib_SRCS misc/gravatarcache.cpp + misc/hash.cpp widgets/gravatardownloadpixmapwidget.cpp widgets/gravatardownloadpixmapdialog.cpp widgets/gravatarconfigwidget.cpp widgets/gravatarconfiguresettingsdialog.cpp job/gravatarresolvurljob.cpp ) ecm_qt_declare_logging_category(gravatarlib_SRCS HEADER gravatar_debug.h IDENTIFIER GRAVATAR_LOG CATEGORY_NAME org.kde.pim.gravatar) kconfig_add_kcfg_files(gravatarlib_SRCS settings/gravatarsettings.kcfgc ) add_library( KF5Gravatar ${gravatarlib_SRCS} ) generate_export_header(KF5Gravatar BASE_NAME gravatar) add_library(KF5::Gravatar ALIAS KF5Gravatar) target_link_libraries(KF5Gravatar PRIVATE KF5::ConfigCore KF5::I18n KF5::WidgetsAddons KF5::ConfigGui KF5::PimCommon KF5::TextWidgets ) target_include_directories(KF5Gravatar INTERFACE "$") target_include_directories(KF5Gravatar PUBLIC "$") set_target_properties(KF5Gravatar PROPERTIES VERSION ${GRAVATAR_VERSION_STRING} SOVERSION ${GRAVATAR_SOVERSION} EXPORT_NAME Gravatar ) install(TARGETS KF5Gravatar EXPORT KF5GravatarTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} ${LIBRARY_NAMELINK} ) ecm_generate_headers(Gravatar_Camelcase_job_HEADERS HEADER_NAMES GravatarResolvUrlJob REQUIRED_HEADERS Gravatar_job_HEADERS PREFIX Gravatar RELATIVE job ) ecm_generate_headers(Gravatar_Camelcase_misc_HEADERS HEADER_NAMES GravatarCache REQUIRED_HEADERS Gravatar_misc_HEADERS PREFIX Gravatar RELATIVE misc ) ecm_generate_headers(Gravatar_Camelcase_widgets_HEADERS HEADER_NAMES GravatarConfigureSettingsDialog GravatarConfigWidget GravatarDownloadPixmapWidget REQUIRED_HEADERS Gravatar_widgets_HEADERS PREFIX Gravatar RELATIVE widgets ) ecm_generate_pri_file(BASE_NAME Gravatar LIB_NAME KF5Gravatar DEPS "" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/Gravatar ) install(FILES ${Gravatar_Camelcase_widgets_HEADERS} ${Gravatar_Camelcase_job_HEADERS} ${Gravatar_Camelcase_misc_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/Gravatar COMPONENT Devel ) install(FILES ${Gravatar_widgets_HEADERS} ${Gravatar_job_HEADERS} ${Gravatar_misc_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/gravatar_export.h ${CMAKE_CURRENT_BINARY_DIR}/gravatarsettings.h DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/gravatar COMPONENT Devel ) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) diff --git a/src/job/gravatarresolvurljob.cpp b/src/job/gravatarresolvurljob.cpp index 901e12c..7e52b79 100644 --- a/src/job/gravatarresolvurljob.cpp +++ b/src/job/gravatarresolvurljob.cpp @@ -1,263 +1,278 @@ /* Copyright (c) 2015-2017 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "gravatarresolvurljob.h" #include "misc/gravatarcache.h" +#include "misc/hash.h" #include "gravatar_debug.h" #include #include #include #include #include #include using namespace Gravatar; class Gravatar::GravatarResolvUrlJobPrivate { public: GravatarResolvUrlJobPrivate() : mNetworkAccessManager(nullptr) , mSize(80) + , mBackends(Gravatar) , mHasGravatar(false) , mUseDefaultPixmap(false) - , mUseLibravatar(false) - , mFallbackGravatar(true) - , mFallbackDone(false) { } QPixmap mPixmap; QString mEmail; - QString mCalculatedHash; + Hash mCalculatedHash; QNetworkAccessManager *mNetworkAccessManager; int mSize; + + enum Backend { + None = 0x0, + Libravatar = 0x1, + Gravatar = 0x2 + }; + int mBackends; + bool mHasGravatar; bool mUseDefaultPixmap; - bool mUseLibravatar; - bool mFallbackGravatar; - bool mFallbackDone; }; GravatarResolvUrlJob::GravatarResolvUrlJob(QObject *parent) : QObject(parent) , d(new Gravatar::GravatarResolvUrlJobPrivate) { } GravatarResolvUrlJob::~GravatarResolvUrlJob() { delete d; } bool GravatarResolvUrlJob::canStart() const { if (PimCommon::NetworkManager::self()->networkConfigureManager()->isOnline()) { return !d->mEmail.trimmed().isEmpty() && (d->mEmail.contains(QLatin1Char('@'))); } else { return false; } } QUrl GravatarResolvUrlJob::generateGravatarUrl(bool useLibravatar) { return createUrl(useLibravatar); } bool GravatarResolvUrlJob::hasGravatar() const { return d->mHasGravatar; } void GravatarResolvUrlJob::startNetworkManager(const QUrl &url) { - if (PimCommon::NetworkManager::self()->networkConfigureManager()->isOnline()) { - if (!d->mNetworkAccessManager) { - d->mNetworkAccessManager = new QNetworkAccessManager(this); - connect(d->mNetworkAccessManager, &QNetworkAccessManager::finished, this, &GravatarResolvUrlJob::slotFinishLoadPixmap); - } - QNetworkReply *reply = d->mNetworkAccessManager->get(QNetworkRequest(url)); - connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(slotError(QNetworkReply::NetworkError))); - } else { - qCDebug(GRAVATAR_LOG) << " network is not connected"; - deleteLater(); - return; + if (!d->mNetworkAccessManager) { + d->mNetworkAccessManager = new QNetworkAccessManager(this); + connect(d->mNetworkAccessManager, &QNetworkAccessManager::finished, this, &GravatarResolvUrlJob::slotFinishLoadPixmap); } + d->mNetworkAccessManager->get(QNetworkRequest(url)); } void GravatarResolvUrlJob::start() { + if (d->mBackends == GravatarResolvUrlJobPrivate::None) + d->mBackends = GravatarResolvUrlJobPrivate::Gravatar; // default is Gravatar if nothing else is selected + d->mHasGravatar = false; - d->mFallbackDone = false; if (canStart()) { - d->mCalculatedHash.clear(); - const QUrl url = createUrl(d->mUseLibravatar); - Q_EMIT resolvUrl(url); - if (!cacheLookup(d->mCalculatedHash)) - startNetworkManager(url); + processNextBackend(); } else { qCDebug(GRAVATAR_LOG) << "Gravatar can not start"; deleteLater(); } } +void GravatarResolvUrlJob::processNextBackend() +{ + if (d->mHasGravatar || d->mBackends == GravatarResolvUrlJobPrivate::None) { + Q_EMIT finished(this); + deleteLater(); + return; + } + + QUrl url; + if (d->mBackends & GravatarResolvUrlJobPrivate::Libravatar) { + d->mBackends &= ~GravatarResolvUrlJobPrivate::Libravatar; + url = createUrl(true); + } else if (d->mBackends & GravatarResolvUrlJobPrivate::Gravatar) { + d->mBackends &= ~GravatarResolvUrlJobPrivate::Gravatar; + url = createUrl(false); + } + + Q_EMIT resolvUrl(url); + if (!cacheLookup(d->mCalculatedHash)) + startNetworkManager(url); + else + processNextBackend(); +} + void GravatarResolvUrlJob::slotFinishLoadPixmap(QNetworkReply *reply) { if (reply->error() == QNetworkReply::NoError) { d->mPixmap.loadFromData(reply->readAll()); d->mHasGravatar = true; //For the moment don't use cache other we will store a lot of pixmap if (!d->mUseDefaultPixmap) { GravatarCache::self()->saveGravatarPixmap(d->mCalculatedHash, d->mPixmap); } - } else if (d->mUseLibravatar && d->mFallbackGravatar && !d->mFallbackDone) { - d->mFallbackDone = true; - d->mCalculatedHash.clear(); - const QUrl url = createUrl(false); - Q_EMIT resolvUrl(url); - if (!cacheLookup(d->mCalculatedHash)) - startNetworkManager(url); - return; + } else { + if (reply->error() == QNetworkReply::ContentNotFoundError) + GravatarCache::self()->saveMissingGravatar(d->mCalculatedHash); + else + qCDebug(GRAVATAR_LOG) << "Network error:" << reply->request().url() << reply->errorString(); } - reply->deleteLater(); - Q_EMIT finished(this); - deleteLater(); -} -void GravatarResolvUrlJob::slotError(QNetworkReply::NetworkError error) -{ - if (error == QNetworkReply::ContentNotFoundError) { - d->mHasGravatar = false; - } + processNextBackend(); } QString GravatarResolvUrlJob::email() const { return d->mEmail; } void GravatarResolvUrlJob::setEmail(const QString &email) { d->mEmail = email; } -QString GravatarResolvUrlJob::calculateHash(bool useLibravator) +Hash GravatarResolvUrlJob::calculateHash(bool useLibravator) { - QCryptographicHash hash(useLibravator ? QCryptographicHash::Sha256 : QCryptographicHash::Md5); - hash.addData(d->mEmail.toLower().toUtf8()); - return QString::fromUtf8(hash.result().toHex()); + const auto email = d->mEmail.toLower().toUtf8(); + if (useLibravator) + return Hash(QCryptographicHash::hash(email, QCryptographicHash::Sha256), Hash::Sha256); + return Hash(QCryptographicHash::hash(email, QCryptographicHash::Md5), Hash::Md5); } bool GravatarResolvUrlJob::fallbackGravatar() const { - return d->mFallbackGravatar; + return d->mBackends & GravatarResolvUrlJobPrivate::Gravatar; } void GravatarResolvUrlJob::setFallbackGravatar(bool fallbackGravatar) { - d->mFallbackGravatar = fallbackGravatar; + if (fallbackGravatar) + d->mBackends |= GravatarResolvUrlJobPrivate::Gravatar; + else + d->mBackends &= ~GravatarResolvUrlJobPrivate::Gravatar; } bool GravatarResolvUrlJob::useLibravatar() const { - return d->mUseLibravatar; + return d->mBackends & GravatarResolvUrlJobPrivate::Libravatar; } void GravatarResolvUrlJob::setUseLibravatar(bool useLibravatar) { - d->mUseLibravatar = useLibravatar; + if (useLibravatar) + d->mBackends |= GravatarResolvUrlJobPrivate::Libravatar; + else + d->mBackends &= ~GravatarResolvUrlJobPrivate::Libravatar; } bool GravatarResolvUrlJob::useDefaultPixmap() const { return d->mUseDefaultPixmap; } void GravatarResolvUrlJob::setUseDefaultPixmap(bool useDefaultPixmap) { d->mUseDefaultPixmap = useDefaultPixmap; } int GravatarResolvUrlJob::size() const { return d->mSize; } QPixmap GravatarResolvUrlJob::pixmap() const { return d->mPixmap; } void GravatarResolvUrlJob::setSize(int size) { if (size <= 0) { size = 80; } else if (size > 2048) { size = 2048; } d->mSize = size; } -QString GravatarResolvUrlJob::calculatedHash() const +Hash GravatarResolvUrlJob::calculatedHash() const { return d->mCalculatedHash; } QUrl GravatarResolvUrlJob::createUrl(bool useLibravatar) { QUrl url; - d->mCalculatedHash.clear(); + d->mCalculatedHash = Hash(); if (!canStart()) { return url; } QUrlQuery query; if (!d->mUseDefaultPixmap) { //Add ?d=404 query.addQueryItem(QStringLiteral("d"), QStringLiteral("404")); } if (d->mSize != 80) { query.addQueryItem(QStringLiteral("s"), QString::number(d->mSize)); } url.setScheme(QStringLiteral("https")); if (useLibravatar) { url.setHost(QStringLiteral("seccdn.libravatar.org")); } else { url.setHost(QStringLiteral("secure.gravatar.com")); } url.setPort(443); d->mCalculatedHash = calculateHash(useLibravatar); - url.setPath(QLatin1String("/avatar/") + d->mCalculatedHash); + url.setPath(QLatin1String("/avatar/") + d->mCalculatedHash.hexString()); url.setQuery(query); return url; } -bool GravatarResolvUrlJob::cacheLookup(const QString &hash) +bool GravatarResolvUrlJob::cacheLookup(const Hash &hash) { bool haveStoredPixmap = false; const QPixmap pix = GravatarCache::self()->loadGravatarPixmap(hash, haveStoredPixmap); - if (haveStoredPixmap && !pix.isNull()) { + if (haveStoredPixmap && !pix.isNull()) { // we know a Gravatar for this hash d->mPixmap = pix; d->mHasGravatar = true; Q_EMIT finished(this); deleteLater(); return true; } - return false; + return haveStoredPixmap; } diff --git a/src/job/gravatarresolvurljob.h b/src/job/gravatarresolvurljob.h index a836843..e2e71bc 100644 --- a/src/job/gravatarresolvurljob.h +++ b/src/job/gravatarresolvurljob.h @@ -1,81 +1,84 @@ /* Copyright (C) 2015-2017 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef GRAVATARRESOLVURLJOB_H #define GRAVATARRESOLVURLJOB_H #include "gravatar_export.h" #include #include #include #include + +class GravatarResolvUrlJobTest; namespace Gravatar { class GravatarResolvUrlJobPrivate; +class Hash; + class GRAVATAR_EXPORT GravatarResolvUrlJob : public QObject { Q_OBJECT public: explicit GravatarResolvUrlJob(QObject *parent = nullptr); ~GravatarResolvUrlJob(); bool canStart() const; void start(); QString email() const; void setEmail(const QString &email); - QUrl generateGravatarUrl(bool useLibravatar); - bool hasGravatar() const; - QString calculatedHash() const; - void setSize(int size); - int size() const; QPixmap pixmap() const; bool useDefaultPixmap() const; void setUseDefaultPixmap(bool useDefaultPixmap); bool useLibravatar() const; void setUseLibravatar(bool useLibravatar); bool fallbackGravatar() const; void setFallbackGravatar(bool fallbackGravatar); Q_SIGNALS: void finished(Gravatar::GravatarResolvUrlJob *); void resolvUrl(const QUrl &url); private Q_SLOTS: void slotFinishLoadPixmap(QNetworkReply *reply); - void slotError(QNetworkReply::NetworkError error); private: + friend class ::GravatarResolvUrlJobTest; + + QUrl generateGravatarUrl(bool useLibravatar); + Hash calculatedHash() const; + void processNextBackend(); void startNetworkManager(const QUrl &url); QUrl createUrl(bool useLibravatar); - QString calculateHash(bool useLibravator); - bool cacheLookup(const QString &hash); + Hash calculateHash(bool useLibravator); + bool cacheLookup(const Hash &hash); GravatarResolvUrlJobPrivate *const d; }; } #endif // GRAVATARRESOLVURLJOB_H diff --git a/src/misc/gravatarcache.cpp b/src/misc/gravatarcache.cpp index dbad6f8..bdcd547 100644 --- a/src/misc/gravatarcache.cpp +++ b/src/misc/gravatarcache.cpp @@ -1,133 +1,211 @@ /* Copyright (c) 2015-2017 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "gravatarcache.h" -#include #include "gravatar_debug.h" +#include "hash.h" #include +#include #include #include +#include #include + +#include +#include + using namespace Gravatar; Q_GLOBAL_STATIC(GravatarCache, s_gravatarCache) class Gravatar::GravatarCachePrivate { public: - GravatarCachePrivate() + template + inline void insertMissingHash(std::vector &vec, const T &hash) + { + auto it = std::lower_bound(vec.begin(), vec.end(), hash); + if (it != vec.end() && *it == hash) + return; // already present (shouldn't happen) + vec.insert(it, hash); + } + + template + inline void saveVector(const std::vector &vec, const QString &fileName) { + QSaveFile f(mGravatarPath + fileName); + if (!f.open(QFile::WriteOnly)) { + qCWarning(GRAVATAR_LOG) << "Can't write missing hashes cache file:" << f.fileName() << f.errorString(); + return; + } + + f.resize(vec.size() * sizeof(T)); + f.write(reinterpret_cast(vec.data()), vec.size() * sizeof(T)); + f.commit(); } - QCache mCachePixmap; + template + inline void loadVector(std::vector &vec, const QString &fileName) + { + if (!vec.empty()) // already loaded + return; + + QFile f(mGravatarPath + fileName); + if (!f.open(QFile::ReadOnly)) + return; // does not exist yet + + if (f.size() % sizeof(T) != 0) { + qCWarning(GRAVATAR_LOG) << "Missing hash cache is corrupt:" << f.fileName(); + return; + } + vec.resize(f.size() / sizeof(T)); + f.read(reinterpret_cast(vec.data()), f.size()); + } + + QCache mCachePixmap; QString mGravatarPath; + std::vector mMd5Misses; + std::vector mSha256Misses; }; GravatarCache::GravatarCache() : d(new Gravatar::GravatarCachePrivate) { d->mCachePixmap.setMaxCost(20); //Make sure that this folder is created. Otherwise we can't store gravatar d->mGravatarPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/gravatar/"); QDir().mkpath(d->mGravatarPath); } GravatarCache::~GravatarCache() { delete d; } GravatarCache *GravatarCache::self() { return s_gravatarCache; } -void GravatarCache::saveGravatarPixmap(const QString &hashStr, const QPixmap &pixmap) +void GravatarCache::saveGravatarPixmap(const Hash &hash, const QPixmap &pixmap) { - if (!hashStr.isEmpty() && !pixmap.isNull()) { - if (!d->mCachePixmap.contains(hashStr)) { - const QString path = d->mGravatarPath + hashStr + QLatin1String(".png"); - qCDebug(GRAVATAR_LOG) << " path " << path; - if (pixmap.save(path)) { - qCDebug(GRAVATAR_LOG) << " saved in cache " << hashStr << path; - d->mCachePixmap.insert(hashStr, new QPixmap(pixmap)); - } - } + if (!hash.isValid() || pixmap.isNull()) + return; + + const QString path = d->mGravatarPath + hash.hexString() + QLatin1String(".png"); + qCDebug(GRAVATAR_LOG) << " path " << path; + if (pixmap.save(path)) { + qCDebug(GRAVATAR_LOG) << " saved in cache " << path; + d->mCachePixmap.insert(hash, new QPixmap(pixmap)); + } +} + +void GravatarCache::saveMissingGravatar(const Hash &hash) +{ + switch (hash.type()) { + case Hash::Invalid: + break; + case Hash::Md5: + d->insertMissingHash(d->mMd5Misses, hash.md5()); + d->saveVector(d->mMd5Misses, QStringLiteral("missing.md5")); + break; + case Hash::Sha256: + d->insertMissingHash(d->mSha256Misses, hash.sha256()); + d->saveVector(d->mSha256Misses, QStringLiteral("missing.sha256")); + break; } } -QPixmap GravatarCache::loadGravatarPixmap(const QString &hashStr, bool &gravatarStored) +QPixmap GravatarCache::loadGravatarPixmap(const Hash &hash, bool &gravatarStored) { gravatarStored = false; - qCDebug(GRAVATAR_LOG) << " hashStr" << hashStr; - if (!hashStr.isEmpty()) { - if (d->mCachePixmap.contains(hashStr)) { - qCDebug(GRAVATAR_LOG) << " contains in cache " << hashStr; + qCDebug(GRAVATAR_LOG) << " hashStr" << hash.hexString(); + if (!hash.isValid()) + return QPixmap(); + + // in-memory cache + if (d->mCachePixmap.contains(hash)) { + qCDebug(GRAVATAR_LOG) << " contains in cache " << hash.hexString(); + gravatarStored = true; + return *(d->mCachePixmap.object(hash)); + } + + // file-system cache + const QString path = d->mGravatarPath + hash.hexString() + QLatin1String(".png"); + if (QFileInfo::exists(path)) { + QPixmap pix; + if (pix.load(path)) { + qCDebug(GRAVATAR_LOG) << " add to cache " << hash.hexString() << path; + d->mCachePixmap.insert(hash, new QPixmap(pix)); gravatarStored = true; - return *(d->mCachePixmap.object(hashStr)); - } else { - const QString path = d->mGravatarPath + hashStr + QLatin1String(".png"); - if (QFileInfo::exists(path)) { - QPixmap pix; - if (pix.load(path)) { - qCDebug(GRAVATAR_LOG) << " add to cache " << hashStr << path; - d->mCachePixmap.insert(hashStr, new QPixmap(pix)); - gravatarStored = true; - return pix; - } - } else { - return QPixmap(); - } + return pix; } } + + // missing gravatar cache (ie. known to not exist one) + switch (hash.type()) { + case Hash::Invalid: + break; + case Hash::Md5: + d->loadVector(d->mMd5Misses, QStringLiteral("missing.md5")); + gravatarStored = std::binary_search(d->mMd5Misses.begin(), d->mMd5Misses.end(), hash.md5()); + break; + case Hash::Sha256: + d->loadVector(d->mSha256Misses, QStringLiteral("missing.sha256")); + gravatarStored = std::binary_search(d->mSha256Misses.begin(), d->mSha256Misses.end(), hash.sha256()); + break; + } + return QPixmap(); } int GravatarCache::maximumSize() const { return d->mCachePixmap.maxCost(); } void GravatarCache::setMaximumSize(int maximumSize) { if (d->mCachePixmap.maxCost() != maximumSize) { d->mCachePixmap.setMaxCost(maximumSize); } } void GravatarCache::clear() { d->mCachePixmap.clear(); } void GravatarCache::clearAllCache() { const QString path = d->mGravatarPath; if (!path.isEmpty()) { QDir dir(path); if (dir.exists()) { const QFileInfoList list = dir.entryInfoList(); // get list of matching files and delete all for (const QFileInfo &it : list) { dir.remove(it.fileName()); } } } clear(); + d->mMd5Misses.clear(); + d->mSha256Misses.clear(); } diff --git a/src/misc/gravatarcache.h b/src/misc/gravatarcache.h index 341f052..0853e15 100644 --- a/src/misc/gravatarcache.h +++ b/src/misc/gravatarcache.h @@ -1,53 +1,56 @@ /* Copyright (C) 2015-2017 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef GRAVATARCACHE_H #define GRAVATARCACHE_H #include "gravatar_export.h" #include namespace Gravatar { class GravatarCachePrivate; +class Hash; + class GRAVATAR_EXPORT GravatarCache { public: static GravatarCache *self(); GravatarCache(); ~GravatarCache(); - void saveGravatarPixmap(const QString &hashStr, const QPixmap &pixmap); + void saveGravatarPixmap(const Hash &hash, const QPixmap &pixmap); + void saveMissingGravatar(const Hash &hash); - QPixmap loadGravatarPixmap(const QString &hashStr, bool &gravatarStored); + QPixmap loadGravatarPixmap(const Hash &hash, bool &gravatarStored); int maximumSize() const; void setMaximumSize(int maximumSize); void clear(); void clearAllCache(); private: Q_DISABLE_COPY(GravatarCache) GravatarCachePrivate *const d; }; } #endif // GRAVATARCACHE_H diff --git a/src/misc/hash.cpp b/src/misc/hash.cpp new file mode 100644 index 0000000..d5e81b3 --- /dev/null +++ b/src/misc/hash.cpp @@ -0,0 +1,108 @@ +/* + Copyright (c) 2017 Volker Krause + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "hash.h" + +#include +#include + +using namespace Gravatar; + +Hash::Hash() + : m_type(Invalid) +{ +} + +Hash::Hash(const QByteArray &data, Type type) + : m_type(type) +{ + switch (type) { + case Invalid: + break; + case Md5: + Q_ASSERT(sizeof(Hash128) == data.size()); + m_hash.md5 = *reinterpret_cast(data.constData()); + break; + case Sha256: + Q_ASSERT(sizeof(Hash256) == data.size()); + m_hash.sha256 = *reinterpret_cast(data.constData()); + break; + } +} + +bool Hash::operator==(const Hash &other) const +{ + if (m_type != other.m_type) + return false; + switch (m_type) { + case Invalid: + return true; + case Md5: + return m_hash.md5 == other.m_hash.md5; + case Sha256: + return m_hash.sha256 == other.m_hash.sha256; + } + Q_UNREACHABLE(); +} + +bool Hash::isValid() const +{ + return m_type != Invalid; +} + +Hash::Type Hash::type() const +{ + return m_type; +} + +Hash128 Hash::md5() const +{ + return m_hash.md5; +} + +Hash256 Hash::sha256() const +{ + return m_hash.sha256; +} + +QString Hash::hexString() const +{ + switch (m_type) { + case Invalid: + return QString(); + case Md5: + return QString::fromLatin1(QByteArray::fromRawData(reinterpret_cast(&m_hash.md5), sizeof(Hash128)).toHex()); + case Sha256: + return QString::fromLatin1(QByteArray::fromRawData(reinterpret_cast(&m_hash.sha256), sizeof(Hash256)).toHex()); + } + Q_UNREACHABLE(); +} + +uint Gravatar::qHash(const Hash &h, uint seed) +{ + switch (h.type()) { + case Hash::Invalid: + return seed; + case Hash::Md5: + return qHashBits(&h.m_hash.md5, sizeof(Hash128), seed); + case Hash::Sha256: + return qHashBits(&h.m_hash.sha256, sizeof(Hash256), seed); + } + Q_UNREACHABLE(); +} diff --git a/src/misc/hash.h b/src/misc/hash.h new file mode 100644 index 0000000..d64cd4a --- /dev/null +++ b/src/misc/hash.h @@ -0,0 +1,82 @@ +/* + Copyright (c) 2017 Volker Krause + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef GRAVATAR_HASH_H +#define GRAVATAR_HASH_H + +#include "gravatar_export.h" + +#include +#include + +class QByteArray; +class QString; + +namespace Gravatar { + +template struct UnsignedInt +{ + bool operator<(const UnsignedInt &other) const + { + return memcmp(data, other.data, Size) < 0; + } + + bool operator==(const UnsignedInt &other) const + { + return memcmp(data, other.data, Size) == 0; + } + + uint8_t data[Size]; +}; +struct Hash128 : public UnsignedInt<16> {}; +struct Hash256 : public UnsignedInt<32> {}; + +class Hash; +unsigned int qHash(const Hash &h, unsigned int seed = 0); + +// exported for unit tests only +class GRAVATAR_EXPORT Hash +{ +public: + enum Type { Invalid, Md5, Sha256 }; + Hash(); + explicit Hash(const QByteArray &data, Type type); + + bool operator==(const Hash &other) const; + + bool isValid() const; + + Type type() const; + Hash128 md5() const; + Hash256 sha256() const; + + QString hexString() const; + +private: + friend unsigned int qHash(const Hash &h, unsigned int seed); + union { + Hash128 md5; + Hash256 sha256; + } m_hash; + Type m_type; +}; + +} + +#endif // GRAVATAR_HASH_H