fix v12
This commit is contained in:
@@ -443,3 +443,19 @@ Community rules and content boundaries / コミュニティルールとコンテ
|
||||
⚠️ 用户反馈「成功页按钮还是老样子」时,先确认**是否真的部署了**:直接抓线上 HTML 看有没有新标记
|
||||
(`grep 'uk-margin-large-top uk-text-center'` 在 `/rules` 上)。实测那次线上仍是 `uk-margin-large-top"`
|
||||
旧结构 → 与浏览器缓存无关,是代码没更新。以后别先怀疑缓存。
|
||||
(第二次复测已生效:`uk-text-center` 在、弹窗按钮 `>送信<`、成功页 `flex: 1 1 160px` 出现 2 次。)
|
||||
|
||||
## 受控用户强制最高审核档(用户要求:不分级,全部级别)
|
||||
|
||||
- `moderation.EffectiveStrictness(ctx, userID, raw)`:**有任何处置(L1–L4)就返回 strict**,忽略账号设置。
|
||||
**处置查询失败时回退账号自身设置**,而不是"失败即强制 strict" —— 否则 DB 抖动会让全站突然加严、
|
||||
正常提交被大面积误拒,影响面远大于漏放一个受控账号。
|
||||
- 7 个审核调用点全部从 `StrictnessFor(raw)` 改为 `EffectiveStrictness(...)`:chat_handler、follow_up×2、
|
||||
page、question、user/vote×3、vote/vote。`StrictnessFor` 保留为纯解析函数(模板回显用)。
|
||||
- `UpdateModerationStrictness`:处置期内直接拒绝保存并 flash `admin.flash.strictnessLocked`。
|
||||
- 设置页 `templates/user/moderation-strictness.html`:`$root.StrictnessLocked` 时提示 + select/按钮 disabled。
|
||||
⚠️ 该文件仍只能用 shell/python 改(Edit/Write 会注入 `data-page-node-id`),本次用 python 精确替换。
|
||||
- Profile handler 注入 `StrictnessLocked`(读 ActiveLevel)。
|
||||
|
||||
验证:`go build ./...` EXIT=0;checktpl OK;db/i18n/templates 测试全绿。
|
||||
性能:每次内容提交多一次 `user_restrictions` 查询(走 user_id 索引),审核本身要走七牛网络,1ms 可忽略。
|
||||
|
||||
@@ -778,6 +778,7 @@ var enDict = map[string]string{
|
||||
|
||||
"admin.flash.moderationStrictnessUpdated": "Moderation strictness updated",
|
||||
"admin.flash.moderationStrictnessInvalid": "Invalid moderation strictness value, nothing was saved",
|
||||
"admin.flash.strictnessLocked": "Moderation strictness is locked at the highest level during a restriction and restored afterwards",
|
||||
|
||||
// Vote management: owner visible (P1)
|
||||
"vote.err.invalidID": "Invalid vote ID",
|
||||
|
||||
@@ -778,6 +778,7 @@ var jaDict = map[string]string{
|
||||
|
||||
"admin.flash.moderationStrictnessUpdated": "審核の厳格度を更新しました",
|
||||
"admin.flash.moderationStrictnessInvalid": "審核の厳格度の値が不正なため、保存しませんでした",
|
||||
"admin.flash.strictnessLocked": "措置期間中はモデレーション強度が最高に固定されます。解除後に元の設定へ戻ります",
|
||||
|
||||
// 投票管理:所有者に表示(P1)
|
||||
"vote.err.invalidID": "無効な投票 ID",
|
||||
|
||||
@@ -778,6 +778,7 @@ var zhCNDict = map[string]string{
|
||||
|
||||
"admin.flash.moderationStrictnessUpdated": "审核严格度已更新",
|
||||
"admin.flash.moderationStrictnessInvalid": "审核严格度取值无效,未保存",
|
||||
"admin.flash.strictnessLocked": "处置期间审核严格度已锁定为最高档,解除后自动恢复你的原设置",
|
||||
|
||||
// 投票管理:箱主可见(P1)
|
||||
"vote.err.invalidID": "无效的投票 ID",
|
||||
|
||||
@@ -778,6 +778,7 @@ var zhTWDict = map[string]string{
|
||||
|
||||
"admin.flash.moderationStrictnessUpdated": "審核嚴格度已更新",
|
||||
"admin.flash.moderationStrictnessInvalid": "審核嚴格度取值無效,未儲存",
|
||||
"admin.flash.strictnessLocked": "處置期間審核嚴格度已鎖定為最高檔,解除後自動恢復你的原設定",
|
||||
|
||||
// 投票管理:箱主可見(P1)
|
||||
"vote.err.invalidID": "無效的投票 ID",
|
||||
|
||||
@@ -275,7 +275,7 @@ func SendMessage(ctx context.Context, cache cache.Cache) {
|
||||
// 内容安全检测(仅文本)
|
||||
// 统一守卫:与提问 / 投票共用同一份档位判定,箱主的「审核严格度」决定放行与否。
|
||||
if content != "" {
|
||||
if _, err := moderation.Check(ctx.Request().Context(), moderation.StrictnessFor(pageUser.ModerationStrictness), ctx.Lang(), content); err != nil {
|
||||
if _, err := moderation.Check(ctx.Request().Context(), moderation.EffectiveStrictness(ctx.Request().Context(), pageUser.ID, pageUser.ModerationStrictness), ctx.Lang(), content); err != nil {
|
||||
// 审核拦截:就地重渲染会话页,回填已输入内容并展示拦截原因。
|
||||
// 不复用 SetErrorFlash + Redirect:那会导致输入框内容在刷新后丢失(用户已确认的无提示+内容消失问题)。
|
||||
renderConversation(ctx, domain, token, content, err)
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/tamakyi/TamaBox/internal/conf"
|
||||
"github.com/tamakyi/TamaBox/internal/db"
|
||||
"github.com/tamakyi/TamaBox/internal/i18n"
|
||||
"github.com/tamakyi/TamaBox/internal/security/censor"
|
||||
)
|
||||
@@ -35,6 +36,24 @@ func StrictnessFor(raw string) censor.Strictness {
|
||||
return censor.ParseStrictness(conf.Security.DefaultModerationStrictness)
|
||||
}
|
||||
|
||||
// EffectiveStrictness 返回账号**实际生效**的审核档位,是内容审核应当使用的入口。
|
||||
//
|
||||
// 处于处置期(L1–L4)的账号一律强制 strict,忽略账号自身设置:
|
||||
// 限制期内允许保留 lax / standard,等于给了箱主一个免审通道 —— 一边被限制、
|
||||
// 一边把门槛调到最低,是明显可被利用的组合。
|
||||
//
|
||||
// 处置查询失败时**回退账号自身设置**,而不是"失败即强制 strict":
|
||||
// DB 抖动时反向兜底会让全站突然加严,正常用户的提交被大面积误拒,
|
||||
// 影响面远大于「漏放一个受控账号」。
|
||||
func EffectiveStrictness(ctx context.Context, userID uint, raw string) censor.Strictness {
|
||||
if userID != 0 {
|
||||
if level, _, err := db.Restrictions.ActiveLevel(ctx, userID); err == nil && level != "" {
|
||||
return censor.StrictnessStrict
|
||||
}
|
||||
}
|
||||
return StrictnessFor(raw)
|
||||
}
|
||||
|
||||
// Check 执行一次内容审核,并按箱主严格度裁决是否放行。
|
||||
//
|
||||
// 这是提问 / 投票 / 私聊三类场景**唯一**应当使用的审核入口。
|
||||
|
||||
@@ -102,7 +102,7 @@ func PublishFollowUp(ctx context.Context, pageUser *db.User, question *db.Questi
|
||||
|
||||
// 内容安全检测:统一守卫,按箱主「审核严格度」裁决是否放行
|
||||
// (提问 / 投票 / 私聊共用同一份判定,见 internal/security/moderation)。
|
||||
if _, err := moderation.Check(ctx.Request().Context(), moderation.StrictnessFor(pageUser.ModerationStrictness), ctx.Lang(), f.Content); err != nil {
|
||||
if _, err := moderation.Check(ctx.Request().Context(), moderation.EffectiveStrictness(ctx.Request().Context(), pageUser.ID, pageUser.ModerationStrictness), ctx.Lang(), f.Content); err != nil {
|
||||
ctx.SetErrorFlash(err.Error())
|
||||
ctx.Redirect(fmt.Sprintf("/_/%s/%d?t=%s", pageUser.Domain, question.ID, question.Token))
|
||||
return
|
||||
@@ -176,7 +176,7 @@ func AnswerFollowUp(ctx context.Context, pageUser *db.User, question *db.Questio
|
||||
followUpID := uint(ctx.ParamInt("followUpID"))
|
||||
|
||||
// 内容安全检测:统一守卫,箱主本人回复同样按自己的审核严格度裁决。
|
||||
if _, err := moderation.Check(ctx.Request().Context(), moderation.StrictnessFor(pageUser.ModerationStrictness), ctx.Lang(), f.Answer); err != nil {
|
||||
if _, err := moderation.Check(ctx.Request().Context(), moderation.EffectiveStrictness(ctx.Request().Context(), pageUser.ID, pageUser.ModerationStrictness), ctx.Lang(), f.Answer); err != nil {
|
||||
// 同请求渲染,用 SetError;原先的 SetErrorFlash 在这里不会显示出来(既有缺陷)。
|
||||
ctx.SetError(err, f)
|
||||
ctx.Success("question/item")
|
||||
|
||||
@@ -356,7 +356,7 @@ func New(ctx context.Context, f form.NewQuestion, pageUser *db.User, recaptcha r
|
||||
// 内容审核严格度:提问 / 私聊 / 投票三条链路共用同一份档位(users.moderation_strictness),
|
||||
// 取值解释与裁决逻辑见 internal/security/censor/policy.go。
|
||||
// 在此解析一次,供下方私聊分支与普通提问分支复用,保证同一请求的判定口径一致。
|
||||
strictness := moderation.StrictnessFor(pageUser.ModerationStrictness)
|
||||
strictness := moderation.EffectiveStrictness(ctx.Request().Context(), pageUser.ID, pageUser.ModerationStrictness)
|
||||
|
||||
// ---------- 私聊处理 ----------
|
||||
if f.StartChat == "true" && ctx.IsLogged {
|
||||
|
||||
@@ -185,7 +185,7 @@ func PublishAnswer(ctx context.Context, pageUser *db.User, question *db.Question
|
||||
// 🚨 Content security check.
|
||||
// 统一守卫:按箱主「审核严格度」裁决是否放行(审核源不可用仍为 fail-closed,
|
||||
// 拒绝发布而非免审放行)。返回的 censorResponse 用于下方落审计字段。
|
||||
censorResponse, err := moderation.Check(ctx.Request().Context(), moderation.StrictnessFor(pageUser.ModerationStrictness), ctx.Lang(), answer)
|
||||
censorResponse, err := moderation.Check(ctx.Request().Context(), moderation.EffectiveStrictness(ctx.Request().Context(), pageUser.ID, pageUser.ModerationStrictness), ctx.Lang(), answer)
|
||||
if err != nil {
|
||||
ctx.SetError(err, f)
|
||||
ctx.Success("question/item")
|
||||
|
||||
@@ -25,6 +25,14 @@ func UpdateModerationStrictness(ctx context.Context, f form.UpdateModerationStri
|
||||
return
|
||||
}
|
||||
|
||||
// 处置期内禁止修改档位:EffectiveStrictness 会强制 strict,允许保存只会让箱主
|
||||
// 以为「我调成宽松了」,实际审核仍按严格执行,是更糟的误导。
|
||||
if level, _, err := db.Restrictions.ActiveLevel(ctx.Request().Context(), ctx.User.ID); err == nil && level != "" {
|
||||
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "admin.flash.strictnessLocked"))
|
||||
ctx.Redirect(returnTab(ctx))
|
||||
return
|
||||
}
|
||||
|
||||
// 非法取值一律拒绝保存:宁可保留原档位,也不要写入一个解释不了的中间状态。
|
||||
if !censor.IsValidStrictness(f.Strictness) {
|
||||
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "admin.flash.moderationStrictnessInvalid"))
|
||||
|
||||
@@ -33,6 +33,11 @@ func Profile(ctx context.Context) {
|
||||
ctx.Data["HasPagePassword"] = ctx.User.PagePassword != ""
|
||||
ctx.Data["HasAskPassword"] = ctx.User.AskPassword != ""
|
||||
ctx.Data["ActiveProfileTab"] = ProfileTab(ctx.Query("tab"))
|
||||
// 处置期内审核严格度被强制为最高档且不可修改(见 moderation.EffectiveStrictness),
|
||||
// 设置页需要把下拉和保存按钮禁用,否则箱主会以为自己的修改生效了。
|
||||
if level, _, err := db.Restrictions.ActiveLevel(ctx.Request().Context(), ctx.User.ID); err == nil && level != "" {
|
||||
ctx.Data["StrictnessLocked"] = true
|
||||
}
|
||||
ctx.Data["TagLimits"] = tagLimits()
|
||||
ctx.Data["ChatLimits"] = chatLimits()
|
||||
InjectFeatureViews(ctx)
|
||||
|
||||
+3
-3
@@ -59,7 +59,7 @@ type voteForm struct {
|
||||
// censorCheck 对单段文本做内容审核。与提问 / 私聊共用 moderation.Check 守卫,
|
||||
// 是否拦截由发起人的「审核严格度」档位决定;fail-closed,服务异常时拒绝发布。
|
||||
func censorCheck(ctx context.Context, text string) error {
|
||||
_, err := moderation.Check(ctx.Request().Context(), moderation.StrictnessFor(ctx.User.ModerationStrictness), ctx.Lang(), text)
|
||||
_, err := moderation.Check(ctx.Request().Context(), moderation.EffectiveStrictness(ctx.Request().Context(), ctx.User.ID, ctx.User.ModerationStrictness), ctx.Lang(), text)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ func parseVoteMeta(ctx context.Context, draft bool) (*voteForm, error) {
|
||||
// 内容审核(fail-closed):非草稿执行;草稿跳过(见上说明)
|
||||
var censorMeta datatypes.JSON
|
||||
if !draft {
|
||||
titleCensor, err := moderation.Check(ctx.Request().Context(), moderation.StrictnessFor(ctx.User.ModerationStrictness), ctx.Lang(), title)
|
||||
titleCensor, err := moderation.Check(ctx.Request().Context(), moderation.EffectiveStrictness(ctx.Request().Context(), ctx.User.ID, ctx.User.ModerationStrictness), ctx.Lang(), title)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -974,7 +974,7 @@ func voteOwnerStrictness(ctx context.Context, ownerID uint) censor.Strictness {
|
||||
Error("Failed to get vote owner for moderation strictness")
|
||||
return censor.StrictnessStrict
|
||||
}
|
||||
return moderation.StrictnessFor(owner.ModerationStrictness)
|
||||
return moderation.EffectiveStrictness(ctx.Request().Context(), ownerID, owner.ModerationStrictness)
|
||||
}
|
||||
|
||||
// censorVoteForPublish 发布前对投票全部可见文本做内容审核(fail-closed)。
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ func InvalidateResultsCache(voteID uint) {
|
||||
func censorCheck(ctx context.Context, owner *db.User, lang, text string) error {
|
||||
strictness := censor.StrictnessStrict
|
||||
if owner != nil {
|
||||
strictness = moderation.StrictnessFor(owner.ModerationStrictness)
|
||||
strictness = moderation.EffectiveStrictness(ctx.Request().Context(), owner.ID, owner.ModerationStrictness)
|
||||
}
|
||||
_, err := moderation.Check(ctx.Request().Context(), strictness, lang, text)
|
||||
return err
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
|
||||
<p class="uk-text-meta uk-text-small" data-page-node-id="GF3aXisM5hvonHBGY2m69p">{{t $root.LangCode "admin.moderationStrictnessHint"}}</p>
|
||||
<p class="uk-text-meta uk-text-small" data-page-node-id="AoSpxE29Qnz8tgDhTEX8SU">{{t $root.LangCode "admin.moderationStrictnessBaseline"}}</p>
|
||||
{{ if $root.StrictnessLocked }}
|
||||
<div class="uk-alert-warning" uk-alert>
|
||||
<p>{{t $root.LangCode "admin.flash.strictnessLocked"}}</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<!-- 档位列必须 uk-table-shrink + nowrap:右侧「拦截范围」文案很长,
|
||||
自动布局会把该列压到极窄,导致 4 字档位名被逐字折行。 -->
|
||||
@@ -48,7 +53,7 @@
|
||||
解析走 internal/template 的 moderationStrictness 函数(同一份事实来源)。 */}}
|
||||
{{ $current := moderationStrictness $root.LoggedUser.ModerationStrictness }}
|
||||
<div class="uk-margin-small-top" data-page-node-id="HIWMgKH7FXZ9BYygBQ23A8">
|
||||
<select name="strictness" class="uk-select uk-width-medium" style="max-width: 100%;" data-page-node-id="43cBp9IvsqBOUUpcIqISNq">
|
||||
<select name="strictness" class="uk-select uk-width-medium" style="max-width: 100%;"{{ if $root.StrictnessLocked }} disabled{{ end }} data-page-node-id="43cBp9IvsqBOUUpcIqISNq">
|
||||
<option value="lax"{{ if eq $current "lax" }} selected{{ end }} data-page-node-id="XLfr3E5HDhGCCe27M9DQQX">
|
||||
{{t $root.LangCode "admin.moderationStrictnessOption.lax"}}
|
||||
</option>
|
||||
@@ -66,7 +71,7 @@
|
||||
</div>
|
||||
|
||||
<div class="tb-card-save" data-page-node-id="vmD40ecSRAGLrWqlz5F2c5">
|
||||
<button type="submit" class="uk-button uk-button-primary" data-page-node-id="r04SFvzxXIgjRvgG0xaQIw">{{t $root.LangCode "admin.updateModerationStrictness"}}</button>
|
||||
<button type="submit" class="uk-button uk-button-primary"{{ if $root.StrictnessLocked }} disabled{{ end }} data-page-node-id="r04SFvzxXIgjRvgG0xaQIw">{{t $root.LangCode "admin.updateModerationStrictness"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user