From 62164ecdf2feabc4ddc5926e7c3ef6fdfa2649be Mon Sep 17 00:00:00 2001 From: Matt Hanley <3798302+matthanley@users.noreply.github.com> Date: Mon, 9 May 2022 12:44:04 +0100 Subject: [PATCH] Added gravatar URL to config to make it configurable (#14490) refs https://github.com/TryGhost/Toolbox/issues/288 - Allows switching out the Gravatar URL to use placeholder images when working with mocked demo data --- core/server/lib/image/gravatar.js | 34 ++++++++++++----- core/server/models/member.js | 5 +-- core/shared/config/defaults.json | 3 ++ test/unit/server/lib/image/gravatar.test.js | 41 +++++++++++++++++++-- test/unit/server/models/member.test.js | 2 +- 5 files changed, 68 insertions(+), 17 deletions(-) diff --git a/core/server/lib/image/gravatar.js b/core/server/lib/image/gravatar.js index 39987381bd..e183b52988 100644 --- a/core/server/lib/image/gravatar.js +++ b/core/server/lib/image/gravatar.js @@ -1,5 +1,6 @@ const Promise = require('bluebird'); const crypto = require('crypto'); +const tpl = require('@tryghost/tpl'); class Gravatar { constructor({config, request}) { @@ -7,21 +8,36 @@ class Gravatar { this.request = request; } + url(email, options) { + if (options.default) { + // tpl errors on token `{default}` so we use `{_default}` instead + // but still allow the option to be passed as `default` + options._default = options.default; + } + const defaultOptions = { + size: 250, + _default: 'blank', + rating: 'g' + }; + const emailHash = crypto.createHash('md5').update(email.toLowerCase().trim()).digest('hex'); + const gravatarUrl = this.config.get('gravatar').url; + return tpl(gravatarUrl, Object.assign(defaultOptions, options, {hash: emailHash})); + } + lookup(userData, timeout) { - let gravatarUrl = '//www.gravatar.com/avatar/' + - crypto.createHash('md5').update(userData.email.toLowerCase().trim()).digest('hex') + - '?s=250'; - if (this.config.isPrivacyDisabled('useGravatar')) { return Promise.resolve(); } - - return Promise.resolve(this.request('https:' + gravatarUrl + '&d=404&r=x', {timeout: timeout || 2 * 1000})) + + // test existence using a default 404, but return a different default + // so we still have a fallback if the image gets removed from Gravatar + const testUrl = this.url(userData.email, {default: 404, rating: 'x'}); + const imageUrl = this.url(userData.email, {default: 'mp', rating: 'x'}); + + return Promise.resolve(this.request(testUrl, {timeout: timeout || 2 * 1000})) .then(function () { - gravatarUrl += '&d=mm&r=x'; - return { - image: gravatarUrl + image: imageUrl }; }) .catch({statusCode: 404}, function () { diff --git a/core/server/models/member.js b/core/server/models/member.js index c5a8864d44..79f6aacc6d 100644 --- a/core/server/models/member.js +++ b/core/server/models/member.js @@ -2,7 +2,7 @@ const ghostBookshelf = require('./base'); const uuid = require('uuid'); const _ = require('lodash'); const config = require('../../shared/config'); -const crypto = require('crypto'); +const {gravatar} = require('../lib/image'); const Member = ghostBookshelf.Model.extend({ tableName: 'members', @@ -311,8 +311,7 @@ const Member = ghostBookshelf.Model.extend({ // Will not use gravatar if privacy.useGravatar is false in config attrs.avatar_image = null; if (attrs.email && !config.isPrivacyDisabled('useGravatar')) { - const emailHash = crypto.createHash('md5').update(attrs.email.toLowerCase().trim()).digest('hex'); - attrs.avatar_image = `https://gravatar.com/avatar/${emailHash}?s=250&d=blank`; + attrs.avatar_image = gravatar.url(attrs.email, {size: 250, default: 'blank'}); } return attrs; diff --git a/core/shared/config/defaults.json b/core/shared/config/defaults.json index fbffacb798..b4b0ee00ab 100644 --- a/core/shared/config/defaults.json +++ b/core/shared/config/defaults.json @@ -140,5 +140,8 @@ }, "twitter": { "privateReadOnlyToken": null + }, + "gravatar": { + "url": "https://www.gravatar.com/avatar/{hash}?s={size}&r={rating}&d={_default}" } } diff --git a/test/unit/server/lib/image/gravatar.test.js b/test/unit/server/lib/image/gravatar.test.js index d5e3d5f617..8a91feb6df 100644 --- a/test/unit/server/lib/image/gravatar.test.js +++ b/test/unit/server/lib/image/gravatar.test.js @@ -2,15 +2,38 @@ const should = require('should'); const Gravatar = require('../../../../../core/server/lib/image/gravatar'); describe('lib/image: gravatar', function () { + const gravatarUrl = 'https://www.gravatar.com/avatar/{hash}?s={size}&r={rating}&d={_default}'; + + it('can build a gravatar url', function () { + const gravatar = new Gravatar({config: { + isPrivacyDisabled: () => false, + get: (config) => { + return config === 'gravatar' ? { + url: gravatarUrl + } : null; + } + }, request: () => {}}); + + gravatar.url('exists@example.com', { + size: 180, + rating: 'r' + }).should.eql('https://www.gravatar.com/avatar/ef6dcde5c99bb8f685dd451ccc3e050a?s=180&r=r&d=blank'); + }); + it('can successfully lookup a gravatar url', function (done) { const gravatar = new Gravatar({config: { - isPrivacyDisabled: () => false + isPrivacyDisabled: () => false, + get: (config) => { + return config === 'gravatar' ? { + url: gravatarUrl + } : null; + } }, request: () => {}}); gravatar.lookup({email: 'exists@example.com'}).then(function (result) { should.exist(result); should.exist(result.image); - result.image.should.eql('//www.gravatar.com/avatar/ef6dcde5c99bb8f685dd451ccc3e050a?s=250&d=mm&r=x'); + result.image.should.eql('https://www.gravatar.com/avatar/ef6dcde5c99bb8f685dd451ccc3e050a?s=250&r=x&d=mp'); done(); }).catch(done); @@ -18,7 +41,12 @@ describe('lib/image: gravatar', function () { it('can handle a non existant gravatar', function (done) { const gravatar = new Gravatar({config: { - isPrivacyDisabled: () => false + isPrivacyDisabled: () => false, + get: (config) => { + return config === 'gravatar' ? { + url: gravatarUrl + } : null; + } }, request: () => { return Promise.reject({statusCode: 404}); }}); @@ -34,7 +62,12 @@ describe('lib/image: gravatar', function () { it('will timeout', function () { const delay = 42; const gravatar = new Gravatar({config: { - isPrivacyDisabled: () => false + isPrivacyDisabled: () => false, + get: (config) => { + return config === 'gravatar' ? { + url: gravatarUrl + } : null; + } }, request: (url, options) => { options.timeout.should.eql(delay); }}); diff --git a/test/unit/server/models/member.test.js b/test/unit/server/models/member.test.js index eae00e49a2..2c8ac79a36 100644 --- a/test/unit/server/models/member.test.js +++ b/test/unit/server/models/member.test.js @@ -35,7 +35,7 @@ describe('Unit: models/member', function () { config.set('privacy:useGravatar', true); const json = toJSON(member); - json.avatar_image.should.eql(`https://gravatar.com/avatar/55502f40dc8b7c769880b10874abc9d0?s=250&d=blank`); + json.avatar_image.should.eql(`https://www.gravatar.com/avatar/55502f40dc8b7c769880b10874abc9d0?s=250&r=g&d=blank`); }); it('avatar_image: skips gravatar when privacy.useGravatar=false', function () {