Module.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | 海豚PHP框架 [ DolphinPHP ]
  4. // +----------------------------------------------------------------------
  5. // | 版权所有 2016~2019 广东卓锐软件有限公司 [ http://www.zrthink.com ]
  6. // +----------------------------------------------------------------------
  7. // | 官方网站: http://dolphinphp.com
  8. // +----------------------------------------------------------------------
  9. namespace app\admin\controller;
  10. use app\admin\model\Module as ModuleModel;
  11. use app\admin\model\Plugin as PluginModel;
  12. use app\admin\model\Menu as MenuModel;
  13. use app\admin\model\Action as ActionModel;
  14. use think\facade\Cache;
  15. use util\Database;
  16. use util\Sql;
  17. use util\File;
  18. use util\PHPZip;
  19. use util\Tree;
  20. use think\Db;
  21. use think\facade\Hook;
  22. use think\facade\Env;
  23. /**
  24. * 模块管理控制器
  25. * @package app\admin\controller
  26. */
  27. class Module extends Admin
  28. {
  29. /**
  30. * 模块首页
  31. * @param string $group 分组
  32. * @param string $type 显示类型
  33. * @author 蔡伟明 <314013107@qq.com>
  34. * @return mixed
  35. */
  36. public function index($group = 'local', $type = '')
  37. {
  38. // 配置分组信息
  39. $list_group = ['local' => '本地模块'];
  40. $tab_list = [];
  41. foreach ($list_group as $key => $value) {
  42. $tab_list[$key]['title'] = $value;
  43. $tab_list[$key]['url'] = url('index', ['group' => $key]);
  44. }
  45. // 监听tab钩子
  46. Hook::listen('module_index_tab_list', $tab_list);
  47. switch ($group) {
  48. case 'local':
  49. // 查询条件
  50. $keyword = $this->request->get('keyword', '');
  51. if (input('?param.status') && input('param.status') != '_all') {
  52. $status = input('param.status');
  53. } else {
  54. $status = '';
  55. }
  56. $ModuleModel = new ModuleModel();
  57. $result = $ModuleModel->getAll($keyword, $status);
  58. if ($result['modules'] === false) {
  59. $this->error($ModuleModel->getError());
  60. }
  61. $type_show = Cache::get('module_type_show');
  62. $type_show = $type != '' ? $type : ($type_show == false ? 'block' : $type_show);
  63. Cache::set('module_type_show', $type_show);
  64. $type = $type_show == 'block' ? 'list' : 'block';
  65. $this->assign('page_title', '模块管理');
  66. $this->assign('modules', $result['modules']);
  67. $this->assign('total', $result['total']);
  68. $this->assign('tab_nav', ['tab_list' => $tab_list, 'curr_tab' => $group]);
  69. $this->assign('type', $type);
  70. return $this->fetch();
  71. break;
  72. case 'online':
  73. return '<h2>正在建设中...</h2>';
  74. break;
  75. default:
  76. $this->error('非法操作');
  77. }
  78. }
  79. /**
  80. * 安装模块
  81. * @param string $name 模块标识
  82. * @param int $confirm 是否确认
  83. * @author 蔡伟明 <314013107@qq.com>
  84. * @throws \think\Exception
  85. * @throws \think\exception\PDOException
  86. */
  87. public function install($name = '', $confirm = 0)
  88. {
  89. // 设置最大执行时间和内存大小
  90. ini_set('max_execution_time', '0');
  91. ini_set('memory_limit', '1024M');
  92. if ($name == '') $this->error('模块不存在!');
  93. if ($name == 'admin' || $name == 'user') $this->error('禁止操作系统核心模块!');
  94. // 模块配置信息
  95. $module_info = ModuleModel::getInfoFromFile($name);
  96. if ($confirm == 0) {
  97. $need_module = [];
  98. $need_plugin = [];
  99. $table_check = [];
  100. // 检查模块依赖
  101. if (isset($module_info['need_module']) && !empty($module_info['need_module'])) {
  102. $need_module = $this->checkDependence('module', $module_info['need_module']);
  103. }
  104. // 检查插件依赖
  105. if (isset($module_info['need_plugin']) && !empty($module_info['need_plugin'])) {
  106. $need_plugin = $this->checkDependence('plugin', $module_info['need_plugin']);
  107. }
  108. // 检查数据表
  109. if (isset($module_info['tables']) && !empty($module_info['tables'])) {
  110. foreach ($module_info['tables'] as $table) {
  111. if (Db::query("SHOW TABLES LIKE '".config('database.prefix')."{$table}'")) {
  112. $table_check[] = [
  113. 'table' => config('database.prefix')."{$table}",
  114. 'result' => '<span class="text-danger">存在同名</span>'
  115. ];
  116. } else {
  117. $table_check[] = [
  118. 'table' => config('database.prefix')."{$table}",
  119. 'result' => '<i class="fa fa-check text-success"></i>'
  120. ];
  121. }
  122. }
  123. }
  124. $this->assign('need_module', $need_module);
  125. $this->assign('need_plugin', $need_plugin);
  126. $this->assign('table_check', $table_check);
  127. $this->assign('name', $name);
  128. $this->assign('page_title', '安装模块:'. $name);
  129. return $this->fetch();
  130. }
  131. // 执行安装文件
  132. $install_file = realpath(Env::get('app_path').$name.'/install.php');
  133. if (file_exists($install_file)) {
  134. @include($install_file);
  135. }
  136. // 执行安装模块sql文件
  137. $sql_file = realpath(Env::get('app_path').$name.'/sql/install.sql');
  138. if (file_exists($sql_file)) {
  139. if (isset($module_info['database_prefix']) && !empty($module_info['database_prefix'])) {
  140. $sql_statement = Sql::getSqlFromFile($sql_file, false, [$module_info['database_prefix'] => config('database.prefix')]);
  141. } else {
  142. $sql_statement = Sql::getSqlFromFile($sql_file);
  143. }
  144. if (!empty($sql_statement)) {
  145. foreach ($sql_statement as $value) {
  146. try{
  147. Db::execute($value);
  148. }catch(\Exception $e){
  149. $this->error('导入SQL失败,请检查install.sql的语句是否正确');
  150. }
  151. }
  152. }
  153. }
  154. // 添加菜单
  155. $menus = ModuleModel::getMenusFromFile($name);
  156. if (is_array($menus) && !empty($menus)) {
  157. if (false === $this->addMenus($menus, $name)) {
  158. $this->error('菜单添加失败,请重新安装');
  159. }
  160. }
  161. // 检查是否有模块设置信息
  162. if (isset($module_info['config']) && !empty($module_info['config'])) {
  163. $module_info['config'] = json_encode(parse_config($module_info['config']));
  164. }
  165. // 检查是否有模块授权配置
  166. if (isset($module_info['access']) && !empty($module_info['access'])) {
  167. $module_info['access'] = json_encode($module_info['access']);
  168. }
  169. // 检查是否有行为规则
  170. if (isset($module_info['action']) && !empty($module_info['action'])) {
  171. $ActionModel = new ActionModel;
  172. if (!$ActionModel->saveAll($module_info['action'])) {
  173. MenuModel::where('module', $name)->delete();
  174. $this->error('行为添加失败,请重新安装');
  175. }
  176. }
  177. // 将模块信息写入数据库
  178. $ModuleModel = new ModuleModel($module_info);
  179. $allowField = ['name','title','icon','description','author','author_url','config','access','version','identifier','status'];
  180. if ($ModuleModel->allowField($allowField)->save()) {
  181. // 复制静态资源目录
  182. File::copy_dir(Env::get('app_path'). $name. '/public', Env::get('root_path'). 'public');
  183. // 删除静态资源目录
  184. File::del_dir(Env::get('app_path'). $name. '/public');
  185. cache('modules', null);
  186. cache('module_all', null);
  187. // 记录行为
  188. action_log('module_install', 'admin_module', 0, UID, $module_info['title']);
  189. $this->success('模块安装成功', 'index');
  190. } else {
  191. MenuModel::where('module', $name)->delete();
  192. $this->error('模块安装失败');
  193. }
  194. }
  195. /**
  196. * 卸载模块
  197. * @param string $name 模块名
  198. * @param int $confirm 是否确认
  199. * @author 蔡伟明 <314013107@qq.com>
  200. * @return mixed
  201. * @throws \think\Exception
  202. * @throws \think\exception\PDOException
  203. */
  204. public function uninstall($name = '', $confirm = 0)
  205. {
  206. if ($name == '') $this->error('模块不存在!');
  207. if ($name == 'admin') $this->error('禁止操作系统模块!');
  208. // 模块配置信息
  209. $module_info = ModuleModel::getInfoFromFile($name);
  210. if ($confirm == 0) {
  211. $this->assign('name', $name);
  212. $this->assign('page_title', '卸载模块:'. $name);
  213. return $this->fetch();
  214. }
  215. // 执行卸载文件
  216. $uninstall_file = realpath(Env::get('app_path').$name.'/uninstall.php');
  217. if (file_exists($uninstall_file)) {
  218. @include($uninstall_file);
  219. }
  220. // 执行卸载模块sql文件
  221. $clear = $this->request->get('clear');
  222. if ($clear == 1) {
  223. $sql_file = realpath(Env::get('app_path').$name.'/sql/uninstall.sql');
  224. if (file_exists($sql_file)) {
  225. if (isset($module_info['database_prefix']) && !empty($module_info['database_prefix'])) {
  226. $sql_statement = Sql::getSqlFromFile($sql_file, false, [$module_info['database_prefix'] => config('database.prefix')]);
  227. } else {
  228. $sql_statement = Sql::getSqlFromFile($sql_file);
  229. }
  230. if (!empty($sql_statement)) {
  231. foreach ($sql_statement as $sql) {
  232. try{
  233. Db::execute($sql);
  234. }catch(\Exception $e){
  235. $this->error('卸载失败,请检查uninstall.sql的语句是否正确');
  236. }
  237. }
  238. }
  239. }
  240. }
  241. // 删除菜单
  242. if (false === MenuModel::where('module', $name)->delete()) {
  243. $this->error('菜单删除失败,请重新卸载');
  244. }
  245. // 删除授权信息
  246. if (false === Db::name('admin_access')->where('module', $name)->delete()) {
  247. $this->error('删除授权信息失败,请重新卸载');
  248. }
  249. // 删除行为规则
  250. if (false === Db::name('admin_action')->where('module', $name)->delete()) {
  251. $this->error('删除行为信息失败,请重新卸载');
  252. }
  253. // 删除模块信息
  254. if (ModuleModel::where('name', $name)->delete()) {
  255. // 复制静态资源目录
  256. File::copy_dir(Env::get('root_path'). 'public/static/'. $name, Env::get('app_path').$name.'/public/static/'. $name);
  257. // 删除静态资源目录
  258. File::del_dir(Env::get('root_path'). 'public/static/'. $name);
  259. cache('modules', null);
  260. cache('module_all', null);
  261. // 记录行为
  262. action_log('module_uninstall', 'admin_module', 0, UID, $module_info['title']);
  263. $this->success('模块卸载成功', 'index');
  264. } else {
  265. $this->error('模块卸载失败');
  266. }
  267. }
  268. /**
  269. * 更新模块配置
  270. * @param string $name 模块名
  271. * @author 蔡伟明 <314013107@qq.com>
  272. */
  273. public function update($name = '')
  274. {
  275. $name == '' && $this->error('缺少模块名!');
  276. $Module = ModuleModel::get(['name' => $name]);
  277. !$Module && $this->error('模块不存在,或未安装');
  278. // 模块配置信息
  279. $module_info = ModuleModel::getInfoFromFile($name);
  280. unset($module_info['name']);
  281. // 检查是否有模块设置信息
  282. if (isset($module_info['config']) && !empty($module_info['config'])) {
  283. $module_info['config'] = json_encode(parse_config($module_info['config']));
  284. } else {
  285. $module_info['config'] = '';
  286. }
  287. // 检查是否有模块授权配置
  288. if (isset($module_info['access']) && !empty($module_info['access'])) {
  289. $module_info['access'] = json_encode($module_info['access']);
  290. } else {
  291. $module_info['access'] = '';
  292. }
  293. // 更新模块信息
  294. if (false !== $Module->save($module_info)) {
  295. $this->success('模块配置更新成功');
  296. } else {
  297. $this->error('模块配置更新失败,请重试');
  298. }
  299. }
  300. /**
  301. * 导出模块
  302. * @param string $name 模块名
  303. * @author 蔡伟明 <314013107@qq.com>
  304. * @throws \think\db\exception\DataNotFoundException
  305. * @throws \think\db\exception\ModelNotFoundException
  306. * @throws \think\exception\DbException
  307. */
  308. public function export($name = '')
  309. {
  310. if ($name == '') $this->error('缺少模块名');
  311. $export_data = $this->request->get('export_data', '');
  312. if ($export_data == '') {
  313. $this->assign('page_title', '导出模块:'. $name);
  314. return $this->fetch();
  315. }
  316. // 模块导出目录
  317. $module_dir = Env::get('root_path'). 'export/module/'. $name;
  318. // 删除旧的导出数据
  319. if (is_dir($module_dir)) {
  320. File::del_dir($module_dir);
  321. }
  322. // 复制模块目录到导出目录
  323. File::copy_dir(Env::get('app_path'). $name, $module_dir);
  324. // 复制静态资源目录
  325. File::copy_dir(Env::get('root_path'). 'public/static/'. $name, $module_dir.'/public/static/'. $name);
  326. // 模块本地配置信息
  327. $module_info = ModuleModel::getInfoFromFile($name);
  328. // 检查是否有模块设置信息
  329. if (isset($module_info['config'])) {
  330. $db_config = ModuleModel::where('name', $name)->value('config');
  331. $db_config = json_decode($db_config, true);
  332. // 获取最新的模块设置信息
  333. $module_info['config'] = set_config_value($module_info['config'], $db_config);
  334. }
  335. // 检查是否有模块行为信息
  336. $action = Db::name('admin_action')->where('module', $name)->field('module,name,title,remark,rule,log,status')->select();
  337. if ($action) {
  338. $module_info['action'] = $action;
  339. }
  340. // 表前缀
  341. $module_info['database_prefix'] = config('database.prefix');
  342. // 生成配置文件
  343. if (false === $this->buildInfoFile($module_info, $name)) {
  344. $this->error('模块配置文件创建失败,请重新导出');
  345. }
  346. // 获取模型菜单并导出
  347. $fields = 'id,pid,title,icon,url_type,url_value,url_target,online_hide,sort,status';
  348. $menus = MenuModel::getMenusByGroup($name, $fields);
  349. if (false === $this->buildMenuFile($menus, $name)) {
  350. $this->error('模型菜单文件创建失败,请重新导出');
  351. }
  352. // 导出数据库表
  353. if (isset($module_info['tables']) && !empty($module_info['tables'])) {
  354. if (!is_dir($module_dir. '/sql')) {
  355. mkdir($module_dir. '/sql', 644, true);
  356. }
  357. if (!Database::export($module_info['tables'], $module_dir. '/sql/install.sql', config('database.prefix'), $export_data)) {
  358. $this->error('数据库文件创建失败,请重新导出');
  359. }
  360. if (!Database::exportUninstall($module_info['tables'], $module_dir. '/sql/uninstall.sql', config('database.prefix'))) {
  361. $this->error('数据库文件创建失败,请重新导出');
  362. }
  363. }
  364. // 记录行为
  365. action_log('module_export', 'admin_module', 0, UID, $module_info['title']);
  366. // 打包下载
  367. $archive = new PHPZip;
  368. return $archive->ZipAndDownload($module_dir, $name);
  369. }
  370. /**
  371. * 创建模块菜单文件
  372. * @param array $menus 菜单
  373. * @param string $name 模块名
  374. * @author 蔡伟明 <314013107@qq.com>
  375. * @return int
  376. */
  377. private function buildMenuFile($menus = [], $name = '')
  378. {
  379. $menus = Tree::toLayer($menus);
  380. // 美化数组格式
  381. $menus = var_export($menus, true);
  382. $menus = preg_replace("/(\d+|'id'|'pid') =>(.*)/", '', $menus);
  383. $menus = preg_replace("/'child' => (.*)(\r\n|\r|\n)\s*array/", "'child' => $1array", $menus);
  384. $menus = str_replace(['array (', ')'], ['[', ']'], $menus);
  385. $menus = preg_replace("/(\s*?\r?\n\s*?)+/", "\n", $menus);
  386. $content = <<<INFO
  387. <?php
  388. // +----------------------------------------------------------------------
  389. // | 海豚PHP框架 [ DolphinPHP ]
  390. // +----------------------------------------------------------------------
  391. // | 版权所有 2016~2019 广东卓锐软件有限公司 [ http://www.zrthink.com ]
  392. // +----------------------------------------------------------------------
  393. // | 官方网站: http://dolphinphp.com
  394. // +----------------------------------------------------------------------
  395. /**
  396. * 菜单信息
  397. */
  398. return {$menus};
  399. INFO;
  400. // 写入到文件
  401. return file_put_contents(Env::get('root_path'). 'export/module/'. $name. '/menus.php', $content);
  402. }
  403. /**
  404. * 创建模块配置文件
  405. * @param array $info 模块配置信息
  406. * @param string $name 模块名
  407. * @author 蔡伟明 <314013107@qq.com>
  408. * @return int
  409. */
  410. private function buildInfoFile($info = [], $name = '')
  411. {
  412. // 美化数组格式
  413. $info = var_export($info, true);
  414. $info = preg_replace("/'(.*)' => (.*)(\r\n|\r|\n)\s*array/", "'$1' => array", $info);
  415. $info = preg_replace("/(\d+) => (\s*)(\r\n|\r|\n)\s*array/", "array", $info);
  416. $info = preg_replace("/(\d+ => )/", "", $info);
  417. $info = preg_replace("/array \((\r\n|\r|\n)\s*\)/", "[)", $info);
  418. $info = preg_replace("/array \(/", "[", $info);
  419. $info = preg_replace("/\)/", "]", $info);
  420. $content = <<<INFO
  421. <?php
  422. // +----------------------------------------------------------------------
  423. // | 海豚PHP框架 [ DolphinPHP ]
  424. // +----------------------------------------------------------------------
  425. // | 版权所有 2016~2019 广东卓锐软件有限公司 [ http://www.zrthink.com ]
  426. // +----------------------------------------------------------------------
  427. // | 官方网站: http://dolphinphp.com
  428. // +----------------------------------------------------------------------
  429. /**
  430. * 模块信息
  431. */
  432. return {$info};
  433. INFO;
  434. // 写入到文件
  435. return file_put_contents(Env::get('root_path'). 'export/module/'. $name. '/info.php', $content);
  436. }
  437. /**
  438. * 设置状态
  439. * @param string $type 类型:disable/enable
  440. * @param array $record 行为日志内容
  441. * @author 蔡伟明 <314013107@qq.com>
  442. * @throws \think\db\exception\DataNotFoundException
  443. * @throws \think\db\exception\ModelNotFoundException
  444. * @throws \think\exception\DbException
  445. */
  446. public function setStatus($type = '', $record = [])
  447. {
  448. $ids = input('param.ids');
  449. empty($ids) && $this->error('缺少主键');
  450. $module = ModuleModel::where('id', $ids)->find();
  451. $module['system_module'] == 1 && $this->error('禁止操作系统内置模块');
  452. $status = $type == 'enable' ? 1 : 0;
  453. // 将模块对应的菜单禁用或启用
  454. $map = [
  455. 'pid' => 0,
  456. 'module' => $module['name']
  457. ];
  458. MenuModel::where($map)->setField('status', $status);
  459. if (false !== ModuleModel::where('id', $ids)->setField('status', $status)) {
  460. // 记录日志
  461. call_user_func_array('action_log', ['module_'.$type, 'admin_module', 0, UID, $module['title']]);
  462. $this->success('操作成功');
  463. } else {
  464. $this->error('操作失败');
  465. }
  466. }
  467. /**
  468. * 禁用模块
  469. * @param array $record 行为日志内容
  470. * @author 蔡伟明 <314013107@qq.com>
  471. * @throws \think\db\exception\DataNotFoundException
  472. * @throws \think\db\exception\ModelNotFoundException
  473. * @throws \think\exception\DbException
  474. */
  475. public function disable($record = [])
  476. {
  477. $this->setStatus('disable');
  478. }
  479. /**
  480. * 启用模块
  481. * @param array $record 行为日志内容
  482. * @author 蔡伟明 <314013107@qq.com>
  483. * @throws \think\db\exception\DataNotFoundException
  484. * @throws \think\db\exception\ModelNotFoundException
  485. * @throws \think\exception\DbException
  486. */
  487. public function enable($record = [])
  488. {
  489. $this->setStatus('enable');
  490. }
  491. /**
  492. * 添加模型菜单
  493. * @param array $menus 菜单
  494. * @param string $module 模型名称
  495. * @param int $pid 父级ID
  496. * @author 蔡伟明 <314013107@qq.com>
  497. * @return bool
  498. */
  499. private function addMenus($menus = [], $module = '', $pid = 0)
  500. {
  501. foreach ($menus as $menu) {
  502. $data = [
  503. 'pid' => $pid,
  504. 'module' => $module,
  505. 'title' => $menu['title'],
  506. 'icon' => isset($menu['icon']) ? $menu['icon'] : 'fa fa-fw fa-puzzle-piece',
  507. 'url_type' => isset($menu['url_type']) ? $menu['url_type'] : 'module_admin',
  508. 'url_value' => isset($menu['url_value']) ? $menu['url_value'] : '',
  509. 'url_target' => isset($menu['url_target']) ? $menu['url_target'] : '_self',
  510. 'online_hide' => isset($menu['online_hide']) ? $menu['online_hide'] : 0,
  511. 'status' => isset($menu['status']) ? $menu['status'] : 1
  512. ];
  513. $result = MenuModel::create($data);
  514. if (!$result) return false;
  515. if (isset($menu['child'])) {
  516. $this->addMenus($menu['child'], $module, $result['id']);
  517. }
  518. }
  519. return true;
  520. }
  521. /**
  522. * 检查依赖
  523. * @param string $type 类型:module/plugin
  524. * @param array $data 检查数据
  525. * @author 蔡伟明 <314013107@qq.com>
  526. * @return array
  527. */
  528. private function checkDependence($type = '', $data = [])
  529. {
  530. $need = [];
  531. foreach ($data as $key => $value) {
  532. if (!isset($value[3])) {
  533. $value[3] = '=';
  534. }
  535. // 当前版本
  536. if ($type == 'module') {
  537. $curr_version = ModuleModel::where('identifier', $value[1])->value('version');
  538. } else {
  539. $curr_version = PluginModel::where('identifier', $value[1])->value('version');
  540. }
  541. // 比对版本
  542. $result = version_compare($curr_version, $value[2], $value[3]);
  543. $need[$key] = [
  544. $type => $value[0],
  545. 'identifier' => $value[1],
  546. 'version' => $curr_version ? $curr_version : '未安装',
  547. 'version_need' => $value[3].$value[2],
  548. 'result' => $result ? '<i class="fa fa-check text-success"></i>' : '<i class="fa fa-times text-danger"></i>'
  549. ];
  550. }
  551. return $need;
  552. }
  553. }