fix v13
This commit is contained in:
@@ -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.EffectiveStrictness(ctx.Request().Context(), pageUser.ID, pageUser.ModerationStrictness), ctx.Lang(), f.Answer); err != nil {
|
||||
if _, err := moderation.Check(ctx.Request().Context(), moderation.EffectiveStrictnessFor(restrictionLevel, pageUser.ModerationStrictness), ctx.Lang(), f.Answer); err != nil {
|
||||
// 同请求渲染,用 SetError;原先的 SetErrorFlash 在这里不会显示出来(既有缺陷)。
|
||||
ctx.SetError(err, f)
|
||||
ctx.Success("question/item")
|
||||
|
||||
@@ -281,6 +281,20 @@ func ListAPI(ctx context.Context) error {
|
||||
return ctx.ServerError()
|
||||
}
|
||||
|
||||
// ⚠️ 这个接口不经过 question.Pager 中间件,必须自己补两道检查:
|
||||
// 1) L4 整页停用:否则「停用」只挡 HTML 页面,API 仍能整页拉取内容;
|
||||
// 2) 页面访问密码:Pager 用 session 标记校验,API 不校验就等于密码形同虚设。
|
||||
// 两者都返回「用户不存在」以隐藏真实原因(不暴露处置状态与密码设置)。
|
||||
if level, _, err := db.Restrictions.ActiveLevel(ctx.Request().Context(), pageUser.ID); err == nil && level == db.RestrictionL4 {
|
||||
return ctx.JSONError(40400, i18n.T(ctx.Lang(), "auth.errUserNotFound"))
|
||||
}
|
||||
if pageUser.PagePassword != "" && !(ctx.IsLogged && ctx.User.ID == pageUser.ID) {
|
||||
passKey := fmt.Sprintf("page_pass_%d", pageUser.ID)
|
||||
if authorized, _ := ctx.Session.Get(passKey).(bool); !authorized {
|
||||
return ctx.JSONError(40100, i18n.T(ctx.Lang(), "password.desc"))
|
||||
}
|
||||
}
|
||||
|
||||
pageQuestions, err := db.Questions.GetByUserID(ctx.Request().Context(), pageUser.ID, db.GetQuestionsByUserIDOptions{
|
||||
Cursor: &dbutil.Cursor{
|
||||
Value: cursorValue,
|
||||
|
||||
+16
-14
@@ -185,26 +185,20 @@ 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.EffectiveStrictness(ctx.Request().Context(), pageUser.ID, pageUser.ModerationStrictness), ctx.Lang(), answer)
|
||||
// 已经查过处置级别,直接用,避免 EffectiveStrictness 再打一次同样的库。
|
||||
censorResponse, err := moderation.Check(ctx.Request().Context(), moderation.EffectiveStrictnessFor(restrictionLevel, pageUser.ModerationStrictness), ctx.Lang(), answer)
|
||||
if err != nil {
|
||||
ctx.SetError(err, f)
|
||||
ctx.Success("question/item")
|
||||
return
|
||||
}
|
||||
|
||||
// L2 收窄公开:审核通过后再把该题置为私密,然后写入回答。
|
||||
// L2 收窄公开:审核通过后写回答,并在同一条 UPDATE 里把题目置为私密。
|
||||
// 顺序不能提前到审核之前 —— 审核失败(违规或审核源不可用,fail-closed)会直接返回,
|
||||
// 那时题目已经被改成私密,等于箱主一次失败的编辑让原有公开内容凭空消失。
|
||||
// 放在这里仍然满足「任何时刻都不出现公开 + 已作答」:此刻它还是一条未作答的私密题。
|
||||
// 也不能拆成「先 SetPrivate 再 AnswerByID」:两条语句之间有「公开 + 已作答」窗口,
|
||||
// 且第二条失败会留下「私密 + 无回答」(内容凭空消失)。见 AnswerByIDPrivate 注释。
|
||||
forcedPrivate := db.RestrictionAtLeast(restrictionLevel, db.RestrictionL2)
|
||||
if forcedPrivate {
|
||||
if err := db.Questions.SetPrivate(ctx.Request().Context(), question.ID); err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to force question private")
|
||||
ctx.SetInternalError()
|
||||
ctx.Success("question/item")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.Images) > 0 {
|
||||
image := f.Images[0]
|
||||
@@ -226,8 +220,14 @@ func PublishAnswer(ctx context.Context, pageUser *db.User, question *db.Question
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.Questions.AnswerByID(ctx.Request().Context(), question.ID, f.Answer); err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to answer question")
|
||||
var answerErr error
|
||||
if forcedPrivate {
|
||||
answerErr = db.Questions.AnswerByIDPrivate(ctx.Request().Context(), question.ID, f.Answer)
|
||||
} else {
|
||||
answerErr = db.Questions.AnswerByID(ctx.Request().Context(), question.ID, f.Answer)
|
||||
}
|
||||
if answerErr != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(answerErr).Error("Failed to answer question")
|
||||
ctx.SetInternalError(f)
|
||||
ctx.Success("question/item")
|
||||
return
|
||||
@@ -241,7 +241,9 @@ func PublishAnswer(ctx context.Context, pageUser *db.User, question *db.Question
|
||||
}
|
||||
|
||||
shared.SafeGo(func() {
|
||||
if question.ReceiveReplyEmail != "" && question.Answer == "" { // We only send the email when the question has not been answered.
|
||||
// L2 下题目刚被强制转为私密:提问者没有访问权限,带 token 的链接点开也是 302。
|
||||
// 这时发「你的提问有回答」只会让人以为内容丢了,因此跳过通知。
|
||||
if question.ReceiveReplyEmail != "" && question.Answer == "" && !forcedPrivate { // We only send the email when the question has not been answered.
|
||||
// Send notification to questioner.
|
||||
if err := mail.SendNewAnswerMail(question.ReceiveReplyEmail, pageUser.Domain, question.ID, question.Content, f.Answer, question.Token); err != nil {
|
||||
logrus.WithContext(stdctx.Background()).WithError(err).Error("Failed to send receive reply mail to questioner")
|
||||
|
||||
@@ -294,6 +294,20 @@ func ShareCard(ctx context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 站方处置:任何级别都不再生成整箱分享图(与单题分享卡一致)。
|
||||
// 放在缓存之前 —— 拦截必须即时生效,否则处置后 30 分钟内仍能拿到旧图。
|
||||
// DB 异常时按已处置处理(fail-closed):这张图会带上箱主信息与二维码,是外传载体。
|
||||
if level, _, err := db.Restrictions.ActiveLevel(ctx.Request().Context(), user.ID); err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to query user restriction")
|
||||
ctx.Redirect("/")
|
||||
return
|
||||
} else if level != "" {
|
||||
logrus.WithContext(ctx.Request().Context()).WithField("domain", domain).WithField("level", level).
|
||||
Info("Profile share card blocked by restriction")
|
||||
ctx.Redirect("/")
|
||||
return
|
||||
}
|
||||
|
||||
// 命中缓存直接返回,避免重复下载图片与重新绘制
|
||||
cacheKey := shareCardCacheKey(user)
|
||||
if cached, ok := shareCardCache.Get(cacheKey); ok {
|
||||
|
||||
Reference in New Issue
Block a user