Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(gravatar): support both sha256 and md5 hash algorithm for gravatar #912

Merged
merged 1 commit into from
Jun 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion conf/artalk.example.simple.yml
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ frontend:
nestSort: DATE_ASC
gravatar:
mirror: https://www.gravatar.com/avatar/
params: d=mp&s=240
params: sha256=1&d=mp&s=240
pagination:
pageSize: 20
readMore: true
Expand Down
2 changes: 1 addition & 1 deletion conf/artalk.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ frontend:
# API URL
mirror: https://www.gravatar.com/avatar/
# API parameters
params: d=mp&s=240
params: sha256=1&d=mp&s=240
# Comment pagination
pagination:
# Number of comments per page
Expand Down
4 changes: 2 additions & 2 deletions conf/artalk.example.zh-CN.yml
Original file line number Diff line number Diff line change
Expand Up @@ -425,9 +425,9 @@ frontend:
# 头像 Gravatar
gravatar:
# API 地址
mirror: https://cravatar.cn/avatar/
mirror: https://weavatar.com/avatar/
# API 参数
params: d=mp&s=240
params: sha256=1&d=mp&s=240
# 评论分页
pagination:
# 每页评论数
Expand Down
3 changes: 0 additions & 3 deletions docs/docs/.vitepress/theme/Artalk.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,6 @@ function initArtalk(conf: any) {
artalk = Artalk.init({
el: el.value,
emoticons: '/assets/emoticons/default.json',
gravatar: {
mirror: 'https://cravatar.cn/avatar/',
},
...conf,
})

Expand Down
4 changes: 2 additions & 2 deletions docs/docs/guide/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,8 @@ ATK_TRUSTED_DOMAINS_0="https://a.com"
| **ATK_FRONTEND_EDITORTRAVEL** | `true` | 评论框穿梭 | frontend.editorTravel (界面配置 > 评论框穿梭) |
| **ATK_FRONTEND_EMOTICONS** | `"https://cdn.jsdelivr.net/gh/ArtalkJS/Emoticons/grps/default.json"` | 表情包 | frontend.emoticons (界面配置 > 表情包) |
| **ATK_FRONTEND_FLATMODE** | `"auto"` | 平铺模式 (可选:`["auto", "true", "false"]`) | frontend.flatMode (界面配置 > 平铺模式) |
| **ATK_FRONTEND_GRAVATAR_MIRROR** | `"https://cravatar.cn/avatar/"` | API 地址 | frontend.gravatar.mirror (界面配置 > 头像 Gravatar > API 地址) |
| **ATK_FRONTEND_GRAVATAR_PARAMS** | `"d=mp&s=240"` | API 参数 | frontend.gravatar.params (界面配置 > 头像 Gravatar > API 参数) |
| **ATK_FRONTEND_GRAVATAR_MIRROR** | `"https://weavatar.com/avatar/"` | API 地址 | frontend.gravatar.mirror (界面配置 > 头像 Gravatar > API 地址) |
| **ATK_FRONTEND_GRAVATAR_PARAMS** | `"sha256=1&d=mp&s=240"` | API 参数 | frontend.gravatar.params (界面配置 > 头像 Gravatar > API 参数) |
| **ATK_FRONTEND_HEIGHTLIMIT_CHILDREN** | `400` | 子评论区域限高 (单位:px) | frontend.heightLimit.children (界面配置 > 内容限高 > 子评论区域限高) |
| **ATK_FRONTEND_HEIGHTLIMIT_CONTENT** | `300` | 评论内容限高 (单位:px) | frontend.heightLimit.content (界面配置 > 内容限高 > 评论内容限高) |
| **ATK_FRONTEND_HEIGHTLIMIT_SCROLLABLE** | `false` | 滚动限高 (允许限高区域滚动) | frontend.heightLimit.scrollable (界面配置 > 内容限高 > 滚动限高) |
Expand Down
8 changes: 6 additions & 2 deletions docs/docs/guide/frontend/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,12 +358,14 @@ gravatar: {
**Gravatar 镜像地址**

- 类型:`String`
- 默认值:`"https://sdn.geekzu.org/avatar/"`
- 默认值:`"https://www.gravatar.com/avatar/"`

如果你觉得 Gravatar 头像加载速度不理想,可以尝试替换。

例如:

> WeAvatar:https://weavatar.com/avatar/
>
> Cravatar:https://cravatar.cn/avatar/
>
> V2EX:https://cdn.v2ex.com/gravatar/
Expand All @@ -377,7 +379,7 @@ gravatar: {
**Gravatar API 参数**

- 类型:`String`
- 默认值:`"d=mp&s=240"`
- 默认值:`"sha256=1&d=mp&s=240"`

例如,你可以通过该配置项设置默认头像 (`d=mp`) 和头像尺寸 (`s=240`)。

Expand All @@ -387,6 +389,8 @@ gravatar: {

::: warning 更新注意

v2.8.7 支持通过 SHA256 加密算法生成 Gravatar 头像链接,以提高用户隐私保护,请在 `params` 中填入 `sha256=1` 启用该加密([#874](https://github.com/ArtalkJS/Artalk/issues/874))。

v2.5.5 已废弃 `gravatar.default` 配置项,请使用 `gravatar.params` 替代。

:::
Expand Down
2 changes: 1 addition & 1 deletion internal/config/cache.go

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions internal/config/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package config

import (
"strings"

"github.com/ArtalkJS/Artalk/internal/utils"
)

// Get the hash function according to the frontend gravatar.params configuration
func GetHashFuncByFrontendConf(conf *Config) func(string) string {
if gravatar, ok := conf.Frontend["gravatar"].(map[string]interface{}); ok {
if params, ok := gravatar["params"].(string); ok {
if strings.Contains(params, "sha256=1") {
return utils.GetSha256Hash
}
}
}
return utils.GetMD5Hash
}
51 changes: 51 additions & 0 deletions internal/config/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package config

import (
"reflect"
"testing"

"github.com/ArtalkJS/Artalk/internal/utils"
"github.com/stretchr/testify/assert"
)

func Test_GetHashFuncByFrontendConf(t *testing.T) {
tests := map[string]struct {
config *Config
expected any
}{
"nil frontend conf": {
config: &Config{Frontend: nil},
expected: utils.GetMD5Hash,
},
"empty frontend conf": {
config: &Config{Frontend: map[string]interface{}{}},
expected: utils.GetMD5Hash,
},
"frontend conf without hash func": {
config: &Config{Frontend: map[string]interface{}{
"gravatar": false,
}},
expected: utils.GetMD5Hash,
},
"frontend conf with gravatar params but without hash func": {
config: &Config{Frontend: map[string]interface{}{
"gravatar": map[string]interface{}{"params": "a=1&b=2"},
}},
expected: utils.GetMD5Hash,
},
"frontend conf with gravatar params containing hash func": {
config: &Config{Frontend: map[string]interface{}{
"gravatar": map[string]interface{}{"params": "a=1&sha256=1&b=2"},
}},
expected: utils.GetSha256Hash,
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
expect := reflect.ValueOf(tc.expected).Pointer()
actual := reflect.ValueOf(GetHashFuncByFrontendConf(tc.config)).Pointer()
assert.Equal(t, expect, actual)
})
}
}
15 changes: 10 additions & 5 deletions internal/core/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@
}

func (app *App) injectDefaultServices() {
// 请勿依赖注入顺序
AppInject[*EmailService](app, NewEmailService(app))
AppInject[*IPRegionService](app, NewIPRegionService(app))
AppInject[*NotifyService](app, NewNotifyService(app))
AppInject[*AntiSpamService](app, NewAntiSpamService(app))
// Please do not depend on the order of dependency injection
AppInject(app, NewEmailService(app))
AppInject(app, NewIPRegionService(app))
AppInject(app, NewNotifyService(app))
AppInject(app, NewAntiSpamService(app))
}

func (app *App) registerDefaultHooks() {
Expand Down Expand Up @@ -270,6 +270,11 @@
// create new dao instance
app.SetDao(dao.NewDao(dbInstance))

// patch: switch comment email hash algorithm by config
app.Dao().SetCommentEmailHashFunc(func(email string) string {
return config.GetHashFuncByFrontendConf(app.Conf())(strings.ToLower(email))
})

Check warning on line 276 in internal/core/base.go

View check run for this annotation

Codecov / codecov/patch

internal/core/base.go#L275-L276

Added lines #L275 - L276 were not covered by tests

return nil
}

Expand Down
13 changes: 11 additions & 2 deletions internal/dao/cook.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Convert Entity to JSON Response Data Structure for API
// TODO: (refactor) consider to extract this file to a new package
package dao

import (
Expand All @@ -10,6 +12,13 @@

const CommonDateTimeFormat = "2006-01-02 15:04:05"

// TODO: (refactor) remove this global variable
var getCommentEmailHash = func(email string) string { return utils.GetMD5Hash(strings.ToLower(email)) }

func (dao *Dao) SetCommentEmailHashFunc(fn func(string) string) {
getCommentEmailHash = fn

Check warning on line 19 in internal/dao/cook.go

View check run for this annotation

Codecov / codecov/patch

internal/dao/cook.go#L18-L19

Added lines #L18 - L19 were not covered by tests
}

// ===============
// Comment
// ===============
Expand Down Expand Up @@ -40,7 +49,7 @@
ContentMarked: markedContent,
UserID: c.UserID,
Nick: user.Name,
EmailEncrypted: utils.GetSha256Hash(strings.ToLower(user.Email)),
EmailEncrypted: getCommentEmailHash(user.Email),
Link: user.Link,
UA: c.UA,
Date: c.CreatedAt.Local().Format(CommonDateTimeFormat),
Expand Down Expand Up @@ -92,7 +101,7 @@
Site: dao.CookSite(&site),
CookedComment: entity.CookedComment{
ID: c.ID,
EmailEncrypted: utils.GetSha256Hash(strings.ToLower(user.Email)),
EmailEncrypted: getCommentEmailHash(user.Email),

Check warning on line 104 in internal/dao/cook.go

View check run for this annotation

Codecov / codecov/patch

internal/dao/cook.go#L104

Added line #L104 was not covered by tests
Link: user.Link,
UA: c.UA,
IsCollapsed: c.IsCollapsed,
Expand Down
13 changes: 3 additions & 10 deletions ui/artalk-sidebar/src/components/Header.vue
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
<script setup lang="ts">
import sha256 from 'crypto-js/sha256'
import { storeToRefs } from 'pinia'
import { useUserStore } from '../stores/user'
import { useNavStore } from '../stores/nav'
import { artalk, isOpenFromSidebar } from '../global'
import { isOpenFromSidebar } from '../global'

const nav = useNavStore()
const router = useRouter()
const user = useUserStore()
const { t } = useI18n()
const { site: curtSite, isAdmin, email } = storeToRefs(user)

const userAvatarImgURL = computed(() => {
const conf = artalk?.ctx.conf?.gravatar
if (!conf) return ``
return `${conf.mirror.replace(/\/$/, '')}/${sha256(email.value.toLowerCase())}?${conf.params.replace(/^\?/, '')}`
})
const { site: curtSite, isAdmin, avatar } = storeToRefs(user)

const avatarClickHandler = () => {
if (!isOpenFromSidebar()) logout()
Expand Down Expand Up @@ -44,7 +37,7 @@ const logout = () => {
</template>
<template v-else>
<div class="avatar" @click="avatarClickHandler">
<img :src="userAvatarImgURL" />
<img :src="avatar" />
</div>
</template>

Expand Down
20 changes: 20 additions & 0 deletions ui/artalk-sidebar/src/stores/user.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { defineStore } from 'pinia'
import { bootParams, getArtalk } from '../global'
import sha256 from 'crypto-js/sha256'
import md5 from 'crypto-js/md5'

export const useUserStore = defineStore('user', {
state: () => ({
Expand Down Expand Up @@ -31,4 +33,22 @@ export const useUserStore = defineStore('user', {
this.token = userData.token
},
},
getters: {
avatar: (state) => {
return getGravatar(state.email)
},
},
})

function getGravatar(email: string) {
// TODO get avatar url from backend api
const conf = getArtalk()?.ctx.conf?.gravatar
if (!conf) return ''

const emailHash =
typeof conf.params == 'string' && conf.params.includes('sha256=1')
? sha256(email.toLowerCase()).toString()
: md5(email.toLowerCase()).toString()

return `${conf.mirror.replace(/\/$/, '')}/${emailHash}?${conf.params.replace(/^\?/, '')}`
}
4 changes: 2 additions & 2 deletions ui/artalk/src/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ const defaults: ArtalkConfig = {
statPageKeyAttr: 'data-page-key',

gravatar: {
mirror: 'https://cravatar.cn/avatar/',
params: 'd=mp&s=240',
mirror: 'https://www.gravatar.com/avatar/',
params: 'sha256=1&d=mp&s=240',
},

pagination: {
Expand Down