简介:eWebEditor V12.0 for ASP 多语言商业版是一套面向Web开发者的可视化富文本编辑器程序包,主要用于在ASP网站后台将传统多行文本输入框替换为所见即所得编辑区域,便于内容管理系统完成网页内容发布与HTML排版。其内置Word导入、多语言界面等能力,适合需要快速集成后台编辑功能、研究编辑器皮肤与上传浏览机制的技术人员参考。压缩包共753个文件,约16.99MB,以gif、jpg、css、htm、asp、js为主,另有少量exe、swf、txt等,分别对应界面图标、样式表、页面模板、服务端脚本与前端交互资源,目录组织较完整。已有442人学习,可为ASP项目中的富文本编辑集成、样式定制、上传与登录模块拆解提供现成素材和实现思路,也便于开发者对照理解传统CMS后台编辑器的资源构成。
1. 一个 .rar 里的富文本编辑器,为什么还有人在部署
前阵子有人问我:手上一个跑了十来年的 ASP 后台,编辑框还是最原始的 textarea,运营要粘 Word 排版、要插图、要改字号,能不能不动框架就补上。答案常常就落在一个压缩包上——eWebEditor V12.0 for ASP 多语言商业版。它是 ASP 年代装机量极大的可视化编辑器:服务端是几个 .asp 文件,前端是一堆 js 加皮肤,样式和按钮配置存在 .mdb 库里,语言包单独放一个目录,解压进 IIS 站点基本就能跑,不需要 .NET、不需要 Node、不需要重新编译。它解决的是“老站点要一个能配置、能换语言、上传可控、不和现有表单打架的编辑框”这个具体问题。适合两类人:一类是维护遗留 ASP 系统的,必须原地加固而不是重构;另一类是接手了别人十年前的后台,连源码注释都找不到,只能靠目录结构反推。不适合新项目,新项目直接上现代编辑器。
2. 解压之后的目录结构:先认文件,再往 IIS 里扔
拿到包的第一件事不是解压到站点根目录,而是先解压到一个临时目录,把结构看明白。eWebEditor 这类 ASP 编辑器的分发包,结构十几年没大变过:一个入口页、一个上传处理页、一堆皮肤和语言资源、一个存配置的库、一个后台管理目录、一个上传落地目录。认不清这几块,后面出的问题全是“玄学”——按钮不出来、图传上去 404、后台进不去,其实都是路径和权限的事。
2.1 六个必须认识的目录与文件
解压后大致是这样的层次(不同构建里的文件名可能略有出入,以你手上那份为准,但职责划分是一致的):
| 路径 | 职责 | 上线时怎么处理 |
|---|---|---|
ewebeditor.asp | 编辑器入口页,接收id、style等参数,输出 iframe 内部的编辑界面 | 保留,站点内引用 |
editor.htm/editor.js | 前端编辑器主体、工具栏、快捷键 | 保留,不要手改 |
style/ | 皮肤目录,每个皮肤一套 css 和按钮图 | 只留要用的 1~2 套,其余删掉减小体积 |
lang/ | 多语言包,按语言分文件 | 只留中文和英文,其余删掉 |
db/或data/ | 存样式、按钮、语言配置的 .mdb 库 | 保留但必须禁止 URL 直接访问 |
uploadfile/或upload/ | 上传文件的落地目录,通常按日期建子目录 | 保留,必须单独设权限、禁脚本执行 |
admin_*.asp/admin/ | 后台管理入口,改样式、改上传限制、改语言 | 上线后改名或整目录限制 IP |
这张表里最容易出事的是后两行。上传目录和配置库如果和编辑器页面同源同目录、还能被浏览器直接访问,等于把“写文件”和“读配置”两个口子同时敞开。血泪经验是:先把db/和uploadfile/的位置想清楚,再决定站点目录怎么切。
2.2 应用池必须开 32 位:Jet 引擎只认 32 位进程
这是新手最容易翻车的一条。老版本 ASP 编辑器的配置库是.mdb格式,服务端通过 Jet 引擎读取,而 32 位的 Jet 提供程序在 64 位进程里是加载不起来的。默认新建的应用池是 64 位、无托管代码,页面一打开就是ADODB.Connection 错误 '800a0e7a' 未找到提供程序,或者干脆80004005。解决办法不是换连接字符串,而是把应用池的“启用 32 位应用程序”打开。
# 以管理员身份运行,注册应用池并强制 32 位 # 老 ASP 站点不要托管运行时,选“无托管代码” C:\Windows\System32\inetsrv\appcmd.exe add apppool /name:"legacy-asp" /managedRuntimeVersion:"" /enable32BitAppOnWin64:true # 建站点并挂到这个池上(路径换成你自己的) C:\Windows\System32\inetsrv\appcmd.exe add site /name:"legacy-web" /physicalPath:"D:\sites\legacy" /bindings:"http/*:8080:" C:\Windows\System32\inetsrv\appcmd.exe set app "legacy-web/" /applicationPool:"legacy-asp"/managedRuntimeVersion:""这个空串的意思是“不加载任何 .NET 运行时”,对纯 ASP 站点来说这是最省事的配置,能避免一堆和web.config继承相关的报错。/enable32BitAppOnWin64:true是让工作进程以 32 位模式启动,代价是单进程内存上限降到 2GB 左右,但对一个富文本编辑器来说完全够用。注意:同一个应用池不能既跑 32 位又跑 64 位的组件,如果你站点里还有别的 64 位依赖,就得单独拆一个池给编辑器目录。
2.3 打开父路径、放开请求体:三行配置解决一半“传不上去”
经典 ASP 里大量使用<!--#include file="../inc/conn.asp"-->这种相对父级路径的写法,IIS 默认是禁止的,报错是“Active Server Pages 错误 'ASP 0131' 不允许父路径”。同时编辑器的表单提交内容比普通表单大得多,一个带 Word 粘贴内容的 HTML 轻松几十上百 KB,超过默认上限就会被直接掐断,前端表现是“点保存没反应”或者页面 404.13。
# 允许父级路径(按站点生效,不要全局开) C:\Windows\System32\inetsrv\appcmd.exe set config "legacy-web" -section:system.webServer/asp /enableParentPaths:"true" /commit:apphost # ASP 层面的请求体上限,单位字节,这里给 20MB C:\Windows\System32\inetsrv\appcmd.exe set config "legacy-web" -section:system.webServer/asp /limits.maxRequestEntityAllowed:"20971520" /commit:apphost # 请求筛选层面的上限,必须一起改,否则先被它拦下 C:\Windows\System32\inetsrv\appcmd.exe set config "legacy-web" -section:system.webServer/security/requestFiltering /requestLimits.maxAllowedContentLength:"20971520" /commit:apphost三个参数要成组理解:enableParentPaths决定 include 能不能往上跳;limits.maxRequestEntityAllowed是 ASP 引擎愿意读多少请求体;requestLimits.maxAllowedContentLength是请求筛选在更外层就拦下的长度。很多人只改了中间一个,前端还是传不上去,就是因为外层那道闸没开——这是最典型的“改了半天没生效”现场。数值别一步给到 100MB,按实际需要给,编辑器一次提交超过 20MB 的内容本身就说明业务流程有问题。
3. 接进现有 ASP 表单:一个隐藏域加一个 iframe
编辑器本身不存数据,它只是一个“把 HTML 塞进表单字段”的中间层。所以接入的核心动作只有两个:页面渲染时用 iframe 加载编辑器并把已有内容灌进去,提交时把 iframe 里编辑后的 HTML 回写到表单字段。搞懂这两步,剩下全是参数。
3.1 最小接入代码:隐藏 textarea + iframe
<!-- 编辑页:隐藏域承接内容,iframe 承载可视化编辑区 --> <form id="frmNews" method="post" action="save.asp" onsubmit="return syncEditor();"> <!-- 用 Server.HTMLEncode 防止存量内容提前破坏 DOM --> <textarea name="content" id="content" style="display:none"><%=Server.HTMLEncode(rs("content") & "")%></textarea> <!-- id 参数要和 textarea 的 id 一致;style 取后台配置里的样式名 --> <iframe id="eWebEditor1" src="/eweb/ewebeditor.asp?id=content&style=standard600&language=zh-cn" frameborder="0" scrolling="no" width="100%" height="420"></iframe> <input type="submit" value="保存" /> </form>这段代码里有三个参数决定成败。id=content是编辑器要绑定的字段名,必须和 textarea 的id完全一致,写错了编辑器能显示但取不到值;style=standard600是后台配置里定义的一套按钮组合,名字对不上会退化成极简工具栏甚至空白;language=zh-cn指向语言包,多语言版就是靠这个参数切界面文案的。iframe 的height建议给足 400 以上,太矮会让工具栏换行,视觉上像“按钮丢了”。
textarea里那段Server.HTMLEncode不能省。存量内容里只要有<或者",直接输出就会把 textarea 提前闭合,表现为“一进编辑页后面全乱了”。
3.2 提交时回写内容:getHTML 的两种写法与降级方案
编辑器在 iframe 内部,主页面拿不到它的 DOM,必须走跨 frame 调用。不同构建暴露的方法名不统一,常见的是getHTML(),也有getHtml()或Editor.getValue()的变体。稳妥的写法是带降级判断,而不是赌一个方法名:
function syncEditor() { var frame = document.getElementById("eWebEditor1"); var win = frame.contentWindow || frame.contentDocument.parentWindow; var html = ""; // 依次尝试常见方法名,命中即用 if (typeof win.getHTML === "function") { html = win.getHTML(); } else if (typeof win.getHtml === "function") { html = win.getHtml(); } else if (win.eWebEditor && typeof win.eWebEditor.getHTML === "function") { html = win.eWebEditor.getHTML(); } else { // 兜底:直接取编辑区 innerHTML,会丢掉部分格式化,但不会提交空内容 var body = win.document.getElementById("editorBody") || win.document.body; html = body ? body.innerHTML : ""; } if (!html || html.replace(/<[^>]*>/g, "").trim() === "") { alert("内容不能为空"); return false; } document.getElementById("content").value = html; return true; }逻辑上分三层:先按方法名探测,这是最干净的路径;探测失败再找编辑器实例对象,有些构建把方法挂在实例上而不是 window 上;最后兜底直接读编辑区innerHTML,牺牲一点格式保真度换“绝不提交空内容”。那个空内容判断也必要——编辑器里的空状态往往是一串<p><br></p>,如果只判html === "",用户能提交一篇空白稿件,这是运营侧最常见的投诉。
参数上还有一个容易忽略的点:syncEditor必须绑在form的onsubmit上并且return false能阻断提交。绑在按钮的onclick上,用户按回车提交就绕过去了,内容直接丢失。
3.3 多语言版怎么切:语言包、CodePage 与乱码的三方对齐
“多语言商业版”的价值在lang/目录和后台的语言配置表:界面按钮的提示文字、右键菜单、上传对话框文案都从这里取。切换方式一般有两种,URL 传language参数,或者在后台给样式绑定默认语言。但真正让多语言翻车的不是语言包,是编码。
老版本编辑器的静态资源(js、css、语言包)多是 GB2312 编码保存的。如果你的站点页面是 UTF-8,就会出现“页面中文正常、编辑器按钮提示全是问号”的诡异现象。原因是浏览器按父页面的 charset 去解码 iframe 里的资源,而文件本身是 GB2312。三种处理方式,按成本从低到高:
- 保持整站 GB2312。在页面顶部写
<%@ CODEPAGE=936 %>,并设置Response.Charset="gb2312"、Session.CodePage=936。存量系统改动最小。 - 只让编辑器目录保持 GB2312。给
eweb/单独加一层配置,让该目录下的响应头带charset=gb2312,主站继续 UTF-8。 - 把语言包和 js 全部转成 UTF-8 并改
ewebeditor.asp的 CODEPAGE。工作量最大,但以后不再有新坑。
我一般选第二种。它把“脏”隔离在一个目录里,主站该用 UTF-8 继续用 UTF-8。
<%' 编辑器目录下的入口页顶部:显式声明编码,避免继承主站设置 Response.CodePage = 936 Response.Charset = "gb2312" Session.CodePage = 936 %>这三行的顺序无所谓,但必须都在任何输出之前。少写Session.CodePage,保存到库里的中文就可能变成乱码——因为表单提交的解码用的是会话代码页,而不是响应代码页。这个细节很多人栽过:页面显示全对,入库全是问号。
4. 上传与 HTML 过滤:把“能传图”关进笼子里
富文本编辑器的风险面几乎全在上传和输出两头。上传这一头,攻击者想要的是“把可执行文件落到你的目录里”;输出这一头,想要的是“把脚本塞进别的用户的浏览器”。这两件事在老 ASP 系统里都必须自己补,不能指望编辑器默认配置。
4.1 上传白名单:扩展名、文件头、落地目录三道闸
只校验扩展名是不够的,a.asp;.jpg、a.jpg.asp、双扩展名截断这些手法在 IIS 上是有历史的。可靠的做法是三道闸一起上:扩展名白名单、文件头魔数校验、落地目录禁脚本。
<% ' 上传后校验:扩展名 + 文件头双白名单 Function CheckUpload(ByVal savePath, ByVal ext) Dim fso, ts, head, ok CheckUpload = False ext = LCase(ext) If InStr(",jpg,jpeg,png,gif,bmp,webp,", "," & ext & ",") = 0 Then Exit Function Set fso = Server.CreateObject("Scripting.FileSystemObject") If Not fso.FileExists(savePath) Then Exit Function ' 读前 8 字节判断真实类型,防止改扩展名绕过 Set ts = fso.OpenTextFile(savePath, 1, False, -2) ' -2 = TristateTrue? 二进制请改用 ADODB.Stream ' 实际项目里用 ADODB.Stream 读二进制头,这里给出判定表 ok = False Select Case ext Case "jpg", "jpeg" : ok = (head = "FFD8FF") Case "png" : ok = (head = "89504E47") Case "gif" : ok = (head = "47494638") End Select CheckUpload = ok ts.Close : Set ts = Nothing : Set fso = Nothing End Function %>上面这段是骨架,head的取值要用ADODB.Stream以二进制方式读出前几个字节再转十六进制比对。参数含义很直白:savePath是文件落地的绝对路径,ext是从文件名取的最后一段扩展名(先LCase再比对,别用Right截 3 位,jpeg会被截错)。只放行图片类型,是因为绝大多数业务只需要插图;如果业务真要传附件,就单独开一个目录、单独一套白名单,并且那个目录绝对不允许脚本执行。
4.2 上传目录禁脚本执行:一条 location 配置
这一步比任何代码校验都重要。就算前面漏了一个畸形文件,只要落地目录不能执行脚本,攻击面就归零。
<!-- 站点 web.config:对上传目录单独关掉脚本处理程序 --> <configuration> <location path="eweb/uploadfile"> <system.webServer> <!-- accessPolicy="Read" 表示只允许静态读取,不映射任何脚本处理器 --> <handlers accessPolicy="Read" /> <staticContent> <clientCache cacheControlMode="DisableCache" /> </staticContent> </system.webServer> </location> </configuration>accessPolicy="Read"是关键:它把这个目录下所有请求限制为静态文件读取,.asp、.asa、.cer一律不执行。配合 4.1 的白名单,形成“传不进来 + 传进来也跑不了”的双保险。另外把db/目录用hiddenSegments从 URL 层挡掉,防止有人直接下载配置库:
<system.webServer> <security> <requestFiltering> <hiddenSegments> <add segment="db" /> <add segment="admin" /> </hiddenSegments> </requestFiltering> </security> </system.webServer>hiddenSegments只挡 URL 访问,不影响服务端用Server.MapPath读取文件,所以编辑器自己还能正常读配置库。这是它比“改文件名”更优雅的地方——改文件名得同步改一堆 include,容易漏。
4.3 输出侧过滤:一个 VBScript 白名单函数
入库的 HTML 直接Response.Write出来,等于把 XSS 的大门敞开。严格的白名单过滤应该做在“存之前”,但老系统往往有大量存量数据,所以常见做法是存之前过滤一次、输出时再过滤一次。下面这个函数只保留排版类标签,其余一律转义:
<% ' 白名单过滤:保留排版标签,剥掉脚本与事件属性 Function SafeHtml(ByVal s) Dim re If IsNull(s) Then SafeHtml = "" : Exit Function s = s & "" ' 1) 先干掉最危险的整段结构 Set re = New RegExp re.IgnoreCase = True : re.Global = True re.Pattern = "<\s*(script|iframe|object|embed|style|link|meta)[\s\S]*?>[\s\S]*?<\s*/\s*\1\s*>" s = re.Replace(s, "") re.Pattern = "<\s*(script|iframe|object|embed|style|link|meta)[^>]*?/?>" s = re.Replace(s, "") ' 2) 剥掉 on* 事件属性与 javascript: 伪协议 re.Pattern = "\son[a-z]+\s*=\s*(""[^""]*""|'[^']*'|[^\s>]+)" s = re.Replace(s, "") re.Pattern = "javascript\s*:" s = re.Replace(s, "") ' 3) 白名单之外的行内样式一律清空(可选,按业务放宽) re.Pattern = "style\s*=\s*(""[^""]*""|'[^']*')" s = re.Replace(s, "") Set re = Nothing SafeHtml = s End Function %>这个函数的三段分别对应三类攻击:第一段封掉能加载外部资源或执行脚本的容器标签;第二段清掉事件属性和伪协议,这是onerror、onload这类最常用的绕过点;第三段是可选收紧,因为老版本的编辑器会往内容里写大量行内style,如果业务不依赖精确排版,清掉最安全。
要注意RegExp在 VBScript 里对\1反向引用的支持有限,第一段的“配对删除”在复杂嵌套 HTML 上不一定彻底,所以真正的防线是第二段和第三段——即使容器标签漏了,没有事件属性也执行不了。这就是为什么不能只靠一条正则。
5. 上线后的排查清单:六个真实翻车现场
5.1 编辑器区域一片空白,或者只有边框
现象:iframe 有高度但里面什么都没有,或者工具栏出来了但编辑区是灰的。
原因通常有三个:ewebeditor.asp的相对路径写错(站点部署在虚拟目录下时最常见),style参数指向的样式名在配置库里不存在,或者db/下的配置库根本没被读到。第三种在 32 位/64 位混配时必然发生——连接对象创建失败,页面静默返回空。
解决顺序:先在浏览器直接打开ewebeditor.asp?id=content&style=standard600,看返回的 HTML 是完整页面还是报错;再打开浏览器开发者工具的网络面板,看皮肤 css 和 js 是不是 404;最后确认应用池 32 位已开、配置库路径可读。三步走完,八成能定位。
5.2 图片上传成功,前台显示 404
现象:上传对话框提示成功,返回的路径也能看到,但页面里图片是红叉,直接访问返回 404。
原因:编辑器返回的是“相对于编辑器目录”的路径,比如uploadfile/20240506/xxx.jpg,而你的内容展示页在站点另一层目录,相对路径就解析错了。
解决办法是统一存绝对路径。后台配置里一般有“上传后返回路径”的选项,改成/eweb/uploadfile/开头;如果配置项找不到,就在保存前对src做一次替换,把相对路径补成站点根相对路径。这个坑不影响功能,但用户会以为图丢了,投诉量很大。
5.3 中文内容入库变问号,或者只剩前半段
现象:编辑器里显示正常,保存后到列表页一看,中文全是?,或者内容从某个字符处被截断。
原因:编码链路没对齐。页面是 UTF-8,但Session.CodePage还是默认的 936,表单提交的中文按 GB2312 解码,超出范围的字符就变问号。截断则是字段长度不够——富文本的 HTML 比纯文本长好几倍,nvarchar(500)这种字段存一篇带格式的文章必然被截。
解决:把响应码页、会话码页、数据库字段长度三处对齐。字段类型优先用ntext/nvarchar(max)这类不定长类型,长度按“纯文本长度 × 8”估算比较稳。
5.4 点保存没反应,也没有任何报错
现象:点保存按钮,页面不动,浏览器网络面板里那条请求是红色或者干脆没发出。
原因:onsubmit里回写内容的函数抛异常了(iframe 还没加载完就点提交),或者请求体超过maxRequestEntityAllowed被 IIS 直接断开。后者的特征是请求有发出、响应为空、状态码 404.13。
解决:给提交按钮加一个“编辑器是否就绪”的判断,未就绪时禁用;同时按 2.3 的三条命令把两个上限一起放大。注意顺序:先确认是前端异常还是服务端拒绝,看网络面板的状态码就能分清,别一上来就改配置。
5.5 后台管理目录被外部扫描到
现象:访问日志里出现大量对admin_login.asp、admin_style.asp的请求,来源分散。
原因:后台目录名是固定的,装完之后没做任何收敛。这类老编辑器的后台默认路径在互联网上是被批量扫的。
解决:上线后立刻把管理目录改名(改完记得同步改内部跳转),并给它加 IP 白名单或者至少加一层 Basic 认证。hiddenSegments挡不住扫描器发请求,它只是让请求拿不到内容,日志里还是会有记录。真正省心的是“改名 + 限制来源”两条一起做。
6. 渐进替换:让老内容在新编辑器里活下来
多数人最后都会走向替换,但替换的难点不在装新编辑器,而在库里那几万篇用 eWebEditor 排版出来的文章——里面全是<font>、<span style="font-size:12pt">、<div align=center>这类老式标签,新编辑器一加载就自动“清洗”,排版全乱。
我一般不在编辑器层面解决,而是在“保存入口”加一层归一化,让新旧编辑器共用同一条入库管道。具体做法是给内容表加一个editor_type字段,老数据标legacy,新数据标modern;展示时对legacy走老样式渲染,编辑时先做一次标签升级再送进新编辑器。
// 前端归一化:把老式排版标签翻译成等价的行内样式,减少视觉跳变 const LEGACY_MAP = { font: (el) => { const size = el.getAttribute("size"); const color = el.getAttribute("color"); const span = document.createElement("span"); if (size) span.style.fontSize = { 1: "10px", 2: "13px", 3: "16px", 4: "18px", 5: "24px" }[size] || ""; if (color) span.style.color = color; span.innerHTML = el.innerHTML; return span; }, center: (el) => { const div = document.createElement("div"); div.style.textAlign = "center"; div.innerHTML = el.innerHTML; return div; } }; function normalizeLegacyHtml(html) { const box = document.createElement("div"); box.innerHTML = html; Object.keys(LEGACY_MAP).forEach((tag) => { box.querySelectorAll(tag).forEach((el) => { el.replaceWith(LEGACY_MAP[tag](el)); }); }); // 老内容里的 <p> 大多没有内边距,统一补一点,避免行距挤在一起 box.querySelectorAll("p").forEach((p) => { if (!p.style.margin) p.style.margin = "0 0 8px 0"; }); return box.innerHTML; }LEGACY_MAP只覆盖最常见的三类标签,font的size映射表是按老浏览器默认值给的近似值,不是精确对应,视觉上够用就行。真正要注意的是replaceWith之后原节点的引用会失效,所以遍历必须在替换前用querySelectorAll一次性取出来——边遍历边替换会漏节点,这个坑我踩过。
替换节奏上,我倾向“新入口用新的、老入口先不动”,观察两周再全量切。切换前一定要做一次抽样验证:从库里按时间倒序取 50 篇老文章,逐篇打开比对渲染结果,重点看表格、居中对齐、字号这三类,这三类占老排版问题的九成。别信自动化比对,HTML 结构差异太大,人眼过一遍最快。
最后一个习惯:不管用哪个编辑器,入库前都把内容过一遍白名单过滤,别指望前端校验。编辑器的版本会换,过滤函数可以一直留着——这是我这些年唯一没后悔过的决定。希望帮到你。
本文还有配套的精品资源,点击获取