修复: 评论组件泄露私密 token、cwebp 无超时、选项长度与去重

Waline(安全)
- item.html 的 path 从 window.location.href 改为剔除 ?t= 后的 URL。
  私密题 /_/<domain>/<id>?t=<token> 的钥匙原本会整条发往 Waline 服务端
  并落进其访问日志。只删 t、保留其他参数,公开题的评论线程键不变。

cwebp(可靠性)
- ConvertGifToAnimatedWebP 改用 exec.CommandContext + 30s 上限。
  原来裸 exec.Command 无超时,畸形/超大 GIF 会把子进程和上传 goroutine 一起挂住。
  超时杀进程并记 Warn,调用方按既有逻辑回退为原 GIF,不影响上传成功。

投票选项长度(正确性)
- vote_options.content 由 varchar(200) 放宽到 300,与配置默认值一致。
  此前 201–300 字的选项通过校验后在写库阶段失败。
- 新增 db.MaxVoteOptionLen;voteOptionMaxLen() 取配置与存储上限的较小值,
  校验与前端 maxlength 同源,避免配置调大后再次越界。

多选 checkbox 去重(正确性)
- 抽出 appendUniqueValue,预置值与「其他」文本共用同一 seen 集合。
  此前重复提交同名字段会让同一选项在统计里被计多次。

测试
- vote_dedupe_test.go:去重与顺序。
- vote_option_len_test.go:配置小于/等于/大于存储上限三种情形。
- TestVoteOptionMaxLengthRoundTrip:300 个多字节字符存回不截断。
- TestVoteOptionColumnWidens:把列改回 200 后 AutoMigrate 能扩回 300(老库升级路径)。

验证:WSL2 Debian + PostgreSQL 全量 go test ./... 27 个包全绿;
隔离实例渲染单题页确认新的 path 逻辑。
This commit is contained in:
tamakyi
2026-11-05 17:04:47 +08:00
parent 7ffab24025
commit 5fdc3df22d
8 changed files with 249 additions and 12 deletions
+11 -1
View File
@@ -34,11 +34,21 @@ type voteOptions struct {
*gorm.DB
}
// MaxVoteOptionLen 是选项内容在数据库中的最大字符数。
//
// 必须与 conf 的默认 vote_option_len 保持一致:配置曾经允许 300 而这里只有 200,
// 于是 201–300 字的选项「校验通过、写库失败」(MySQL 严格模式下报错,
// PostgreSQL 直接报 value too long)。改列宽而不是改配置,是因为配置本来
// 就是给箱主按需调小的,不该反过来限制它的上界。
//
// ⚠️ 改这个常量时必须同步改下面 Content 字段的 size 标签(结构体标签不能引用常量)。
const MaxVoteOptionLen = 300
// VoteOption 选项表:单选/多选/评分/排序共用。
type VoteOption struct {
dbutil.Model
VoteID uint `gorm:"index:idx_option_vote" json:"vote_id"`
Content string `gorm:"size:200" json:"content"`
Content string `gorm:"size:300" json:"content"`
SortOrder int `gorm:"default:0" json:"sort_order"`
}
+89
View File
@@ -114,3 +114,92 @@ func testVoteOptionsCountByVoteID(t *testing.T, ctx context.Context, db *voteOpt
require.Nil(t, err)
require.Equal(t, int64(0), got)
}
// TestVoteOptionMaxLengthRoundTrip 保证「配置允许的长度」真的能存下。
//
// 曾经 conf 允许 300 字而列宽只有 200:201–300 字的选项通过校验后在写库时才失败
// (PostgreSQL 报 value too long,MySQL 严格模式同样报错),用户看到的是「内部错误」。
func TestVoteOptionMaxLengthRoundTrip(t *testing.T) {
conn, cleanup := newTestDB(t)
t.Cleanup(func() {
if err := cleanup("vote_options"); err != nil {
t.Fatal(err)
}
})
ctx := context.Background()
store := NewVoteOptionsStore(conn)
// 结构体标签不能引用常量,列宽与 MaxVoteOptionLen 是两处独立字面量,
// 这里显式比一次,防止只改了一边。
columns, err := conn.Migrator().ColumnTypes(&VoteOption{})
require.Nil(t, err)
for _, ct := range columns {
if ct.Name() != "content" {
continue
}
if length, ok := ct.Length(); ok {
require.EqualValues(t, MaxVoteOptionLen, length, "vote_options.content 列宽必须与 MaxVoteOptionLen 一致")
}
}
// 用多字节字符填满上限,顺便验证按字符而非字节计数。
content := ""
for len([]rune(content)) < MaxVoteOptionLen {
content += "選"
}
option, err := store.Create(ctx, CreateVoteOptionOptions{VoteID: 1, Content: content})
require.Nil(t, err)
got, err := store.ListByVoteID(ctx, 1)
require.Nil(t, err)
require.Len(t, got, 1)
require.Equal(t, option.ID, got[0].ID)
require.Equal(t, MaxVoteOptionLen, len([]rune(got[0].Content)), "存进去的字符数应与上限一致,未被截断")
}
// TestVoteOptionColumnWidens 验证老库升级路径:已经存在 varchar(200) 的列,
// 跑一次 AutoMigrate 后必须变成 MaxVoteOptionLen。
//
// 只改 model 标签是不够的 —— 如果 AutoMigrate 不扩列,老部署上线后
// 201–300 字的选项依旧写不进去,而新库(测试库)永远发现不了这个问题。
func TestVoteOptionColumnWidens(t *testing.T) {
conn, cleanup := newTestDB(t)
t.Cleanup(func() {
if err := cleanup("vote_options"); err != nil {
t.Fatal(err)
}
})
// 退回老库的列宽(两种方言语法不同)。
var alter string
switch conn.Dialector.Name() {
case "postgres":
alter = "ALTER TABLE vote_options ALTER COLUMN content TYPE varchar(200)"
case "mysql":
alter = "ALTER TABLE vote_options MODIFY COLUMN content varchar(200)"
default:
t.Skipf("未适配的方言:%s", conn.Dialector.Name())
}
require.Nil(t, conn.Exec(alter).Error)
contentLength := func() int64 {
columns, err := conn.Migrator().ColumnTypes(&VoteOption{})
require.Nil(t, err)
for _, ct := range columns {
if ct.Name() != "content" {
continue
}
length, ok := ct.Length()
require.True(t, ok, "content 列应有明确长度")
return length
}
t.Fatal("未找到 content 列")
return 0
}
require.EqualValues(t, 200, contentLength(), "前置条件:列宽应已被改回 200")
require.Nil(t, conn.AutoMigrate(&VoteOption{}))
require.EqualValues(t, MaxVoteOptionLen, contentLength(), "AutoMigrate 必须把列宽扩到 MaxVoteOptionLen")
}