简介:eWebEditor V12.0 for ASP 多语言商业版是一套面向WEB开发人员与网站内容管理人员的可视化富文本编辑器资源包,主要用于在ASP网站后台把传统多行文本输入框替换为所见即所得编辑区域,让最终用户可直接发布HTML格式内容,并支持内置Word导入等商业版能力。压缩包共753个文件,约16.99MB,以442个gif图标、131个css样式、65个jpg图片及56个htm示例页面为主,同时包含29个asp服务端脚本、22个js脚本和4个exe程序,覆盖配置、上传、浏览、登录等基础模块,便于按现有站点结构进行部署与二次调整。现有442人学习,说明其在ASP富文本编辑场景中仍具参考价值。对需要为CMS、企业站或老旧ASP后台补充可视化编辑能力的开发者而言,可直接获得较完整的皮肤素材、页面示例与服务端代码,用于快速集成、界面适配和功能验证,减少从零搭建编辑器的时间。
1. 一个 .rar 里的老编辑器,为什么还值得花半天读配置
需求单上只有一句「后台编辑器老是报错,看看能不能换」,等你远程登上那台 Windows Server,才发现整个内容后台是十几年前写的 ASP,编辑器是 eWebEditor V12.0 for ASP 多语言商业版,压缩包里还躺着一份当年的部署说明。这不是新项目选型,而是遗留系统维护:你要判断它能不能继续用、语言包怎么加、上传口子怎么堵、什么时候该把它换掉。这篇笔记面向正在维护老 ASP 站点的后端和运维,把接入链路、多语言配置、三个必调参数和几类高频故障一次讲清楚,让你拿到包之后不至于从目录结构开始猜。
2. 接入链路拆解:从 iframe 加载到 ASP 落地入库
先把这套东西的运行模型说清楚,否则后面所有报错都像玄学。老一代 ASP 富文本编辑器几乎都是同一个套路:主页面不直接渲染编辑区,而是用一个 iframe 指向编辑器自己的入口页,入口页里再输出一套按钮条加一个可编辑区域,用户敲的内容留在这个 iframe 的文档里,提交时由编辑器把 HTML 写回父页面的隐藏域。理解这条链路,你就知道该在哪一层排查:父页面、iframe 入口、上传处理页、还有入库那一步。
2.1 先摸清四块组成,别一上来读全量源码
一个完整的发行目录,通常可以切成四块。第一块是入口页,负责接收id、style、language这类查询参数,拼出 iframe 内部的 HTML 骨架;第二块是样式配置,决定工具栏上有哪些按钮、上传走哪个地址、允许哪些扩展名;第三块是上传处理页,接收文件并落盘;第四块是语言包目录,一个语种一个文件。十几年的包往往还夹带了示例页、旧版备份、编辑器皮肤,全读一遍没有必要。
我一般的顺序是:先打开示例页跑通一次「编辑—提交—入库—回显」的完整闭环,再回头逐项改配置。因为这个链路里任何一环出问题,表现形式都一样——「保存不了」。先把闭环跑通,等于先给自己留了一颗后悔药,后面改配置改坏了能立刻对比回来。
提示:不同发行批次的目录命名和配置文件名有差异,下面的路径和参数名按常见约定写,实际以你手里那份入口页和样式配置为准。不要照抄文件名,要照抄判断方法。
2.2 最小接入:一个 ASP 表单页的完整片段
下面这段是父页面的骨架,把编辑器挂上去,并保证提交时隐藏域里有内容。注意隐藏域的name必须和 iframe 地址里的id参数完全一致,这是最容易写错的一处。
<!-- edit.asp:编辑页,编辑器以 iframe 方式挂载 --> <form id="frmNews" method="post" action="save.asp"> <!-- 隐藏域的名字要和下面 iframe 的 id 参数一致 --> <input type="hidden" name="content1" id="content1" value="<%=Server.HTMLEncode(sContent) %>"> <iframe id="edtFrame" src="ewebeditor/ewebeditor.asp?id=content1&style=standard&language=zh-cn" width="100%" height="420" frameborder="0" scrolling="no"></iframe> <input type="submit" value="保存"> </form> <script> // 编辑器通常在自身内部接管 submit 时的内容同步; // 一旦你换成自定义按钮或 ajax 提交,就必须手动同步一次 document.getElementById("frmNews").onsubmit = function () { var frame = document.getElementById("edtFrame"); var box = document.getElementById("content1"); // 取内容的入口函数名各版本不同,先在其入口页里搜 getHTML / getContent 一类关键字 if (frame.contentWindow && frame.contentWindow.getEditorContent) { box.value = frame.contentWindow.getEditorContent(); } return true; }; </script>三个参数逐个说。id是内容回写的目标,它既可以是隐藏域的 name,也可以是某个 textarea 的 id,取决于版本实现;写错的表现是「编辑区有内容,提交后数据库是空的」。style是工具栏样式的名字,对应样式配置里的一套按钮集,你可以为编辑和审核各配一套。language是语言包标识,必须和语言目录下的文件名对应,写一个不存在的值通常不会报错,只是静默回退到默认语种——这就是很多人以为「多语言版不生效」的真实原因。
还有一处细节:value里的历史内容必须做Server.HTMLEncode。老代码里常见直接<%=sContent%>塞进 value 属性,内容里只要有一个双引号,属性就被截断,回显出来的编辑区只剩半句话,而在数据库里内容其实是完整的。这类「看起来丢了内容」的问题,九成出在回显这一侧。
2.3 提交与入库:转义还原、字段类型、长字段顺序
内容从表单上来之后,不能直接进库。老编辑器为了兼容 IE 的 range 操作,习惯把内容里的双引号做反斜杠转义,也就是"变成\",你如果原样存进去,前台渲染时就会看到多出来的反斜杠。这个坑非常典型,属于血泪经验级别的坑,处理成本极低但不处理就一直在。
<% ' 清洗编辑器提交上来的 HTML:还原转义 + 去掉会截断内容的控制字符 Function CleanEditorHtml(s) Dim t t = s & "" ' 1) 还原 \" 为 "(Chr(92) 是反斜杠,避免在 VBScript 里写转义地狱) t = Replace(t, Chr(92) & """", """") ' 2) NUL 字符会让部分 ADO 驱动在写入时截断后续内容 t = Replace(t, Chr(0), "") ' 3) 单引号不在这里处理,交给参数化查询 CleanEditorHtml = t End Function Dim sBody, cmd sBody = CleanEditorHtml(Request.Form("content1")) If Len(sBody) > 60000 Then Response.Status = "413 Payload Too Large" Response.Write "正文超出单篇上限" Response.End End If Set cmd = Server.CreateObject("ADODB.Command") cmd.ActiveConnection = conn cmd.CommandText = "UPDATE t_news SET body=? WHERE id=?" cmd.CommandType = 1 ' adLongVarWChar = 203,长度给足,不要用 8000 卡死自己 cmd.Parameters.Append cmd.CreateParameter("body", 203, 1, Len(sBody) + 1, sBody) cmd.Parameters.Append cmd.CreateParameter("id", 3, 1, , CLng(Request.Form("id"))) cmd.Execute %>逻辑说明:第一步做转义还原,顺序必须在入库之前,不能在渲染时补,否则同一个字段你会在不同页面各修一次;第二步去掉 NUL,这个字符肉眼看不见,但会让某些驱动在写入时把后面的内容整段丢掉,属于黑匣子型故障;第三步用参数化查询,而不是拼 SQL 字符串再手工替换单引号,因为编辑器产出的 HTML 里带单引号是常态,手工替换迟早漏一处。
参数说明:203对应adLongVarWChar,用于 ntext 或 nvarchar(max) 字段;第三个参数1是adParamInput;长度参数给Len(sBody)+1,给 0 在某些驱动上会被当成空串。如果你的库是老版本 Access,那条「长字段参数必须最后 Append」的规则一定要记住——memo 类型字段一旦排在参数集合中间,写入结果会被静默截断成 255 字节,而且不报错。这个坑我见过不止一次,排查时先看参数顺序,能省半天。
字段类型这块,能用nvarchar(max)就别用ntext,前者支持Replace和LEN,后者在 T-SQL 里很多字符串函数直接报错,后期做内容清洗会非常难受。长度上限建议在应用层显式判断,而不是指望数据库报错——超过上限时数据库给的错误信息通常很含糊,前端看到的就是一个 500。
3. 多语言商业版的落地:语言包结构、切换与回退兜底
「多语言商业版」这个说法容易被误解成编辑器会自动翻译,其实它做的是 UI 文案的多语种化:按钮的 title、右键菜单、上传对话框里的提示文字,都从语言包取值。正文内容本身仍然是用户输入的,编辑器不会碰。所以这一章要解决的问题是:语言包怎么组织、怎么按用户选、新增一个语种要动哪几处。
3.1 语言包目录结构与键值约定
常见的做法是一个语言目录加若干文件,一个文件一个语种,内容是一组键值对。键名在同一个发行版里是固定的,值可以随便改。判断键名是否完整,最省事的办法是把两个语种文件并排打开,看键的数量对不对得上。
| 元素 | 常见形式 | 说明 |
|---|---|---|
| 目录 | language/或lang/ | 与入口页同级或在其下一级 |
| 文件名 | zh-cn.js、en.js、zh-tw.js | 必须与调用时传的language参数一致 |
| 键 | Bold、InsertImage、Upload | 不要改键名,改了就对不上 |
| 值 | 加粗、插入图片 | 可以按业务口径调整措辞 |
| 编码 | UTF-8 或 GB2312 | 必须与页面声明一致,见第 5 章 |
值是可以按业务改的。比如「插入图片」在内部系统里改成「插入图片(单张不超过 2MB)」,比在页面上另加一行提示更省事,用户点开对话框之前就知道限制。这种小改动性价比很高,值得做。
要注意键名不要重命名。有些人为了一眼看懂,把Bold改成加粗,本地跑着没问题,等到升级或补丁覆盖时整个语言文件被还原,改动全没了,而且不报错。改值不改键,是这类配置文件的通用纪律。
3.2 按会话和浏览器语言自动切换
用户不该每次都手动选语言。合理的优先级是:显式 URL 参数 > 会话缓存 > 浏览器Accept-Language> 默认语种。下面这段是入口页里的选择逻辑,重点是那个白名单——language参数最终会影响文件路径的拼接,不做白名单就是给自己开一个目录穿越的口子。
<% ' 语言选择:显式参数 > 会话 > 浏览器偏好 > 默认 Dim sLang, sAllowed, sAcc ' 白名单必须与 language 目录下的文件名一一对应,新增语种时同步加进来 sAllowed = "zh-cn,zh-tw,en,ja" sLang = LCase(Trim(Request.QueryString("lang") & "")) If InStr("," & sAllowed & ",", "," & sLang & ",") = 0 Then sLang = "" ' 不在白名单直接丢弃 If sLang = "" Then sLang = LCase(Trim(Session("editorLang") & "")) If sLang = "" Then sAcc = LCase(Request.ServerVariables("HTTP_ACCEPT_LANGUAGE") & "") ' 只做粗匹配,不追求完整的 RFC 语言协商 If InStr(sAcc, "zh-tw") > 0 Or InStr(sAcc, "zh-hk") > 0 Then sLang = "zh-tw" ElseIf InStr(sAcc, "zh") > 0 Then sLang = "zh-cn" ElseIf InStr(sAcc, "ja") > 0 Then sLang = "ja" Else sLang = "en" End If End If Session("editorLang") = sLang ' 缓存,避免每次请求都算一遍 %>逻辑说明:先LCase和Trim归一化,再走白名单校验,不在名单里的值一律置空而不是报错——报错会让攻击者知道参数被使用了。会话缓存这一步很关键,它让用户在页面间跳转时语言保持一致,否则每进一个页面都会根据浏览器偏好重新判断,出现「一会儿中文一会儿英文」的割裂感。
参数说明:sAllowed这个字符串既是白名单也是新增语种的登记表,加语种时两处要一起改(目录和这里)。粗匹配只覆盖常见的几个前缀,别指望它能正确处理zh-Hant-TW这种带脚本子标签的写法;真要做细,就把Accept-Language按逗号切开、去掉;q=权重、逐个比对,但那通常是过度设计,内部系统用不上。
3.3 新增一个语种的四步落地
第一步,复制一份最接近的现有语言文件,改名成目标标识,比如ko.js。第二步,只改值,键名一个不动,改完把新旧两个文件并排比对键数量。第三步,把标识加进白名单字符串,并在入口页的粗匹配里补一个分支。第四步,用?lang=xx强制走一遍所有带弹窗的功能——上传、插入表格、插入链接、全屏,这四个是文案最密集的地方,缺键通常在这几处先暴露。
缺键的表现是显示成键名本身,比如按钮上写着InsertImage,或者弹窗标题是空的。多数实现不会为此抛异常,所以不能靠日志发现,只能靠人眼过一遍。如果语种多,写个几十行的脚本扫一遍各文件的键集合做差集,比人工核对靠谱得多。
注意:语言文件和入口页的编码必须一致。GB2312 的语言文件被 UTF-8 页面加载,通常表现为整片中文变问号或方块,而不是报错。改编码时用「另存为」而不是「改声明」。
4. 三个必调参数:工具栏、过滤白名单、上传目录
配置项看起来很多,真正影响安全和可用性的就三处:工具栏给谁哪些按钮、提交的 HTML 允许保留什么、上传往哪儿落。前面两项决定内容质量,第三项决定你会不会在某个周一早上发现站点目录里多了几个不认识的脚本文件。
4.1 工具栏按角色裁剪,别给所有人全套按钮
工具栏样式本质是一个按钮清单。给内容编辑的样式里塞上「插入脚本」「插入 iframe」「查看源码」,等于把 XSS 的入口直接交出去。我的做法是按角色拆成两三套样式,入口页根据当前登录用户的角色传不同的style参数。
| 样式名 | 面向角色 | 保留按钮 | 去掉的 |
|---|---|---|---|
basic | 普通投稿 | 加粗、斜体、列表、链接、撤销 | 源码、脚本、iframe、样式表 |
standard | 内容编辑 | 上面全部 + 图片、表格、字号 | 源码、脚本、iframe |
full | 站点管理员 | 全部 | 无 |
拆样式的时候有个细节:不要只在前端隐藏按钮。老实现里按钮清单是输出到页面上的,用户按 F12 删掉一段判断就能把隐藏的功能调出来。所以样式选择必须在服务端根据会话里的角色决定,而不是让前端传一个style参数上来。
第二件事是提交时的服务端过滤。不要指望编辑器的「过滤」开关能兜住所有情况,它是给正常编辑行为做格式清理的,不是安全边界。真正的白名单要放在入库之前,用一份允许标签和允许属性的清单过一遍。
<% ' 极简白名单:只保留排版必需标签,属性只留 href/src/alt/width/height ' 生产环境建议用成熟的 HTML 清洗库,这里给的是思路和判断点 Function WhitelistHtml(s) Dim re, tags, i tags = Array("p","br","strong","b","em","i","ul","ol","li","a","img","table","tr","td","th","h3","h4","blockquote") Set re = New RegExp re.IgnoreCase = True re.Global = True ' 先把不在白名单里的标签整体剥掉(保留内部文本) re.Pattern = "<\/?([a-zA-Z][a-zA-Z0-9]*)[^>]*>" WhitelistHtml = re.Replace(s, "") ' 占位:真实实现需按 tag 逐个判断后再决定替换 Set re = Nothing End Function %>上面这段是示意,不打算让你直接上生产——正则清洗 HTML 天然不可靠,属性里的javascript:、onerror=、大小写混写、畸形嵌套都能绕过。真实的判断点是三条:一是白名单必须按「标签 + 属性 + 属性值协议」三层校验,href和src要检查是不是http/https/相对路径;二是清洗放在入库前做一次、输出前再做一次,两次的成本远低于一次事故;三是能用现成的清洗组件就别自己写,自己写的那份迟早会被绕过。
4.2 过滤开关与白名单的边界在哪
编辑器自带的过滤一般做两件事:把 Word 粘贴带进来的垃圾标签压平、把不认识的标签转义成可见文本。它对付的是「内容难看」,不是「内容危险」。所以你会遇到一种情况:编辑器里看着一切正常,入库的内容里却带着一段onclick,因为那段是粘贴进来的,过滤没覆盖到。
判断边界的方法很直接:把编辑器自带的过滤当成排版工具,把服务端白名单当成安全工具,两者不要互相替代。前者可以随时关掉不影响安全,后者一旦关掉就什么都不剩。
4.3 上传目录与大小限制:三个必调的地方
上传这块要动三处:落盘路径、扩展名判定、目录的执行权限。三处缺一处都不算配好。
<% ' 上传落盘前的服务端校验,不信任任何前端传来的扩展名和文件名 Dim sExt, arrAllow, i, bOK, sNewName, sGuid sExt = LCase(Mid(sFileName, InStrRev(sFileName, ".") + 1)) arrAllow = Array("jpg", "jpeg", "png", "gif", "webp") bOK = False For i = 0 To UBound(arrAllow) If sExt = arrAllow(i) Then bOK = True Next If Not bOK Then Response.Status = "403 Forbidden" Response.End End If ' 文件名不复用客户端传来的名字,避免 .. / 空格 / 中文截断 带来的路径问题 sGuid = CreateObject("Scriptlet.TypeLib").GUID sNewName = Replace(Replace(Replace(sGuid, "{", ""), "}", ""), "-", "") & "." & sExt ' 落盘目录:站点根目录之外的独立盘符或独立虚拟目录, ' 并在 IIS 里对该目录关闭「脚本和可执行文件」权限 %>逻辑说明:扩展名判定要基于「最后一个点之后」的字符串,而不是用InStr找第一个点——a.asp.jpg这种双扩展名是经典手法。文件名重生成的意义不只是防重名,更是切断「客户端控制落盘路径」这条链。最后一条最容易被忽略:目录建在站点里、又允许脚本执行,那么即使扩展名校验做对了,某些环境下的解析差异仍然可能让文件被当成脚本执行。
三处参数建议这样定:单文件大小按业务定,图片类 2MB、附件类 10MB 是个合理起点;单篇正文长度上限设在应用层,别等数据库报错;上传目录按日期分子目录,避免单目录文件数过万之后列目录变慢。
提示:上传接口的地址不要写死在前端配置里就完事。老实现允许通过参数指定上传处理页的路径,这个参数必须由服务端固定,不能让请求方决定。
5. 避坑排查:上传绕过、内容截断、内核兼容
这一章按「现象 → 原因 → 解决」来写,都是我在维护这类老系统时反复遇到的。有些问题的表现和原因隔得很远,第一眼完全看不出关联,所以排查顺序比经验更重要。
5.1 上传类:白名单改了,文件还是传上来了
现象:在配置里把允许扩展名改成只有图片,测试传一个.txt被拦住了,但过两天巡检时在目录里发现了.asa文件。
原因:老实现的上传处理页会从请求参数里读扩展名和保存路径,配置里的白名单只作用于前端的上传对话框。也就是说,直接构造一个 POST 请求,绕开对话框,配置根本不参与判定。这类问题不是配置错误,是架构层面的信任边界放错了位置。
解决:把扩展名判定、文件大小判定、落盘路径全部在服务端重做一遍,配置里的白名单只当第一道粗筛。更进一步,上传目录单独挂一个虚拟目录,在 IIS 里把该目录的执行权限设为「无」,只留静态文件读取。最后加一条巡检:每天扫一遍上传目录里非白名单扩展名的文件,有就告警。这条巡检做了之后,同类问题基本不会再到「周一早上才发现」的程度。
5.2 内容类:提交成功,正文却只剩前半段
现象:编辑一篇长文,点保存,提示成功,回来看内容被砍掉一半,砍的位置不固定,和字符数有关但又不是严格的字数上限。
原因:两层。第一层是 IIS 对请求实体的长度限制,默认值在 200KB 上下(不同版本取值有差异,以实际配置为准),超过之后表单里后面的字段根本到不了 ASP。第二层是入库时的字段类型或参数长度,用了nvarchar(4000)或者把长字段参数排在中间,写入被静默截断。
解决:先看 IIS 的请求实体上限,按需调大,同时把这个值和应用层的正文上限对齐——应用层上限高于 IIS 上限时,用户会看到「提示成功但内容不全」,这比直接报错糟糕得多。然后检查字段类型,换成nvarchar(max);如果是 Access,确认 memo 字段的参数是最后一个 Append。最后在保存后加一次长度校验:读回来的长度和提交长度不一致就记日志,这行日志能帮你提前发现所有静默截断。
5.3 渲染类:Chrome 下工具栏点了没反应
现象:编辑区能显示、能输入文字,但点「加粗」「插入图片」毫无反应,控制台没有明显报错,或者只有一句关于document.selection的提示。用 IE 打开一切正常。
原因:这一代编辑器依赖document.selection.createRange()这类只有 IE 内核才提供的接口来做光标位置和选区操作。换到 Chromium 内核后这些接口不存在,相关代码直接抛异常,但因为异常发生在事件回调里,表现就是「按钮没反应」。
解决:先确认你手上那份发行包有没有针对现代内核的补丁分支,有就优先用补丁,不要自己改核心脚本——那部分代码耦合度很高,改一处崩三处。没有补丁的情况下,可行路径是把编辑区退化成 textarea 加一个「预览」按钮,虽然体验下降,但至少不会让编辑流程断掉。这条也是我判断「该不该继续留着它」的分水岭:如果连基础输入都不稳,任何加固都是白费力气。
5.4 部署类:UTF-8 页面里中文全变问号
现象:本地测试一切正常,部署到另一台服务器后,编辑器界面和提交的中文都变成问号,数据库里存的也是问号。
原因:编码链路上有一处不一致。ASP 页面用CODEPAGE声明,语言文件自己带编码,数据库字段的排序规则(collation)又是另一套,上传处理页可能还是第三套。任何一处掉队,中文就会在某个环节被替换成问号,而且这个过程不可逆——问号存进去就恢复不回来了。
解决:把整条链路统一到 UTF-8:页面声明、表单页的charset、语言文件另存为 UTF-8、数据库字段用nvarchar并在连接串里明确字符集。改完之后做一次全链路回归:输入中文、保存、重新打开编辑、前台渲染、导出,五个环节都看一遍。已经变成问号的历史数据没有后悔药,只能从备份或原始稿件重录,所以这类改动一定要在低峰期做,并且提前备好回滚方案。
6. 加固与退出:把「能跑」变成「敢留」
维护到这一步,你需要给出一句结论:这套东西是继续用半年,还是三个月内换掉。我一般用一张检查表来支撑这个判断,而不是凭感觉。表格里每一项都是可以当场验证的,验证不过的项就是你要在结论里写清楚的风险点。
| 检查项 | 通过标准 | 不通过的处理 |
|---|---|---|
| 上传目录执行权限 | IIS 中该目录脚本执行已关闭 | 立即改配置,当天完成 |
| 服务端扩展名白名单 | 绕过前端直接 POST 仍被拦 | 补服务端校验,加目录巡检 |
| 正文长度链路 | 应用层上限 ≤ IIS 请求上限 | 两者对齐,加长度回读校验 |
| HTML 白名单 | 粘贴带事件属性的内容被清洗 | 引入成熟清洗组件,入出各一次 |
| 多语言参数 | 非法lang值被丢弃而非拼路径 | 加白名单,改完回归四个弹窗 |
| 浏览器兼容 | 目标浏览器下基础编辑可用 | 评估补丁,无补丁则降级为 textarea |
迁移这件事,我不建议一刀切。可行的节奏是三步:先把编辑入口做成可切换的,用配置或灰度名单决定走老编辑器还是新编辑器,这一步不改数据结构,风险最低;然后让新内容走新编辑器,老内容保持只读展示,避免回写时被新编辑器的清洗规则改得面目全非;最后才是批量重存历史内容,而且必须先在副本库上跑一遍,统计有多少篇会被清洗改动、改动幅度多大,再决定是全量跑还是只跑访问量靠前的部分。
有个细节值得单独说:新编辑器产出的 HTML 结构通常和老的不一样,如果前台模板里有针对老结构的 CSS 选择器,视觉上会出现莫名其妙的错位。所以切换前先抓二十篇有代表性的内容,在新编辑器里打开再保存,然后对比前台渲染截图。这一步花不了两小时,但能挡掉上线后最难定位的一类问题。
我自己的习惯是,任何一次对这类老系统的改动,都在改动前留一份「能跑」的完整快照,包括配置文件、语言包、上传目录结构和一份样本数据。老系统的依赖关系往往是隐式的,你改 A 的时候不知道 B 依赖 A 的某个副作用,出问题时唯一的救命稻草就是那份快照。这套流程不优雅,但它在过去几年里救过我很多次。希望帮到你。
本文还有配套的精品资源,点击获取