KindEditor 编辑器深度优化指南:上传目录归档、批量限额、p 标签清理与 CSRF 安全加固

KindEditor 集成在 DedeCMS、ECShop、phpcms 多个 CMS 里的痛点集中在上传目录混乱、批量限额低、p 标签残留与 file_manager_json.php 未鉴权。本文按 6 个维度给出 plugins/multiimage、kindeditor.js、php/upload_json.php 的具体改造代码,并补全 nginx/php.ini/.htaccess 三层限额一致性。

更新 33 分钟阅读 3,992 阅读

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;  // 30MB

php.ini 三个关键参数

仅改前后端不够,PHP 本身有三道闸门,必须同步调整:

upload_max_filesize = 30M
post_max_size = 100M
max_file_uploads = 200
max_execution_time = 300
memory_limit = 256M

post_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 有这些已知问题:

  1. 没有 CSRF token 校验,任何域名都能往后端 post 文件。
  2. file_manager_json.php 没有权限校验,未登录用户能列出 attached/ 目录所有文件。
  3. 白名单扩展名靠后缀判断,能被双扩展名(pic.jpg.php)绕过。
  4. 未做 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。

分享到
标签
版权声明

本文标题:《KindEditor 编辑器深度优化指南:上传目录归档、批量限额、p 标签清理与 CSRF 安全加固》

本文链接:https://zhangwenbao.com/kindeditor-image-upload.html

版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交