KindEditor 是 2006 年起就被广泛集成进 PHP/.NET CMS 的所见即所得编辑器,DedeCMS、ECShop、phpcms、Discuz 多个版本都把它当过默认富文本编辑器。绝大多数项目用它都会撞上同一组痛点:图片上传目录混乱、上传大小受限、单图传完会自动包一层 <p><br/></p>、网络图片入口有安全隐患、多图批量上传数量太少。本文按上传目录、上传限额、网络图片关闭、p 标签包裹、视觉显示优化、CSRF 与 XSS 加固六个维度逐一给出代码改造,覆盖 KindEditor 4.1.x 主流版本。
KindEditor 整体架构与改造前的准备
核心目录结构
典型部署路径下 KindEditor 的目录长这样:
kindeditor/
├── kindeditor-all.js # 完整版 JS(开发版)
├── kindeditor-all-min.js # 完整版 JS(压缩版)
├── kindeditor.js # 核心 JS
├── plugins/ # 插件目录
│ ├── image/ # 单图上传插件
│ ├── multiimage/ # 多图批量上传
│ ├── filemanager/ # 文件管理器
│ └── ...
├── themes/ # 皮肤
├── lang/ # 多语言
├── php/
│ ├── upload_json.php # 单图与文件上传后端
│ ├── file_manager_json.php
│ └── JSON.php
└── jsp/ # JSP 后端(如不用 JSP 可删)改造前先做一份完整备份,所有路径写改动备忘以便回滚。
识别 KindEditor 实例的两种调用形态
KindEditor 的初始化有两种常见写法,决定后续改造代码改哪儿:
形态 A:完整富文本编辑器
KindEditor.create('#editor', {
uploadJson: '/kindeditor/php/upload_json.php',
fileManagerJson: '/kindeditor/php/file_manager_json.php',
allowFileManager: true,
items: ['source','|','undo','redo','|','image','multiimage','...']
});形态 B:单独调用图片上传弹窗(不带编辑器)
var editor = KindEditor.editor({
uploadJson: '/kindeditor/php/upload_json.php',
allowFileManager: true
});
editor.loadPlugin('image', function() {
editor.plugin.imageDialog({
showRemote: false,
clickFn: function(url, title, width, height) {
// 处理结果
}
});
});关闭网络图片标签页
KindEditor 默认的图片上传弹窗有「本地上传」「网络图片」「图片空间」三个标签页。「网络图片」让用户输入任意 URL 直接插入页面,对站点有两个隐患:
- 外链图片随时失效,正文图片显示成裂图。
- 第三方图床的隐私追踪 cookie 会跟着请求发送,触发用户的隐私警告。
关闭方法按调用形态分两种:
形态 B(单独图片上传)
在调用 imageDialog 时直接传参:
editor.plugin.imageDialog({
showRemote: false,
showLocal: true
});形态 A(编辑器内的图片按钮)
编辑 plugins/image/image.js,找到内部对 imageDialog 的调用:
self.plugin.imageDialog({
imageUrl : K(this.cmd.range.startContainer).attr('src'),
clickFn : function(url, title, width, height, border, align) {
// ...
}
});加上 showRemote: false:
self.plugin.imageDialog({
showRemote: false,
imageUrl : K(this.cmd.range.startContainer).attr('src'),
clickFn : function(url, title, width, height, border, align) {
// ...
}
});同样的改法对 multiimage.js 也适用——多图批量上传弹窗也有网络图片入口。
批量上传:调整数量、单张大小、并发
前端限额
编辑 plugins/multiimage/multiimage.js,定位到:
KindEditor.plugin('multiimage', function(K) {
var self = this,
imageSizeLimit = K.undef(self.imageSizeLimit, '5MB'),
imageUploadLimit = K.undef(self.imageUploadLimit, 20);
...
});把默认 5MB / 20 张分别改大。生产里我常用的设置是 30MB / 200 张:
var imageSizeLimit = K.undef(self.imageSizeLimit, '30MB'),
imageUploadLimit = K.undef(self.imageUploadLimit, 200);不建议直接改成 1000 张,浏览器一次拖入 1000 张图片会让 DOM 卡死,分批上传体验更好。
后端限额(upload_json.php)
编辑 php/upload_json.php,定位:
$max_size = 1000 * 1024; // 默认 1MB改成与前端一致:
$max_size = 30 * 1024 * 1024; // 30MBphp.ini 三个关键参数
仅改前后端不够,PHP 本身有三道闸门,必须同步调整:
upload_max_filesize = 30M
post_max_size = 100M
max_file_uploads = 200
max_execution_time = 300
memory_limit = 256Mpost_max_size 必须 ≥ 单文件大小 × 单次上传数量,否则一次性上传 50 张 5MB 图会触发 413 Request Entity Too Large。memory_limit 不够会让 PHP 在 GD 库处理图片时内存不足致命错误。
nginx 同步调整
nginx 默认 client_max_body_size 是 1MB,必须改:
http {
client_max_body_size 100m;
client_body_timeout 300s;
client_body_buffer_size 128k;
}这一项不改的话,nginx 会先于 PHP 拒绝请求,前端看到的是 413 而不是 PHP 的友好错误提示。
Apache 同步调整
.htaccess 里加:
php_value upload_max_filesize 30M
php_value post_max_size 100M
php_value max_execution_time 300
php_value memory_limit 256M
LimitRequestBody 104857600上传目录改造与按日期归档
原始默认行为
upload_json.php 默认所有文件都上传到 attached/ 目录,时间长了会累积几万个文件在一个目录里,运维查找麻烦、ext4 文件系统单目录文件数太多会导致 ls 卡顿、备份增量同步也变慢。
按日期归档的改法
编辑 php/upload_json.php,定位:
// 文件保存目录路径
$save_path = $php_path . '../attached/';
// 文件保存目录URL
$save_url = $base_url . '../attached/';改成按年月分目录:
$year_month = date('Ym'); // 例如 202405
$save_path = $php_path . '../attached/' . $year_month . '/';
$save_url = $base_url . '../attached/' . $year_month . '/';
if (!file_exists($save_path)) {
mkdir($save_path, 0755, true);
}0755 在多数生产环境下足够(owner=apache/nginx 用户写入,group/other 只读),不要用 0777。
按业务模块进一步细分
如果你的站点有多个业务模块(比如商品图、文章图、用户头像走同一个 KindEditor),可以通过 GET/POST 参数传递 module 名做更细分类:
$module = isset($_REQUEST['module']) ? preg_replace('/[^a-z0-9_]/i', '', $_REQUEST['module']) : 'default';
$save_path = $php_path . '../attached/' . $module . '/' . $year_month . '/';
$save_url = $base_url . '../attached/' . $module . '/' . $year_month . '/';preg_replace 这一步是安全屏障,避免恶意构造的 module 名做目录穿越(比如 module=../../etc)。
文件名重命名规则
原始 upload_json.php 用以下规则给文件改名:
$new_file_name = date('YmdHis') . '_' . rand(10000, 99999) . '.' . $file_ext;这个规则在并发场景下还有概率撞名(同一秒内两个用户都摇到 12345)。建议改成 UUID 或加上原始文件名 hash:
$new_file_name = date('YmdHis') . '_' . substr(md5(uniqid(mt_rand(), true)), 0, 12) . '.' . $file_ext;UUID 部分 12 位 hex 共 16^12 ≈ 281 万亿组合,撞名概率近乎为零。
图片自动包 p 标签的处理
问题描述
KindEditor 默认在图片插入到正文后会自动包一层 <p><br/></p>,原意是给用户留下后续输入文字的位置。但当用户连续插入多张图时,每张图前后都会出现一个空 p,正文渲染时多出大量空行,移动端尤其难看。
方案 A:保留 p 但去掉空 p
编辑 kindeditor.js,找到这一段:
html = html.replace(/(<(?:p|p\s[^>]*)>)\s*(<\/p>)/ig, '$1<br />$2');改成:
html = html.replace(/(<(?:p|p\s[^>]*)>)\s*(<\/p>)/ig, '');原代码保留空 p 并填一个 br;新代码直接删除空 p。空段落消失,多图连排布局紧凑。
方案 B:给所有图片单独包 p
这一改法适合「每张图一个段落」的版式(旅行游记、产品多图展示)。在 kindeditor.js 的 insertimage 函数里:
insertimage : function() {
var self = this;
self.exec('insertimage', ...);
}改成把 img 包在 p 里再 exec:
insertimage : function(url, title, width, height, border, align) {
var img = '<p><img src="' + url + '" alt="' + title + '" /></p>';
this.exec('inserthtml', img);
}方案 C:清理后端正文
有些情况编辑器输出已经写库,再去前端清空 p 不现实。这时候改 PHP 后端,在保存前清理:
function clean_editor_html($html) {
// 删除空段落
$html = preg_replace('/<p[^>]*>\s*(<br\s*\/?>)?\s*<\/p>/i', '', $html);
// 合并多余空白
$html = preg_replace('/\s{2,}/', ' ', $html);
return $html;
}
$content = clean_editor_html($_POST['content']);视觉显示优化(编辑器内)
问题:富文本所见即所得不像
KindEditor 在 iframe 内渲染正文,自带 css 让段落紧贴、图片溢出、光标显示不清。这与最终前台展示效果差距大,作者不知道发出去会变成什么样。
定位 _getInitHtml 函数
kindeditor.js 内 _getInitHtml 函数返回 iframe 内的初始 HTML,包含 head 里的 style 块。原始 style 大概长这样:
'body {' +
' font:12px/1.5 \'Microsoft YaHei\',\'simsun\',\'Helvetica\',sans-serif;' +
' margin:8px;' +
'}' +
'p {margin:5px 0;}' +
'img {border:0;}'对照前台样式调整
建议把 iframe 内的 css 与前台正文 css 大致对齐。我项目里常用的调整:
'body {' +
' font:16px/1.7 \'Microsoft YaHei\',\'PingFang SC\',sans-serif;' +
' margin:16px;' +
' color: #333;' +
' background: #fff;' +
'}' +
'p {margin:12px 0; line-height:1.7;}' +
'p img {max-width:100%; height:auto; border-radius:4px;}' +
'img {border:0; padding:0 4px;}' +
'h2 {font-size:22px; margin-top:24px; border-left:4px solid #4a90e2; padding-left:10px;}' +
'h3 {font-size:18px; margin-top:18px; color:#4a90e2;}' +
'a {color:#4a90e2; text-decoration:none;}' +
'pre {background:#f4f4f4; padding:12px; border-radius:4px; overflow-x:auto;}'安全加固:CSRF / XSS / 文件类型验证
upload_json.php 的默认安全漏洞
KindEditor 4.1.x 自带的 upload_json.php 有这些已知问题:
- 没有 CSRF token 校验,任何域名都能往后端 post 文件。
- file_manager_json.php 没有权限校验,未登录用户能列出 attached/ 目录所有文件。
- 白名单扩展名靠后缀判断,能被双扩展名(pic.jpg.php)绕过。
- 未做 MIME 真实类型检查,能用伪造 header 上传 PHP 文件。
加 CSRF token
upload_json.php 顶部加:
session_start();
if (empty($_REQUEST['_csrf']) || $_REQUEST['_csrf'] !== $_SESSION['kindeditor_csrf']) {
header('Content-Type: application/json');
echo json_encode(['error' => 1, 'message' => 'CSRF check failed']);
exit;
}前端调用 KindEditor 时把 token 传过去:
KindEditor.create('#editor', {
uploadJson: '/kindeditor/php/upload_json.php?_csrf=<?php echo $_SESSION['kindeditor_csrf']; ?>'
});双扩展名拦截
原代码:
$ext = strtolower(strrchr($file_name, '.'));
$ext = trim($ext, '.');
if (!in_array($ext, $ext_arr[$dir_name])) { ... }双扩展名(image.jpg.php)会被识别成 .php 拒绝,但 image.php.jpg 会被识别成 .jpg 通过——实际是 PHP 文件。修复:检查全部扩展名:
$file_parts = explode('.', $file_name);
$dangerous = ['php', 'phtml', 'php3', 'php4', 'php5', 'pht', 'asp', 'aspx', 'jsp', 'cer', 'cdx', 'asa'];
foreach ($file_parts as $part) {
if (in_array(strtolower($part), $dangerous)) {
die(json_encode(['error' => 1, 'message' => 'Dangerous extension']));
}
}MIME 真实类型检查
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['imgFile']['tmp_name']);
$allowed_mimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($mime, $allowed_mimes)) {
die(json_encode(['error' => 1, 'message' => 'Invalid MIME type']));
}file_manager_json.php 加登录验证
file_manager_json.php 头部加:
session_start();
if (empty($_SESSION['user_id']) || $_SESSION['user_role'] !== 'admin') {
header('HTTP/1.1 403 Forbidden');
exit;
}这一项至关重要——之前发现过黑产专门扫描 KindEditor 的 file_manager_json.php,匿名拉走整个 attached/ 目录的图片清单做爬虫源。
多语言、皮肤、自定义工具栏的扩展
切换中文界面
KindEditor.create('#editor', {
langType: 'zh-CN',
themeType: 'default'
});同时确保 lang/zh-CN.js 文件存在。如果想自定义某些菜单文案,直接编辑这个文件,热更新无需重启。
定制工具栏按钮顺序
KindEditor.create('#editor', {
items: [
'source', '|', 'undo', 'redo', '|',
'cut', 'copy', 'paste', 'plainpaste', 'wordpaste', '|',
'justifyleft', 'justifycenter', 'justifyright', '|',
'insertorderedlist', 'insertunorderedlist', '|',
'fontname', 'fontsize', '|',
'forecolor', 'hilitecolor', 'bold', 'italic', 'underline', '|',
'image', 'multiimage', 'flash', 'media', '|',
'table', 'hr', 'emoticons', '|',
'link', 'unlink'
]
});移除掉「网络图片」「图片空间」「flash」等不常用功能,工具栏更整洁,同时降低被滥用的风险。
与 WordPress / Discuz / DedeCMS 的具体集成
WordPress 后台用 KindEditor 替换 TinyMCE
不建议这么做。WordPress 5.0 之后默认编辑器是 Gutenberg block editor,KindEditor 是 2010 年代设计的传统富文本,强行替换会丢失所有 block 功能。如果非要用,参考 wp-kindeditor 这类老旧插件。
DedeCMS 把内置编辑器换成 KindEditor
编辑 include/inc/inc_fun_funAdmin.php 找到 ShowEditor 函数,替换 fck/ueditor 的调用为 KindEditor。具体步骤涉及 JS path 调整、上传后端对接 dedeAjax,非常繁琐,仅在二次开发项目里值得做。
Discuz 集成
Discuz 默认编辑器是自家的 ckeditor 改造版。强行替换会破坏帖子 BBCode 解析。建议只在自定义页面(比如插件后台)里用 KindEditor,原帖编辑器保留 Discuz 默认。
实战故障排查
故障 1:图片上传成功但前台显示裂图
查后端返回的 JSON:
{"error":0,"url":"/kindeditor/attached/202405/test.jpg"}看 url 字段——如果是相对路径但你的页面在子目录,会拼错。修复 upload_json.php 让 url 始终返回绝对路径:
$file_url = ($_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . $save_url . $new_file_name;
echo json_encode(['error' => 0, 'url' => $file_url]);故障 2:上传 50 张图后浏览器卡死
一次性把所有图加入 DOM 让浏览器渲染压力爆炸。改造 multiimage.js 让它分批 5 张一次插入:
function batchInsert(files, idx) {
if (idx >= files.length) return;
var batch = files.slice(idx, idx + 5);
insertImages(batch);
setTimeout(function() { batchInsert(files, idx + 5); }, 200);
}故障 3:粘贴 Word 文档进编辑器后样式爆炸
Word 粘贴的 HTML 里夹杂了大量 mso-* 内联样式与命名空间。KindEditor 自带 wordpaste 按钮可以清理,但默认未启用。在 items 工具栏配置里把 wordpaste 加进去,并设置 pasteType 配置:
KindEditor.create('#editor', {
pasteType: 1, // 0=禁止粘贴, 1=纯文本粘贴, 2=允许 HTML 粘贴
afterCreate: function() {
this.cmd.range.addRange(/* ... */);
}
});故障 4:移动端编辑器无法拖拽图片上传
KindEditor 4.1.x 没有针对移动端优化,多图上传依赖 input[type=file][multiple],移动浏览器对此支持差。建议移动端改用 wangEditor 或 Quill 这类现代编辑器。
故障 5:上传后图片自动旋转 90°
iPhone 拍的横屏照片 EXIF 里有旋转信息,浏览器渲染前会按 EXIF 旋转。后端用 GD 重新保存时丢失 EXIF,前端再加载就「转向不对」。修复:上传时同步处理 EXIF:
$exif = exif_read_data($_FILES['imgFile']['tmp_name']);
if (!empty($exif['Orientation'])) {
$img = imagecreatefromjpeg($_FILES['imgFile']['tmp_name']);
switch ($exif['Orientation']) {
case 3: $img = imagerotate($img, 180, 0); break;
case 6: $img = imagerotate($img, -90, 0); break;
case 8: $img = imagerotate($img, 90, 0); break;
}
imagejpeg($img, $save_path . $new_file_name, 90);
}常见问题解答
KindEditor 还在维护吗?
原作者 Roddy 自 2014 年后已不再活跃维护。最后一个版本是 4.1.12(2017 年)。如果是新项目,建议选择 wangEditor、CKEditor 5、TinyMCE 6、Quill 这类有持续更新的现代编辑器。如果是老项目里已经用了 KindEditor,本文的改造方案足够覆盖大多数运营需求。
upload_json.php 改了之后多图上传仍然走老目录?
多图上传走的是同一个 upload_json.php,但路径计算可能因为 PHP_SELF 不同而走到不同的相对路径分支。建议改造时把所有 save_path/save_url 计算都改用绝对路径,避免分支不一致。
批量上传 200 张图速度很慢怎么办?
瓶颈通常在后端 GD 库处理图片。三个加速:上传时不做缩略图(异步交给 cron);用 Imagick 替代 GD(处理速度快 30%);前端先压缩再上传(用 browser-image-compression 这类 JS 库)。
KindEditor 与 fileinput.js 哪个上传体验更好?
fileinput.js 的 UI 更现代,支持拖拽、进度条、缩略图预览,移动端友好。但它只是单纯文件上传组件,不带富文本编辑功能。如果业务只是「上传图片」可以用 fileinput.js 替代 KindEditor 的 multiimage 模块。
能否把 KindEditor 切换成 markdown 编辑器?
切换成本接近重写。建议直接用 SimpleMDE、EasyMDE、Editor.md 这种专业 markdown 编辑器,KindEditor 强行支持 markdown 体验差。
file_manager_json.php 不加权限校验有多严重?
非常严重。曾遇到过实战案例:黑产扫到未保护的 file_manager_json.php,遍历整个 attached/ 目录找用户上传的身份证、合同截图,扒走数据库。所有线上 KindEditor 部署都必须加管理员权限校验。
升级 PHP 到 7.4+ 后上传报错?
KindEditor 4.1.12 的 PHP 后端用了一些 PHP 7+ 移除的写法(each() 函数、__autoload)。需要手动改成 foreach、spl_autoload_register 替代。或者干脆把 upload_json.php 重写成现代 PHP 版本(200 行内)。
多个站点共用一个 KindEditor 部署可行吗?
可行,但 upload_json.php 必须按 Host 区分上传目录,否则站点 A 上传的图能被站点 B 访问。在 upload_json.php 顶部加:$host_dir = preg_replace('/[^a-z0-9_.-]/i', '_', $_SERVER['HTTP_HOST']); $save_path .= $host_dir . '/';
KindEditor 的 SVG 上传支持吗?
默认扩展名白名单不含 svg。手动加上后要注意:SVG 文件可以包含 <script> 标签触发 XSS。如果要支持 SVG 上传,必须先用 svgo 或 DOMPurify 清理脚本标签再保存。
KindEditor 与 TinyMCE 哪个更轻?
KindEditor 完整版约 250KB(gzip 后 80KB),TinyMCE 6 约 700KB(gzip 后 200KB)。KindEditor 在体积上有优势,但 TinyMCE 的功能丰富度、文档质量、移动端体验都更胜一筹。轻量项目可继续用 KindEditor,复杂项目建议迁移 TinyMCE。