KindEditor 以輕量、國產化支持好著稱。本文展示在 Yii 1.x 中封裝 KEditor Widget,實現編輯器渲染、圖片/文件上傳、服務器文件瀏覽管理的完整一體化方案。
protected/extensions/KEditor/
├── KEditor.php # 主 Widget 類
├── KEditorManage.php # 文件瀏覽管理 Action
├── KEditorUpload.php # 文件上傳 Action
└── keSource/ # KindEditor 源碼目錄
├── kindeditor.js
├── kindeditor-min.js
├── themes/
├── plugins/
└── lang/
<?php
class KEditor extends CWidget {
public $model; // 表單模型
public $name; // 字段名 (必填)
public $id; // DOM ID (可選)
public $textareaOptions = []; // textarea 屬性
public $properties = []; // KindEditor 配置屬性
private $_baseUrl; // 資源發布 URL
public static function getUploadPath() {
if (isset(Yii::app()->params['uploadPath'])) {
return Yii::getPathOfAlias('webroot') . str_replace('/', DIRECTORY_SEPARATOR, Yii::app()->params['uploadPath']);
}
$dir = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'keSource';
return Yii::app()->getAssetManager()->getPublishedPath($dir) . DIRECTORY_SEPARATOR . 'upload';
}
public static function getUploadUrl() {
if (isset(Yii::app()->params['uploadPath'])) {
return Yii::app()->baseUrl . Yii::app()->params['uploadPath'];
}
$dir = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'keSource';
return Yii::app()->getAssetManager()->publish($dir) . '/upload';
}
public function init() {
if ($this->name === null) throw new CException('name 屬性不能為空');
$dir = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'keSource';
$this->_baseUrl = Yii::app()->getAssetManager()->publish($dir);
$cs = Yii::app()->getClientScript();
$cs->registerCssFile($this->_baseUrl . '/themes/default/default.css');
$cs->registerScriptFile(YII_DEBUG ? $this->_baseUrl . '/kindeditor.js' : $this->_baseUrl . '/kindeditor-min.js');
}
public function run() {
$cs = Yii::app()->getClientScript();
// 渲染 textarea
$taOptions = $this->getTextareaOptions();
$taOptions['name'] = CHtml::resolveName($this->model, $this->name);
$this->id = $taOptions['id'] = CHtml::getIdByName($taOptions['name']);
echo CHtml::activeTextArea($this->model, $this->name, $taOptions);
// 初始化 KindEditor
$props = CJavaScript::encode($this->getKeProperties());
$js = <<id} = K.create('#{$this->id}', {$props});
});
EOF;
$cs->registerScript('KE_' . $this->name, $js, CClientScript::POS_HEAD);
}
protected function getTextareaOptions() {
$allow = ['rows', 'cols', 'style'];
$params = [];
foreach ($allow as $key) {
if (isset($this->textareaOptions[$key])) $params[$key] = $this->textareaOptions[$key];
}
$params['name'] = $params['id'] = $this->name;
return $params;
}
protected function getKeProperties() {
$allowKeys = [
'width', 'height', 'minWidth', 'minHeight', 'items', 'noDisableItems',
'filterMode', 'htmlTags', 'wellFormatMode', 'resizeType', 'themeType',
'langType', 'designMode', 'fullscreenMode', 'basePath', 'themesPath',
'pluginsPath', 'langPath', 'minChangeSize', 'urlType', 'newlineTag',
'pasteType', 'dialogAlignType', 'shadowMode', 'useContextmenu',
'syncType', 'indentChar', 'cssPath', 'cssData', 'bodyClass',
'colorTable', 'afterCreate', 'afterChange', 'afterTab', 'afterFocus',
'afterBlur', 'afterUpload', 'uploadJson', 'fileManagerJson',
'allowPreviewEmoticons', 'allowImageUpload', 'allowFlashUpload',
'allowMediaUpload', 'allowFileUpload', 'allowFileManager',
'fontSizeTable', 'imageTabIndex', 'formatUploadUrl', 'fullscreenShortcut',
'extraFileUploadParams'
];
$params = [];
foreach ($allowKeys as $key) {
if (isset($this->properties[$key])) $params[$key] = $this->properties[$key];
}
return $params;
}
}
<?php
class KEditorManage extends CAction {
public function run() {
Yii::import('ext.KEditor.KEditor');
$rootPath = KEditor::getUploadPath() . '/';
$rootUrl = KEditor::getUploadUrl() . '/';
$extArr = ['gif', 'jpg', 'jpeg', 'png', 'bmp'];
$dirName = empty($_GET['dir']) ? '' : trim($_GET['dir']);
if (!in_array($dirName, ['', 'image', 'flash', 'media', 'file'])) {
echo 'Invalid Directory name.'; exit;
}
if ($dirName) {
$rootPath .= $dirName . '/';
$rootUrl .= $dirName . '/';
if (!file_exists($rootPath)) mkdir($rootPath);
}
// 路徑解析
if (empty($_GET['path'])) {
$currentPath = realpath($rootPath) . '/';
$currentUrl = $rootUrl;
$currentDirPath = '';
$moveupDirPath = '';
} else {
$currentPath = realpath($rootPath) . '/' . $_GET['path'];
$currentUrl = $rootUrl . $_GET['path'];
$currentDirPath = $_GET['path'];
$moveupDirPath = preg_replace('/(.*\/)[^\/]+\/$/', '$1', $currentDirPath);
}
// 安全檢查
if (strpos($currentPath, '..') !== false || !preg_match('/\/$/', $currentPath) || !is_dir($currentPath)) {
echo 'Access denied.'; exit;
}
// 掃描文件
$fileList = [];
$handle = new DirectoryIterator($currentPath);
foreach ($handle as $file) {
if ($file->isDot()) continue;
$item = [
'filename' => $file->getFilename(),
'datetime' => date('Y-m-d H:i:s', $file->getMTime()),
];
if ($file->isDir()) {
$item['is_dir'] = true;
$item['has_file'] = count(scandir($file->getPathname())) > 2;
$item['filesize'] = 0;
$item['is_photo'] = false;
$item['filetype'] = '';
} else {
$item['is_dir'] = false;
$item['has_file'] = false;
$item['filesize'] = $file->getSize();
$ext = $file->getExtension();
$item['is_photo'] = in_array($ext, $extArr);
$item['filetype'] = $ext;
}
$fileList[] = $item;
}
// 排序
$order = empty($_GET['order']) ? 'name' : strtolower($_GET['order']);
usort($fileList, function($a, $b) use ($order) {
if ($a['is_dir'] && !$b['is_dir']) return -1;
if (!$a['is_dir'] && $b['is_dir']) return 1;
if ($order === 'size') return $a['filesize'] - $b['filesize'];
if ($order === 'type') return strcmp($a['filetype'], $b['filetype']);
return strcmp($a['filename'], $b['filename']);
});
// 輸出 JSON
header('Content-Type: application/json; charset=UTF-8');
echo CJSON::encode([
'moveup_dir_path' => $moveupDirPath,
'current_dir_path' => $currentDirPath,
'current_url' => $currentUrl,
'total_count' => count($fileList),
'file_list' => $fileList,
]);
exit;
}
}
<?php
class KEditorUpload extends CAction {
public function run() {
$dir = isset($_GET['dir']) ? trim($_GET['dir']) : 'file';
$extArr = [
'image' => ['gif', 'jpg', 'jpeg', 'png', 'bmp'],
'flash' => ['swf', 'flv'],
'media' => ['swf', 'flv', 'mp3', 'wav', 'wma', 'wmv', 'mid', 'avi', 'mpg', 'asf', 'rm', 'rmvb'],
'file' => ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'htm', 'html', 'txt', 'zip', 'rar', 'gz', 'bz2'],
];
if (empty($extArr[$dir])) {
echo CJSON::encode(['error' => 1, 'message' => '目錄名不正確']); exit;
}
$maxSize = 2 * 1024 * 1024; // 2MB
$uploadImage = CUploadedFile::getInstanceByName('imgFile');
Yii::import('ext.KEditor.KEditor');
$uploadDir = KEditor::getUploadPath() . '/' . $dir;
if (!file_exists($uploadDir)) mkdir($uploadDir);
$dateDir = $uploadDir . '/' . date('Ymd');
if (!file_exists($dateDir)) mkdir($dateDir);
$uploadUrl = KEditor::getUploadUrl() . '/' . $dir . '/' . date('Ymd');
if ($uploadImage instanceof CUploadedFile) {
if ($uploadImage->size > $maxSize) {
echo CJSON::encode(['error' => 1, 'message' => '文件過大']); exit;
}
$ext = $uploadImage->extensionName;
if (!in_array($ext, $extArr[$dir])) {
echo CJSON::encode(['error' => 1, 'message' => '不允許的擴展名']); exit;
}
$filename = date('YmdHis') . '_' . rand(10000, 99999) . '.' . $ext;
$savePath = $dateDir . '/' . $filename;
$fileUrl = $uploadUrl . '/' . $filename;
$uploadImage->saveAs($savePath);
echo CJSON::encode(['error' => 0, 'url' => $fileUrl]);
} else {
echo CJSON::encode(['error' => 1, 'message' => '上傳失敗']);
}
}
}
'params' => array(
'adminEmail' => 'webmaster@example.com',
'uploadPath' => '/upload', // 站點根目錄下 upload 文件夾
),
class DefaultController extends Controller {
public function actions() {
return array(
'upload' => array('class' => 'application.extensions.KEditor.KEditorUpload'),
'manageJson' => array('class' => 'application.extensions.KEditor.KEditorManage'),
);
}
}
<?php $this->widget('ext.KEditor.KEditor', array(
'model' => $model,
'name' => 'content',
'properties' => array(
'uploadJson' => '/admin/default/upload',
'fileManagerJson' => '/admin/default/manageJson',
'newlineTag' => 'br',
'allowFileManager' => true,
'afterCreate' => "js:function() {
K('#ChapterForm_all_len').val(this.count());
K('#ChapterForm_word_len').val(this.count('text'));
}",
'afterChange' => "js:function() {
K('#ChapterForm_all_len').val(this.count());
K('#ChapterForm_word_len').val(this.count('text'));
}",
),
'textareaOptions' => array(
'style' => 'width:98%;height:400px;',
),
)); ?>
var editor1 = K.create('#editor_modelname_name', {
uploadJson: "/admin/default/upload",
fileManagerJson: "/admin/default/manageJson",
newlineTag: "br",
allowFileManager: true,
afterCreate: function() {
K('#ChapterForm_all_len').html(this.count());
K('#ChapterForm_word_len').html(this.count('text'));
},
afterChange: function() {
K('#ChapterForm_all_len').html(this.count());
K('#ChapterForm_word_len').html(this.count('text'));
}
});
| 項目 | 措施 |
|---|---|
| 上傳目錄執行權限 | Nginx: location /upload { php_flag engine off; } Apache: php_admin_flag engine off |
| 文件類型驗證 | 雙重校驗:擴展名白名單 + getimagesize() 驗證圖片真實性 |
| 文件名隨機化 | 時間戳 + 隨機數,避免覆蓋與猜解 |
| 大小限制 | 前端配置 + 後端 $maxSize 雙重限制 |
| 瀏覽目錄遍歷防護 | strpos($currentPath, '..') 檢查 + realpath() 規範化 |
| 資源發布 | AssetManager 發布 keSource 目錄,支持調試/壓縮切換 |
通過 KEditor Widget 封裝渲染、KEditorUpload 處理上傳、KEditorManage 提供文件瀏覽 三位一體架構,實現了 KindEditor 在 Yii 中的深度整合。核心優勢在於:配置屬性透傳、JS 回調支持、上傳/瀏覽路徑統一管理、AssetManager 自動發布靜態資源。此模式同樣適用於 CKEditor、TinyMCE、UEditor 等編輯器整合。