Overleaf checkSanitize 开发脚本解析:用 MediaWiki parse API 校验 Learn 页面 HTML 经 sanitize-html 净化后的一致性
【免费下载链接】overleafA web-based collaborative LaTeX editor项目地址: https://gitcode.com/GitHub_Trending/ov/overleaf
本文聚焦 Overleaf 仓库中的开发辅助脚本checkSanitize:它从 Overleaf Learn 支持 Wiki(MediaWiki)拉取全部页面,模拟 Web 服务对页面 HTML 执行的 sanitize-html 净化流程,并逐页比净化前后差异,帮助开发者在调整净化配置时及时发现会被“误伤”的合法标记。读完本文,你将掌握该脚本的运行方式、数据抓取机制、诊断输出字段的含义,以及为什么它选择 MediaWiki parse API 而非批量导出(bulk export)作为数据源。
脚本要解决的问题
Overleaf 的 Learn(支持文档/Wiki)页面内容托管在 MediaWiki 上,Web 服务渲染前会把这些页面 HTML 交给sanitize-html做净化,防止不受信任的标记进入页面。净化配置(allowlist)过严,就可能把 Wiki 中合法使用的元素“洗掉”或改写,导致线上页面与 Wiki 上看到的预览不一致。
checkSanitize就是这个差异检测器。从 checkSanitizeOptions.mjs 源码中的注释可以看到checkSanitizeOptions is only used in dev env——它是一个仅面向开发环境的校验工具,而非线上运行时组件。其核心判断逻辑在 checkSanitizeOptions.mjs:
text = normalize(text, title) const sanitized = normalize(sanitizeHtml(text, sanitizeOptions)) if (text === sanitized) return // 净化前后一致,静默通过页面净化前后字符串完全一致即通过、不输出任何内容;一旦不一致,就打印一段诊断块(见后文“诊断输出”一节)。脚本引用的共享净化配置路径为modules/learn/app/src/sanitizeOptions.mjs(相对 checkSanitizeOptions.mjs 的导入)。需要说明:从当前仓库快照的源码结构看,services/web/modules/下仅包含 full-project-search、history-v1、launchpad、server-ce-scripts、user-activate 等目录,未见该文件实体,可推断此脚本依赖的 Learn 模块配置随完整开发环境提供,本文仅按脚本代码中的引用路径描述其作用。
运行方式
按 README 的说明,脚本的调用方式为(在services/web目录下):
node scripts/learn/checkSanitize/index.mjs https://LEARN_WIKI其中https://LEARN_WIKI是 Learn Wiki 站点的 Base URL,需要替换为实际地址。index.mjs 对参数做了严格校验——最后一个命令行参数必须以http开头,否则抛出带用法提示的错误:
const BASE_URL = process.argv.pop() if (!BASE_URL.startsWith('http')) { throw new Error( 'Usage: node scripts/learn/checkSanitize/index.mjs https://LEARN_WIKI' ) }主流程非常直接(index.mjs):
getAllPagesAndCache(BASE_URL)获取全站页面列表(带本地缓存);- 对每个页面
scrapeAndCachePage(BASE_URL, page)拉取 parse API 结果; - 取出
parsed.title与parsed.text['*'](MediaWiki parse 结果中*键对应正文 HTML),调用checkSanitizeOptions(page, title, text)执行校验; - 单页出错时先打印
---分隔线与出错页名,再向上抛出,终止整个巡检。
此外,脚本通过 scriptRunner(来自scripts/lib/ScriptRunner.mjs)包裹执行:它会把脚本运行信息(OL_POD_NAME、OL_USERNAME、OL_IMAGE_VERSION等环境变量)写入 MongoDB 的 ScriptLog 集合,若设置了OL_USERNAME,还会在控制台打印 admin 后台的 script-log 跟踪链接。也就是说,这个巡检脚本的运行记录同样可被管理员追溯。
数据抓取层:parse API、分页与本地缓存
抓取逻辑集中在 scrape.mjs,它基于仓库内的@overleaf/fetch-utils(libraries/fetch-utils/index.js)完成 HTTP 请求,并设计了本地缓存以避免重复请求 Wiki。
页面正文:MediaWiki parse API
scrape() 请求 Wiki 的api.php:
const uri = new URL(baseUrl + '/learn-scripts/api.php') uri.search = new URLSearchParams({ page, action: 'parse', format: 'json', redirects: true, }).toString()即action=parse(解析单页并返回 HTML)、format=json、redirects=true(跟随重定向)。scrapeAndCachePage() 先尝试读缓存文件data/learnPages/<页名>.json;未命中才请求 API,取响应中的parse对象,若缺失则打印原始响应并抛出'bad contents',命中后把结果格式化写入缓存。
页名转文件名时有一个实用的防御(getName()):MediaWiki 存在极长的页面标题,直接 percent-encode 后可能超过文件系统文件名长度上限,因此超过 100 字符时截断并追加页名的 SHA-1 哈希,保证唯一且可落盘。
页面列表:generator=allpages + 游标翻页
getAllPagesFrom() 使用查询接口枚举全部页面:
action=query、generator=allpages:生成器模式列出所有页面;gapfilterredir=nonredirects:过滤掉重定向页,源码注释解释这是为了避免把同一内容的重定向页校验两遍;gaplimit=100:把默认每页 10 条提升到 100 条,减少往返;...continueFrom:透传游标。
getAllPages() 依据响应中的continue字段循环翻页直到游标耗尽,最后对页面列表做sort()使巡检顺序稳定可复现。getAllPagesAndCache() 进一步把整个列表缓存到data/learnPages/allPages.txt,下次运行直接复用。所有缓存文件都落在services/web/data/learnPages/下。
核心校验逻辑:normalize、快速 diff 与诊断输出
normalize:消除无关差异
sanitizeOptions 校验前 的 raw 文本会先经过 normalize() 规整,目的是剔除那些“Web 端本来就会丢掉、但与净化配置无关”的差异,让比对聚焦于真正的净化行为:
<style>块处理。Wiki 页面为预览保留<style>,而 Web 端渲染时会丢弃。默认行为(OMIT_STYLE未设为'false'时)是把<style>整块删掉;若设置环境变量EXTRACT_STYLE=true,会先用 prettier(parser: 'css')格式化该 CSS,再以sha1(css)-<encodeURIComponent(title)>.css的文件名写入data/dumpFolder/供人工检查(checkSanitizeOptions.mjs)。相关环境变量:EXTRACT_STYLE:取值'true'时提取并 dump 各页 CSS;OMIT_STYLE:非'false'(默认)时丢弃 style 块,设为'false'则保留参与比对。
- 注释剔除:删除每页底部的
<!-- \nNewPP limit report...注释(MediaWiki 的渲染统计),以及数学字符标注产生的<!-- . -->空注释。 - 一致包裹:若输出不以
<html><head>开头,则包一层<html><head>…</head></html>,保证 cheerio 解析渲染一致。 - 内联 style 归一:去掉
style="…;"的尾分号,并把:、;后的空白规范化(如margin: 1px→margin:1px)。 - cheerio 重新序列化:最后用
cheerio.load(blob).html()再走一遍解析与序列化,作为最终的 canonical 形式。
净化结果sanitizeHtml(text, sanitizeOptions)之后也会再跑一遍normalize,确保双方处于同一规范化口径再比较。
快速定位首个不一致点
不一致时,findFirstMismatch() 以chunkSize = 100为步长做整块前缀比较,快速跳过相同前缀,定位第一处分歧的偏移量。peak() 再围绕该偏移前后各取zoomOut = 50字符的上下文,并用JSON.stringify包装(这样换行等控制字符会以\n等转义形式可见,便于在终端对照)。
诊断输出字段
一旦某页净化后发生变化,脚本向 stderr 打印如下八行(与 README 示例一致):
| 字段 | 含义 |
|---|---|
page/title | MediaWiki 页面名与标题,便于直接定位回 Wiki |
match | HTML 规范化后是否完全一致(text === sanitized) |
toText | 去掉全部标签后的纯文本是否一致——即净化是否改变了用户可见文字 |
text | 不一致点前后的原始 HTML 片段(JSON 转义) |
sanitized | 不一致点前后的净化后 HTML 片段 |
textToText | 原始 HTML 对应的纯文本片段 |
sanitizedToText | 净化后 HTML 对应的纯文本片段 |
toText与 HTML 比对是双层防线:即使标签被替换,只要最终可见文字不变(toText: true),影响通常只是样式层面的;反之若纯文本也变了,说明净化直接吞掉了内容,优先级更高。
示例输出解读
README 给出了一条真实诊断样例:某支持页面里,Wiki 作者用<nowiki>包裹了一段 URL 字面量。净化后<nowiki>标签被转义成<nowiki>...</nowiki>,于是:
match: false、toText: false——HTML 与可见文本都发生了变化;- 对照
text与sanitized两行,能看到分歧恰好落在<nowiki>处; - 对照
textToText与sanitizedToText两行,还能直观看到转义导致纯文本在</nowiki>位置提前“截断”成<nowiki>...的样子。
README 特别提示Note the hidden/escaped <nowiki> element.——这个被隐藏的转义标签正是问题根源。整体体验上,你看到的是“HTML 并排比对 + 纯文本 diff”两层信息(原文:you will see a plain-text diff),可以快速判断是配置漏放行了某个标签,还是 Wiki 标记本身依赖了不应出现的元素。
为什么不用 MediaWiki 的 bulk export
README 专门说明了数据源选择的原因(原文):MediaWiki 有批量导出(bulk export)功能,但它的 HTML 转义行为与 Web 服务实际使用的 parse API 不一致——bulk export 不会转义所有占位符形式的 HTML 样元素,例如<project-id或<document goes here>这类未闭合的伪标签。若用 bulk export 的数据做校验,会产生与线上真实数据源(parse API,即action=parse)不同的伪差异,因此脚本统一走与 Web 渲染链路同源的 parse API,保证“测的就是线上吃的数据”。
小结与相关文件
checkSanitize是一个典型的“配置回归检查”工具:以 Wiki parse API 为唯一数据源,用与线上一致的sanitizeOptions做净化,通过 normalize + 双层 diff 精确定位净化副作用,并借助本地缓存与 ScriptLog 让巡检可重复、可追溯。适合在修改 sanitize 配置、Wiki 模板结构变化时运行一遍全站巡检。
关键文件索引:
| 文件 | 作用 |
|---|---|
| README.md | 用法、bulk export 局限与示例输出 |
| index.mjs | 入口:参数校验、逐页巡检主循环 |
| checkSanitizeOptions.mjs | normalize、sanitizeHtml 比对、诊断打印 |
| scrape.mjs | parse API / allpages 查询、缓存与翻页 |
| ScriptRunner.mjs | 脚本运行日志(ScriptLog)基础设施 |
| libraries/fetch-utils/index.js | fetchString/fetchJson/RequestFailedError |
适用前提:该脚本面向开发环境(源码注释明确标注 only used in dev env),需要一个可达的 MediaWiki Learn 站点地址;缓存目录(services/web/data/learnPages、services/web/data/dumpFolder)为运行产物,首次运行会自动创建。
【免费下载链接】overleafA web-based collaborative LaTeX editor项目地址: https://gitcode.com/GitHub_Trending/ov/overleaf
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考