- 前端
- UI组件
【免费下载链接】ckeditor4
The best enterprise-grade WYSIWYG editor. Fully customizable with countless features and plugins.
导读
在 CKEditor 4 中启用 Table Selection(表格选择)插件后,用户可以用鼠标框选整行/整列单元格并按下Backspace、Delete键直接删除选中行。但如果编辑器处于只读(read-only)状态,这种"选中即删除"的交互就会绕过只读保护,造成表格内容被意外清空。本文以仓库中的手动测试用例 readonly.md 为线索,完整还原该缺陷(GitHub Issue #1489)的复现步骤、预期行为,并结合 plugin.js 与 editor.js 的源码剖析修复原理,同时给出自动化测试用例的解读,帮助你理解并验证只读模式下表格选择的安全行为。
背景:一个被 #1489 修复的只读模式漏洞
Bug 来源与修复版本
在 CKEditor 4.11.0 之前的版本中,存在这样一个缺陷:当 Table Selection 插件启用时,编辑器处于只读模式下,表格内容仍然可以被删除。该问题在 CHANGES.md 中被明确记录:
Fixed:Table contents can be removed in read-only mode when the [Table Selection] plugin is used.(对应 Issue #1489,于 4.11.0 版本修复)
同类问题在历史上并不少见,例如:
- #1516:伪造选择(fake selection)允许在只读模式下用Backspace/Delete键删除内容;
- #1570:伪造选择允许在只读模式下用Ctrl/Cmd+X剪切内容;
- #808 / #3260:Widget 在只读模式下拖拽导致内容消失、拖拽手柄仍然可见;
- #2816 / #2874:增强图片(image2)在只读模式下出现调整大小手柄。
这些条目共同说明:CKEditor 4 的只读保护需要插件层面逐一加固,而表格选择的键盘删除路径正是其中之一。
手动测试用例的定位
仓库中用于验证该修复的手动测试用例位于 tests/plugins/tableselection/manual/readonly.md,配套的可运行页面为 tests/plugins/tableselection/manual/readonly.html。这两个文件共同构成了一个典型的 Bender 手动测试(manual test)用例,供 QA 或开发者按步骤在真实浏览器中验证行为。
手动测试用例逐条解读
测试用例文档(readonly.md)全文语义
@bender-ui: collapsed @bender-tags: 1489, 4.11.0, bug @bender-ckeditor-plugins: wysiwygarea, toolbar, tableselection 1. Focus the editor. 1. Select the first row in a table. 1. Press `backspace` key. 1. Repeat 1-3 with `delete` key. 1. Repeat 1-3 with `a`, `k` keys (random key validation). ## Expected Selected row didn't change. ## Unexpected Selected row has been deleted.先解读头部三个 Bender 指令:
@bender-ui: collapsed:手动测试页面在 Bender 测试面板中以折叠方式展示,便于聚焦编辑器本身;@bender-tags: 1489, 4.11.0, bug:标记该用例关联的 Issue 编号(1489)、修复版本(4.11.0)与类型(缺陷回归验证);@bender-ckeditor-plugins: wysiwygarea, toolbar, tableselection:声明测试所需的最小插件集——经典编辑器编辑区(wysiwygarea)、工具栏(toolbar)以及被测对象表格选择(tableselection)。
五步操作流程的核心意图
- Focus the editor(聚焦编辑器):保证键盘事件进入编辑器实例,同时让表格选择处于激活状态;
- Select the first row in a table(选中表格第一行):通过 Table Selection 的伪造选择机制选中整行单元格;
- Press
backspacekey(按下退格键):验证退格键不会删除选中行; - Repeat 1-3 with
deletekey(用删除键重复 1-3):验证Delete键同样无效; - Repeat 1-3 with
a,kkeys(用随机字符键重复 1-3):这是"随机键验证"(random key validation)——因为表格伪造选择状态下字符输入同样会先清空单元格再插入,因此需要确认任意普通按键在只读模式下都不会触发清空操作。
预期结果(Expected):被选中的行不发生任何变化,编辑器内容保持完整。意外结果(Unexpected):选中的行被整体删除——这正是 #1489 描述的缺陷行为。
测试页面(readonly.html)的搭建方式
配套页面 readonly.html 包含一个 3 行 × 4 列的标准表格(Cell 1.1.1到Cell 1.3.4),并在脚本中通过配置项直接初始化只读编辑器:
<div id="editor1"> <table border="1"> <tr> <td>Cell 1.1.1</td> <td>Cell 1.1.2</td> <td>Cell 1.1.3</td> <td>Cell 1.1.4</td> </tr> <!-- 共 3 行,每行 4 个单元格 --> </table> </div> <script> if ( bender.tools.env.mobile ) { bender.ignore(); } bender.tools.ignoreUnsupportedEnvironment( 'tableselection' ); CKEDITOR.replace( 'editor1', { readOnly : true } ); </script>这里有两个值得注意的细节:
readOnly : true是核心开关:它通过配置项让编辑器一启动就进入只读模式(区别于运行时再调用setReadOnly());- 环境守卫:移动端环境直接
bender.ignore()跳过本用例,同时bender.tools.ignoreUnsupportedEnvironment( 'tableselection' )会在浏览器不支持该插件(如 IE 11 以下)时自动忽略——这与插件自身isSupportedEnvironment()的限制保持一致(见下文源码分析)。
源码级修复原理:只读模式下拦截一切键盘编辑路径
修复点一:keydown 监听器中的editor.readOnly守卫
修复的核心在 plugins/tableselection/plugin.js。Table Selection 的键盘集成通过keyboardIntegration( editor )注入,其中getTableOnKeyDownListener返回的处理器管理着方向键、退格、删除与回车键在表格伪造选择下的行为:
var keystrokes = { 37: 1, // Left Arrow 38: 1, // Up Arrow 39: 1, // Right Arrow, 40: 1, // Down Arrow 8: 1, // Backspace 46: 1, // Delete 13: 1 // Enter }, ... return function( evt ) { // Use getKey directly in order to ignore modifiers. var key = evt.data.getKey(), keystroke = evt.data.getKeystroke(), ... // Handle only left/right/del/bspace keys. // Disable editing cells in readonly mode (#1489). if ( !keystrokes[ key ] || editor.readOnly ) { return; } ... };关键行是:
if ( !keystrokes[ key ] || editor.readOnly ) { return; }!keystrokes[ key ]:非表格选择管理的按键(例如a、k这类普通字符键)直接不处理;editor.readOnly:只要编辑器处于只读状态,即使按键命中管理列表(8 退格、46 删除、13 回车、37-40 方向键),也立即 return,不执行任何后续的单元格清空或删除逻辑。
这就是修复 #1489 的第一道闸门:它把"删除整行/清空单元格"这条路径在只读模式下彻底短路。
修复点二:keypress 监听器中的editor.readOnly守卫
伪造选择状态下还有一个独立的字符输入路径——tableKeyPressListener(plugin.js)。它的职责是处理用户在选中单元格后直接输入字符的情况(先清空被选单元格,再把字符放入首个单元格),这也是手动测试中"随机键验证"(步骤 5)要覆盖的场景:
function tableKeyPressListener( evt ) { var selection = editor.getSelection(), // Enter key also produces character, but Firefox doesn't think so (gh#415). isCharKey = evt.data.$.charCode || ( evt.data.getKey() === 13 ), ... // Disable editing cells in readonly mode (#1489). if ( editor.readOnly ) { return; } ... }这里在判断isCharKey之前就率先检查editor.readOnly——只读模式下连字符键的"清空后插入"行为也被一并拦截。注意该判断位于"是否产生字符"判断之前,意味着只要编辑器只读,无论按什么键(包括 Gecko 内核浏览器中所有按键都会触发 keypress 的情况)都不会进入清空流程。
两个监听器的挂载与优先级
这两个监听器在keyboardIntegration( editor )末尾统一挂载(plugin.js):
var editable = editor.editable(); editable.attachListener( editable, 'keydown', getTableOnKeyDownListener( editor ), null, null, -1 ); editable.attachListener( editable, 'keypress', tableKeyPressListener, null, null, -1 ); editable.attachListener( editable, 'keyup', getTableKeyUpListener( editor ), null, null, -1 );监听优先级为-1(低优先级),配合attachListener与 editable 生命周期绑定,确保编辑器销毁或重新加载内容时监听器随之清理。此外,getTableKeyUpListener专门处理Tab键(keyCode 9)在表格选择中的导航并负责恢复伪造选择(restoreFakeSelection),它本身不涉及内容修改,因此无需只读守卫。
被拦截的下游逻辑:清空与删行
一旦没有editor.readOnly守卫,被选中的表格元素会走上两条删除路径(可对照理解为什么会有"整行被删除"的意外结果):
clearCellInRange( range )(plugin.js):对td/th调用node.setText( '' )清空文本,对其他元素调用range.deleteContents()删除内容,随后为未删除的单元格补充 bogus<br>防止行塌陷;deleteEmptyTablePart( node, ranges )(plugin.js):当选中范围完整覆盖tr(甚至table)时,会放大范围边界节点并执行mergedRanges[ 0 ].deleteContents(),从而直接删除整行或整张表格——这正是手动测试"Unexpected: Selected row has been deleted"所描述的现象来源。
只读守卫正是为了让这两条路径在只读模式下永远不被触发。
只读机制的底层支撑:editor.readOnly 从何而来
配置项config.readOnly
editor.readOnly属性由核心在配置加载阶段初始化。在 core/editor.js 的onConfigLoaded中:
editor.readOnly = isEditorReadOnly(); function isEditorReadOnly() { if ( config.readOnly ) { return true; } if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE ) { if ( editor.element.is( 'textarea' ) ) { return editor.element.hasAttribute( 'disabled' ) || editor.element.hasAttribute( 'readonly' ); } else { return editor.element.isReadOnly(); } } else if ( editor.elementMode == CKEDITOR.ELEMENT_MODE_REPLACE ) { return editor.element.hasAttribute( 'disabled' ) || editor.element.hasAttribute( 'readonly' ); } return false; }从源码结构可以看出,editor.readOnly的判定优先级为:
config.readOnly配置为true——即手动测试中CKEDITOR.replace( 'editor1', { readOnly : true } )的用法,这是最直接的开关;- 替换模式(replace):被替换的
<textarea>带disabled或readonly属性; - 内联模式(inline):textarea 同样看
disabled/readonly属性;非 textarea 元素则看element.isReadOnly()(即元素或其祖先是否带contenteditable="false")。
config.readOnly的完整说明记录在 core/editor.js,默认值为false。
运行时切换:setReadOnly()
除了启动时通过配置进入只读,也可以在运行时调用editor.setReadOnly( isReadOnly )(core/editor.js):
setReadOnly: function( isReadOnly ) { isReadOnly = ( isReadOnly == null ) || isReadOnly; if ( this.readOnly != isReadOnly ) { this.readOnly = isReadOnly; // Block or release BACKSPACE key according to current read-only // state to prevent browser's history navigation. this.keystrokeHandler.blockedKeystrokes[ 8 ] = +isReadOnly; this.editable().setReadOnly( isReadOnly ); // Fire the readOnly event so the editor features can update // their state accordingly. this.fire( 'readOnly' ); } }该方法同步完成四件事:更新readOnly属性、按状态拦截/放行全局Backspace(keyCode 8,防止浏览器历史回退)、设置编辑区(editable)只读、触发readOnly事件供各插件刷新自身状态。Table Selection 插件的两个键盘监听器读取的正是这个editor.readOnly属性,因此无论是启动配置还是运行时切换,都能被正确感知。
插件对环境与依赖的约束
从 plugin.js 的插件定义可以看到两条重要约束:
CKEDITOR.plugins.add( 'tableselection', { requires: 'clipboard,tabletools', isSupportedEnvironment: function() { return !( CKEDITOR.env.ie && CKEDITOR.env.version < 11 ); }, ... init: function( editor ) { // Disable unsupported browsers. if ( !this.isSupportedEnvironment() ) { return; } ... } } );- 插件依赖
clipboard与tabletools,所以手动测试只声明三个插件也能正常工作(wysiwygarea、toolbar 是编辑器外壳,tableselection 会拉取其 requires 依赖); - IE 11 以下不支持该插件,这也是
readonly.html中调用ignoreUnsupportedEnvironment( 'tableselection' )的原因。
另外,伪造选择的视觉实现通过 CSS 类cke_table-faked-selection呈现(plugin.js),并在init中通过editor.addContentsCss( this.path + 'styles/tableselection.css' )注入内容样式;鼠标选择路径则由fakeSelectionMouseHandler监听 document 上的mousedown/mousemove/mouseup事件驱动(plugin.js),与键盘路径相互独立。
自动化测试:与手动用例对应的回归断言
除了手动测试,仓库还提供了对应的自动化回归用例:test random keys are not removing readonly selection(tests/plugins/tableselection/tableselection.js):
// (#1489) 'test random keys are not removing readonly selection': function( editor ) { var selection = editor.getSelection(), editable = editor.editable(), table = CKEDITOR.document.getById( 'simpleTable' ).getHtml(); editor.setReadOnly( true ); bender.tools.setHtmlWithSelection( editor, table ); var row = editor.editable().findOne( 'tr' ); selection.selectElement( row ); editable.fire( 'keydown', new CKEDITOR.dom.event( { keyCode: 8 } ) ); // backspace editable.fire( 'keydown', new CKEDITOR.dom.event( { keyCode: 46 } ) ); // delete editable.fire( 'keypress', new CKEDITOR.dom.event( { keyCode: 65, charCode: 65 } ) ); // `a` editable.fire( 'keypress', new CKEDITOR.dom.event( { keyCode: 93, charCode: 93 } ) ); // `t` editor.setReadOnly( false ); assert.areSame( bender.tools.compatHtml( table ), editor.getData(), 'Editor data' ); }这个用例与手动测试一一对应:
| 手动测试步骤 | 自动化测试对应动作 |
|---|---|
| 聚焦编辑器 | setHtmlWithSelection建立内容与选区 |
| 选中第一行 | selection.selectElement( row )(选中整行tr) |
按backspace | 触发keydown,keyCode 8 |
按delete | 触发keydown,keyCode 46 |
按a、k随机键 | 触发keypress,keyCode/charCode 65(a)与 93(t) |
| 断言选中行未改变 | assert.areSame( compatHtml( table ), editor.getData() ) |
测试的关键手法是先editor.setReadOnly( true )再模拟按键事件,最后恢复可编辑状态并断言getData()与初始 HTML 完全一致——任何被clearCellInRange/deleteEmptyTablePart触发的清空或删行都会导致断言失败。该用例位于测试套件文件头部声明的两个编辑器(classic与inline,见 tableselection.js)上各运行一遍,覆盖两种编辑模式。
如何在本地运行与验证
运行手动测试
手动测试用例依赖 Bender 测试框架,仓库根目录的 bender.js 与 bender.ci.js 提供了测试配置。典型流程:
- 安装依赖并启动 Bender 服务(
npm install后运行 bender 相关脚本); - 在浏览器中打开 Bender 测试面板,定位到
tests/plugins/tableselection/manual/readonly.html; - 按 readonly.md 的五步操作逐一执行,重点观察步骤 3-5 中选中行是否保持不变;
- 对照Expected / Unexpected判定结果。
若想亲眼看到修复前的缺陷行为,可以临时移除 plugin.js 中的editor.readOnly判断后重新构建——但请注意:仓库为只读资源,此操作仅用于本地实验验证,不应修改仓库文件。更稳妥的方式是直接运行上文的自动化测试用例来验证当前代码的行为。
自动化测试的运行范围
自动化用例位于 tests/plugins/tableselection/tableselection.js,文件头部声明了bender-ckeditor-plugins: tableselection, clipboard、bender-ckeditor-remove-plugins: undo,并引入了_helpers/tableselection.js中的tableSelectionHelpers与mockMouseSelection辅助函数。运行该套件即可将 readonly 回归用例与其余表格选择测试(合并单元格、嵌套表格、粘贴等)一并验证。
总结与延伸阅读
围绕 #1489,CKEditor 4 在 4.11.0 中通过在键盘删除路径与字符输入路径两处增加editor.readOnly守卫修复了只读模式下表格内容可被删除的缺陷。这套"手动测试 + 自动化回归 + 源码守卫"的组合体现了该项目的质量保障模式:
- 手动测试(readonly.md / readonly.html)用于真实浏览器体验验证;
- 自动化测试(tableselection.js)用模拟事件保证持续回归;
- 源码守卫(plugin.js 与 plugin.js)从机制上杜绝绕过。
只读保护在整个项目中是一个系统性工程,除表格选择外,CHANGES.md 中还记录了伪造选择剪切(#1570)、退格删除(#1516)、Widget 拖拽(#808)、增强图片缩放(#2816 / #2874)等一系列同类修复。理解editor.readOnly属性(core/editor.js)与config.readOnly配置项(core/editor.js)的联动机制,是排查任何"只读模式下内容仍被修改"问题的起点——而 Table Selection 插件的两处守卫,正是这一机制在表格场景下的标准实践。
- 前端
- UI组件
【免费下载链接】ckeditor4
The best enterprise-grade WYSIWYG editor. Fully customizable with countless features and plugins.
相关推荐
CKEditor 4 Find 插件只读模式查错:trac12848 缺陷修复与 Bender 手动测试全解析
CKEditor 4 Find 插件只读模式查错:trac12848 缺陷修复与 Bender 手动测试全解析 导读 本文以 CKEditor 4 仓库中的一条
前端UI组件CKEditor 4 Table Selection 插件按 Enter 键行为深度解析:从手动测试用例到源码实现
CKEditor 4 Table Selection 插件按 Enter 键行为深度解析:从手动测试用例到源码实现 本文以 tests/plugins/tabl
前端UI组件CKEditor 4 Table Selection 插件拖拽选区边界:trac16755 手动测试用例与源码机制解读
CKEditor 4 Table Selection 插件拖拽选区边界:trac16755 手动测试用例与源码机制解读 导读 本文围绕 CKEditor 4 官
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考