diff --git a/app/classes/UploadLogic.php b/app/classes/UploadLogic.php index 9b12a830c..f5100f7a6 100644 --- a/app/classes/UploadLogic.php +++ b/app/classes/UploadLogic.php @@ -236,6 +236,98 @@ public function move($fieldName, $fileType, $uuid = '', $originName = '', $origi return $this->uploadError('上传失败', 4); } + /** + * @param $fieldName + * @return array + */ + public function movePluginZip($fieldName) + { + //文件保存目录URL + //$saveUrl = ATTACHMENT_URL; + $savePath = STORAGE_PATH . "plugin_zip/"; + $relatePath = ''; + //PHP上传失败 + if ($_FILES[$fieldName]['error'] != UPLOAD_ERR_OK) { + switch ($_FILES[$fieldName]['error']) { + case '1': + $error = '超过php.ini允许的大小'; + break; + case '2': + $error = '超过表单允许的大小'; + break; + case '3': + $error = '图片只有部分被上传'; + break; + case '4': + $error = '没有文件被上传'; + break; + case '6': + $error = '找不到临时目录'; + break; + case '7': + $error = '写文件到硬盘出错'; + break; + case '8': + $error = '不允许的扩展名'; + break; + case '999': + default: + $error = '未知错误。'; + } + return $this->uploadError($error, (int)$_FILES[$fieldName]['error']); + } + //有上传文件时 + if (empty($_FILES) === false) { + //原文件名 + $fileName = $_FILES[$fieldName]['name']; + if (empty($originName)) { + $originName = $fileName; + } + //服务器上临时文件名 + $tmpName = $_FILES[$fieldName]['tmp_name']; + //检查文件名 + if (!$fileName) { + return $this->uploadError('请选择文件'); + } + //检查目录 + if (@is_dir($savePath) === false) { + return $this->uploadError("上传目录不存在"); + } + //检查目录写权限 + if (@is_writable($savePath) === false) { + return $this->uploadError("上传目录没有写权限"); + } + //获得文件扩展名 + $tempArr = explode(".", $fileName); + $fileExt = array_pop($tempArr); + $fileExt = trim($fileExt); + $fileExt = strtolower($fileExt); + if ($fileExt != 'zip') { + $msg = "上传文件的扩展名错误.只允许zip格式."; + return $this->uploadError($msg); + } + //新文件名 + $newFileName = date("YmdHis") . '_' . rand(10000, 99999) . '.' . $fileExt; + //移动文件 + $filePath = $savePath . $newFileName; + if (move_uploaded_file($tmpName, $filePath) === false) { + return $this->uploadError("上传文件失败."); + } + @chmod($filePath, 0644); + //$fileUrl = $saveUrl . $newFileName; + $relatePath .= $newFileName; + $uuid = quickRandom() . mt_rand(10000, 999999); + return [ + 'message' => '上传成功', + 'error' => 0, + 'url' => '', + 'filename' => $originName, + 'relate_path' => $relatePath, + 'uuid' => $uuid + ]; + } + return $this->uploadError('上传失败', 4); + } /** * 统一返回上传返回值 * @param string $msg diff --git a/app/ctrl/BaseCtrl.php b/app/ctrl/BaseCtrl.php index c76b3a81d..d5cf583f8 100644 --- a/app/ctrl/BaseCtrl.php +++ b/app/ctrl/BaseCtrl.php @@ -605,17 +605,8 @@ public function getPluginDirArr($pluginDir) public function loadPlugin() { $pluginModel = new PluginModel(); - $plugins = $pluginModel->getRows('id, name, title'); - $pluginsKeyArr = array_column($plugins, null, 'name'); - $dirPluginArr = $this->getPluginDirArr(PLUGIN_PATH); - foreach ($dirPluginArr as $dirName => $item) { - if (!isset($pluginsKeyArr[$dirName])) { - $tmp = $item; - $tmp['status'] = PluginModel::STATUS_UNINSTALLED; - $tmp['is_system'] = '0'; - $plugins[] = $tmp; - } - } + $plugins = $pluginModel->getRows('id, name, title, status',['status'=>PluginModel::STATUS_INSTALLED]); + if ($plugins) { foreach ($plugins as $plugin) { $pluginName = $plugin['name']; diff --git a/app/ctrl/admin/PluginManager.php b/app/ctrl/admin/PluginManager.php index 92b78ca91..a94115dab 100644 --- a/app/ctrl/admin/PluginManager.php +++ b/app/ctrl/admin/PluginManager.php @@ -12,6 +12,8 @@ use main\app\model\issue\IssueStatusModel; use main\app\classes\IssueStatusLogic; use main\app\model\PluginModel; +use main\lib\FileUtil; +use ZipArchive; /** * 插件管理控制器 @@ -61,11 +63,11 @@ public function pageIndex() */ public function fetchAll() { - $filterType = isset($_GET['type']) ? $_GET['type']:'all'; - $filterRange = isset($_GET['range']) ? $_GET['range']:'all'; + $filterType = isset($_GET['type']) ? $_GET['type'] : 'all'; + $filterRange = isset($_GET['range']) ? $_GET['range'] : 'all'; $conditionArr = []; - if($filterType!='all'){ + if ($filterType != 'all') { $conditionArr['type'] = trimStr($filterType); } @@ -73,11 +75,11 @@ public function fetchAll() $model = new PluginModel(); $dbPluginsArr = $model->getRows('*', $conditionArr); foreach ($dbPluginsArr as $dbPlugin) { - if($filterType=='all' || $dbPlugin['type']==$filterType){ + if ($filterType == 'all' || $dbPlugin['type'] == $filterType) { $plugins[] = $dbPlugin; } } - $uninstallPlugins = []; + $uninstallPlugins = []; $pluginsKeyArr = array_column($dbPluginsArr, null, 'name'); $dirPluginArr = $this->getPluginDirArr(PLUGIN_PATH); foreach ($dirPluginArr as $dirName => $item) { @@ -87,36 +89,48 @@ public function fetchAll() $tmp['status'] = PluginModel::STATUS_UNINSTALLED; $tmp['is_system'] = '0'; if ($filterType == 'all' || $tmp['type'] == $filterType) { - $uninstallPlugins[] = $tmp; + $uninstallPlugins[] = $tmp; } } } // var_dump($filterRange); - if($filterRange=='all') { + if ($filterRange == 'all') { foreach ($uninstallPlugins as $uninstallPlugin) { $plugins[] = $uninstallPlugin; } } - if($filterRange=='uninstalled') { - $plugins = $uninstallPlugins; + if ($filterRange == 'uninstalled') { + $dbUninstalledPluginsArr = $model->getRows('*', ['status'=>PluginModel::STATUS_UNINSTALLED]); + $plugins = $dbUninstalledPluginsArr + $uninstallPlugins; + } + if ($filterRange == 'installed') { + $plugins = $model->getRows('*', ['status'=>PluginModel::STATUS_INSTALLED]); } // print_r($uninstallPlugins); // 判断目录是否存在 - if($plugins){ + if ($plugins) { foreach ($plugins as & $plugin) { $name = $plugin['name']; if (!isset($dirPluginArr[$name])) { $plugin['status'] = PluginModel::STATUS_INVALID; } + $jsonFile = PLUGIN_PATH . $name . '/plugin.json'; + if ( !file_exists($jsonFile) ) { + $plugin['status'] = PluginModel::STATUS_INVALID; + } + $jsonArr = json_decode(file_get_contents($jsonFile, true)); + if ( !$jsonArr ) { + $plugin['status'] = PluginModel::STATUS_INVALID; + } $plugin['status_text'] = PluginModel::$statusArr[$plugin['status']]; $plugin['type_text'] = @PluginModel::$typeArr[$plugin['type']]; } } - + sort($plugins); $data = []; - $data['installed_count'] = count($dbPluginsArr); - $data['uninstalled_count'] = count($uninstallPlugins); + $data['installed_count'] = $model->getCount(['status'=>PluginModel::STATUS_INSTALLED]); + $data['uninstalled_count'] = count($uninstallPlugins)+$model->getCount(['status'=>PluginModel::STATUS_UNINSTALLED]); $data['plugins'] = $plugins; $this->ajaxSuccess('', $data); } @@ -146,34 +160,52 @@ public function get($id) } /** + * 导入插件 * @throws \Doctrine\DBAL\DBALException * @throws \Exception */ - public function install() + public function import() { - // 发布安装事件 if (empty($_POST)) { $this->ajaxFailed('错误', '没有提交表单数据'); } $_POST['name'] = trimStr($_POST['name']); if (isset($_POST['name']) && empty($_POST['name'])) { - $errorMsg['name'] = '名称不能为空'; + $errorMsg['name'] = '插件名称不能为空'; } $model = new PluginModel(); if (isset($model->getByName($_POST['name'])['id'])) { - $errorMsg['name'] = '名称已经被使用或已经安装了'; + $errorMsg['name'] = '名称已经被使用或已经安装了,请先删除'; } if (!empty($errorMsg)) { $this->ajaxFailed('参数错误', $errorMsg, BaseCtrl::AJAX_FAILED_TYPE_FORM_ERROR); } $pluginName = $_POST['name']; + $zipFile = $_POST['zip']; + $filePath = STORAGE_PATH . "plugin_zip/" . $zipFile; + if (!file_exists($filePath)) { + $this->ajaxFailed('提示', '参数错误,上传的zip文件丢失'); + } + if (file_exists(PLUGIN_PATH . $pluginName)) { + $this->ajaxFailed('提示', '相同的插件目录名称已经存在,请先删除'); + } + //var_dump($filePath); + $zip = new ZipArchive; + $res = $zip->open($filePath); + if ($res !== true) { + $this->ajaxFailed('提示', '上传的zip打开失败,错误代码:'.$res); + } + $zip->extractTo(PRE_APP_PATH); + $zip->close(); $jsonFile = PLUGIN_PATH . $pluginName . '/plugin.json'; if (!file_exists($jsonFile)) { $this->ajaxFailed('提示', '参数错误,配置文件plugin.json缺失'); } - $jsonArr = json_decode(file_get_contents($jsonFile), true); + if (!$jsonArr) { + $this->ajaxFailed('提示', '配置文件plugin.json格式错误,请检查'); + } $info = []; $info['name'] = $pluginName; $info['title'] = $jsonArr['title']; @@ -181,21 +213,75 @@ public function install() $info['url'] = $jsonArr['url']; $info['version'] = $jsonArr['version']; $info['icon_file'] = $jsonArr['icon_file']; - $info['icon_file'] = $jsonArr['icon_file']; $info['company'] = $jsonArr['company']; $info['description'] = $jsonArr['description']; - $info['status'] = PluginModel::STATUS_INSTALLED; + $info['status'] = PluginModel::STATUS_UNINSTALLED; $info['is_system'] = '0'; - list($ret, $msg) = $model->replace($info); if ($ret) { - // 发布安装事件 - $event = new PluginPlacedEvent($this, $info); - $this->dispatcher->dispatch($event, $pluginName.'@'.Events::onPluginInstall); - $this->ajaxSuccess('提示', '安装成功'); + @unlink($filePath); + $this->ajaxSuccess('提示', '导入成功'); } else { $this->ajaxFailed('服务器错误:', $msg); } + + } + + + /** + * @throws \Doctrine\DBAL\DBALException + * @throws \Exception + */ + public function install() + { + // 发布安装事件 + if (empty($_POST)) { + $this->ajaxFailed('错误', '没有提交表单数据'); + } + $_POST['name'] = trimStr($_POST['name']); + if (isset($_POST['name']) && empty($_POST['name'])) { + $errorMsg['name'] = '名称不能为空'; + } + if (!empty($errorMsg)) { + $this->ajaxFailed('参数错误', $errorMsg, BaseCtrl::AJAX_FAILED_TYPE_FORM_ERROR); + } + $pluginName = $_POST['name']; + $model = new PluginModel(); + $pluginRow = $model->getByName($_POST['name']); + if (isset($pluginRow['id'])) { + $event = new PluginPlacedEvent($this, $pluginRow); + $this->dispatcher->dispatch($event, $pluginName . '@' .Events::onPluginInstall); + $model->updateById($pluginRow['id'], ['status'=>PluginModel::STATUS_INSTALLED]); + }else{ + + $jsonFile = PLUGIN_PATH . $pluginName . '/plugin.json'; + if (!file_exists($jsonFile)) { + $this->ajaxFailed('提示', '参数错误,配置文件plugin.json缺失'); + } + $jsonArr = json_decode(file_get_contents($jsonFile), true); + $info = []; + $info['name'] = $pluginName; + $info['title'] = $jsonArr['title']; + $info['type'] = $jsonArr['type']; + $info['url'] = $jsonArr['url']; + $info['version'] = $jsonArr['version']; + $info['icon_file'] = $jsonArr['icon_file']; + $info['icon_file'] = $jsonArr['icon_file']; + $info['company'] = $jsonArr['company']; + $info['description'] = $jsonArr['description']; + $info['status'] = PluginModel::STATUS_INSTALLED; + $info['is_system'] = '0'; + list($ret, $msg) = $model->replace($info); + if ($ret) { + // 发布安装事件 + $event = new PluginPlacedEvent($this, $info); + $this->dispatcher->dispatch($event, $pluginName . '@' . Events::onPluginInstall); + } else { + $this->ajaxFailed('服务器错误:', $msg); + } + } + $this->ajaxSuccess('提示', '安装成功'); + } /** @@ -223,7 +309,7 @@ public function unInstall() } else { // 发布卸载事件 $event = new PluginPlacedEvent($this, $plugin); - $this->dispatcher->dispatch($event, $plugin['name'].'@'.Events::onPluginUnInstall); + $this->dispatcher->dispatch($event, $plugin['name'] . '@' . Events::onPluginUnInstall); $this->ajaxSuccess('操作成功'); } } @@ -286,22 +372,22 @@ public function add() mkdir($pluginDirName); } $this->rcopy(PLUGIN_PATH . 'plugin_tpl', $pluginDirName); - $pluginClassName = Inflector::classify($name.'_plugin'); - $pluginClassFile = $pluginDirName."/PluginSubscriber.php"; - $pluginSrc = str_replace(["plugin_tpl","TplPlugin"],[$name,$pluginClassName], file_get_contents($pluginClassFile)); + $pluginClassName = Inflector::classify($name . '_plugin'); + $pluginClassFile = $pluginDirName . "/PluginSubscriber.php"; + $pluginSrc = str_replace(["plugin_tpl", "TplPlugin"], [$name, $pluginClassName], file_get_contents($pluginClassFile)); file_put_contents($pluginClassFile, $pluginSrc); - $pluginIndexFile = $pluginDirName."/index.php"; - file_put_contents($pluginIndexFile, str_replace(["plugin_tpl"],[$name], file_get_contents($pluginIndexFile))); + $pluginIndexFile = $pluginDirName . "/index.php"; + file_put_contents($pluginIndexFile, str_replace(["plugin_tpl"], [$name], file_get_contents($pluginIndexFile))); rename($pluginDirName . '/plugin.json.tpl', $pluginDirName . '/plugin.json'); $replaceArr = []; - foreach ($info as $key =>$item) { - $replaceArr['{{'.$key.'}}'] = $item; + foreach ($info as $key => $item) { + $replaceArr['{{' . $key . '}}'] = $item; } $replaceArr['{{main_class}}'] = $pluginClassName; $jsonFile = $pluginDirName . '/plugin.json'; - $jsonSrc = str_replace(array_keys($replaceArr),array_values($replaceArr), file_get_contents($jsonFile)); + $jsonSrc = str_replace(array_keys($replaceArr), array_values($replaceArr), file_get_contents($jsonFile)); file_put_contents($jsonFile, $jsonSrc); //list($ret, $msg) = $model->insert($info); @@ -435,7 +521,7 @@ public function delete() $pluginRow = $model->getByName($name); if (!empty($pluginRow)) { //$errorMsg['name'] = '插件已卸载或不存在'; - if($pluginRow['is_system']=='1'){ + if ($pluginRow['is_system'] == '1') { $this->ajaxFailed('参数错误', '系统自带的插件不能删除'); } diff --git a/app/ctrl/admin/Upload.php b/app/ctrl/admin/Upload.php index 0d8932255..790016b10 100644 --- a/app/ctrl/admin/Upload.php +++ b/app/ctrl/admin/Upload.php @@ -4,6 +4,8 @@ use \main\app\ctrl\BaseAdminCtrl; use \main\app\classes\UploadLogic; +use main\lib\FileUtil; +use ZipArchive; /** * @@ -41,6 +43,62 @@ public function plugin() exit; } + public function pluginZip() + { + $uploadLogic = new UploadLogic(); + $ret = $uploadLogic->movePluginZip('qqfile'); + if($ret['error']==0){ + $ret['error'] = ''; + $ret['success'] = true; + $ret['plugin_arr'] = []; + $ret['name'] = ''; + $filePath = STORAGE_PATH . "plugin_zip/".$ret['relate_path']; + if (!extension_loaded('zip')) { + $ret['error'] = -1; + $ret['success'] = false; + $ret['message'] = 'zip扩展没有安装'; + } + @mkdir($filePath.'-unzip'); + $zip = new ZipArchive; + $res = $zip->open($filePath); + if ($res) { + $zip->extractTo($filePath.'-unzip'); + $zip->close(); + $dirNum = 0; + $dir = $filePath.'-unzip/plugin/'; + $pluginDirName = ''; + if ($dh = opendir($dir)) { + while (($file = readdir($dh)) !== false) { + if( $file == '.' || $file == '..'){ + continue; + } + if(is_dir($dir.$file)){ + $pluginDirName = $file; + $dirNum++; + } + } + closedir($dh); + } + if($dirNum>1){ + $ret['error'] = -3; + $ret['success'] = false; + $ret['message'] = 'plugin目录下只应该存在一个文件'; + } + $ret['name'] = $pluginDirName; + $pluginArr = json_decode(file_get_contents($dir."/{$pluginDirName}/plugin.json"), true); + $ret['plugin_arr'] = $pluginArr; + //rrmdir($dir); + FileUtil::unlinkDir($filePath.'-unzip'); + }else{ + $ret['error'] = -2; + $ret['success'] = false; + $ret['message'] = 'zip打开失败'; + } + } + header('Content-type: application/json; charset=UTF-8'); + echo json_encode($ret); + exit; + } /** * 上传头像 * @throws \Exception diff --git a/app/function/system.php b/app/function/system.php index f5f361fbe..c82c5ba0d 100644 --- a/app/function/system.php +++ b/app/function/system.php @@ -110,3 +110,25 @@ function mbstrlen($str) return $count; } +/** + * 删除文件夹 + * @param $src + */ +function rrmdir($src) { + $dir = opendir($src); + while(false !== ( $file = readdir($dir)) ) { + if (( $file != '.' ) && ( $file != '..' )) { + $full = $src . '/' . $file; + if ( is_dir($full) ) { + rrmdir($full); + } + else { + unlink($full); + } + } + } + closedir($dir); + rmdir($src); +} + + diff --git a/app/view/twig/admin/plugin/index.twig b/app/view/twig/admin/plugin/index.twig index 9339ee200..19449641b 100644 --- a/app/view/twig/admin/plugin/index.twig +++ b/app/view/twig/admin/plugin/index.twig @@ -23,87 +23,93 @@ {% include 'twig/common/body/page-left.twig' %}
-{% include 'twig/common/body/header-content.twig' %} + {% include 'twig/common/body/header-content.twig' %} - -
- {% include 'twig/admin/common-page-nav-admin.twig' %} + +
+ {% include 'twig/admin/common-page-nav-admin.twig' %} -
-
-
-
-
-
-
- {% include 'twig/admin/common_plugin_left_nav.twig' %} -
-
-
- -
- - - - - - -
-
+
- 名称只能以英文字母开始,不能包含特殊字符和中文 + 名称只能以英文字母开始,不能包含特殊字符和中文
@@ -214,11 +220,72 @@
+ + +