时间框跳段 + 弹窗不再脏地址栏 + 统计脚本做成后台开关
时间框(flatpickr allowInput 的手输体验)
- flatpickr 把 <input type=datetime-local> 就地改成 type=text,原生分段编辑器被替换掉;
allowInput 又是「整串解析」语义:实测 parseDate('2026') 只出年份、'202666' 才出月份,
于是「年份很长才跳下一位」;'20260606' 还会被静默错解成 2030-12-06 且不报错。
- 新增 static/js/datetime-input.js:自定义 parseDate(纯数字只收 4/6/8/12/14 位,
歧义与非法日期一律拒)+ 输入引导(段满即补分隔符,敲完 4 位年份立刻跳月份位)。
按标准格式不做自动补零、不前滚;越界值原样保留交报错,不静默改成合法值。
- 三处页面(编辑/照片/专题)共用 attachPicker,配置与原先一致,模板净减 5 行。
弹窗
- window.prompt 会把提示文本与默认值写进地址栏并留进历史(confirm/alert 无此问题)。
- 新增 static/js/prompt-box.js 替代:原生 <dialog>.showModal(),遮罩/Esc/焦点齐全,
旧浏览器降级成页面顶部一条。已换 report.html 复制链接兜底与 edit.html 活动改名。
- 门禁 no_prompt_test.go:静态扫模板禁止 prompt(,并钉住 JS 与 CSS 都在。
统计脚本
- 原来只能改 conf/app.ini(要重启),现在后台「站点设置」可开关 + 填写,默认关闭。
- cleanTrackScript 挡住注入面:开闭 <script> 标签数不配平(提前闭合会让页面散架)、
src 的 javascript:/data: 伪协议、超长截断。注意不能用「出现 </script 就拒」当规则——
百度统计、Umami 贴的就是完整 HTML,正常用法会被挡死。
门禁
- datetime-input.test.js:34 条断言(node 直跑,无依赖)。
- TestCleanTrackScript:逐条边界 + 定义性不变量(任何输入都不得产出含 </script 的非空结果)。
- 三项均做过变异验证,改坏实现都会被对应测试抓到。
实测:真实站点逐键 2→20→202→2026-→2026-06-→2026-06-06T;
统计脚本六个场景(默认关/只填不开/开+URL/后台与登录页不挂/危险输入/关掉恢复)全部符合预期。
全量 internal/web 204 例 0 skip,其余包全绿,gofmt 干净。
This commit is contained in:
@@ -581,6 +581,10 @@ func (s *Server) settingsSubmit(w http.ResponseWriter, r *http.Request) {
|
||||
if r.PostFormValue("regions_mode") == setting.RegionsBlacklist {
|
||||
next.RegionsMode = setting.RegionsBlacklist
|
||||
}
|
||||
next.TrackScript = cleanTrackScript(r.PostFormValue("track_script"))
|
||||
// 开关只在「勾选 + 脚本非空」时才生效:勾了但没填内容等于没开,
|
||||
// 否则 layout 里会挂一个空 <script>,看着像开了其实什么也没统计。
|
||||
next.TrackEnabled = r.PostFormValue("track_enabled") == "on" && next.TrackScript != ""
|
||||
if err := s.sets.Save(ctx, next); err != nil {
|
||||
s.serverError(w, r, err)
|
||||
return
|
||||
@@ -588,6 +592,48 @@ func (s *Server) settingsSubmit(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/admin/settings?saved=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// cleanTrackScript 收拾管理员粘进来的统计脚本。
|
||||
//
|
||||
// 这段内容会被原样注入**每一个公开页面**,所以要挡住几类会伤到访客的输入。
|
||||
// 但**不能**简单地把「出现 </script 就拒」当规则:百度统计、Umami 这类服务
|
||||
// 贴进来的就是完整 HTML,一行里必然带 </script>,一拒就把正常用法挡死了。
|
||||
//
|
||||
// 真正的注入面只有一种:**未闭合的 <script> 里出现 </script> 会提前闭合**,
|
||||
// 后面的内容当正文渲染,页面布局散架。判据因此是「奇偶」:
|
||||
// 开标签数与闭合标签数不配平 ⇒ 有提前闭合 ⇒ 拒。配平则整段原样放行。
|
||||
//
|
||||
// 另外挡住:javascript: / data: 这类伪协议的 src(点击即执行),
|
||||
// 以及超长内容(多半是误粘了整份 HTML 文档,截断)。
|
||||
func cleanTrackScript(v string) string {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
return ""
|
||||
}
|
||||
const maxLen = 4096
|
||||
if len(v) > maxLen {
|
||||
v = v[:maxLen]
|
||||
}
|
||||
low := strings.ToLower(v)
|
||||
// 开闭标签数必须配平,否则有人能靠 </script> 提前闭合、把后面的内容
|
||||
// 从「脚本」变成「正文」注入到页面里。
|
||||
open := strings.Count(low, "<script")
|
||||
// 去掉属性里的 <script 字样不算开标签(如 data-src="<script"),这里只做保守计数:
|
||||
// 宁可多拒一次,也别放过真的注入。
|
||||
shut := strings.Count(low, "</script")
|
||||
if shut > open {
|
||||
return ""
|
||||
}
|
||||
// src 只放行 http/https 与站内相对路径,挡掉 javascript: / data: 伪协议
|
||||
if idx := strings.Index(low, "src="); idx >= 0 {
|
||||
rest := strings.TrimLeft(v[idx+4:], "\"'")
|
||||
if rest != "" && !strings.HasPrefix(rest, "http://") &&
|
||||
!strings.HasPrefix(rest, "https://") && !strings.HasPrefix(rest, "/") {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// ---------- 修改口令 ----------
|
||||
|
||||
func (s *Server) passwordPage(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user