Plugin.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  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\common\builder\ZBuilder;
  11. use app\admin\model\Plugin as PluginModel;
  12. use app\admin\model\HookPlugin as HookPluginModel;
  13. use think\facade\Cache;
  14. use util\Sql;
  15. use think\Db;
  16. use think\facade\Hook;
  17. /**
  18. * 插件管理控制器
  19. * @package app\admin\controller
  20. */
  21. class Plugin extends Admin
  22. {
  23. /**
  24. * 首页
  25. * @param string $group 分组
  26. * @param string $type 显示类型
  27. * @author 蔡伟明 <314013107@qq.com>
  28. * @return mixed
  29. */
  30. public function index($group = 'local', $type = '')
  31. {
  32. // 配置分组信息
  33. $list_group = ['local' => '本地插件'];
  34. $tab_list = [];
  35. foreach ($list_group as $key => $value) {
  36. $tab_list[$key]['title'] = $value;
  37. $tab_list[$key]['url'] = url('index', ['group' => $key]);
  38. }
  39. // 监听tab钩子
  40. Hook::listen('plugin_index_tab_list', $tab_list);
  41. switch ($group) {
  42. case 'local':
  43. // 查询条件
  44. $keyword = $this->request->get('keyword', '');
  45. if (input('?param.status') && input('param.status') != '_all') {
  46. $status = input('param.status');
  47. } else {
  48. $status = '';
  49. }
  50. $PluginModel = new PluginModel;
  51. $result = $PluginModel->getAll($keyword, $status);
  52. if ($result['plugins'] === false) {
  53. $this->error($PluginModel->getError());
  54. }
  55. $type_show = Cache::get('plugin_type_show');
  56. $type_show = $type != '' ? $type : ($type_show == false ? 'block' : $type_show);
  57. Cache::set('plugin_type_show', $type_show);
  58. $type = $type_show == 'block' ? 'list' : 'block';
  59. $this->assign('page_title', '插件管理');
  60. $this->assign('plugins', $result['plugins']);
  61. $this->assign('total', $result['total']);
  62. $this->assign('tab_nav', ['tab_list' => $tab_list, 'curr_tab' => $group]);
  63. $this->assign('type', $type);
  64. return $this->fetch();
  65. break;
  66. case 'online':
  67. break;
  68. }
  69. }
  70. /**
  71. * 安装插件
  72. * @param string $name 插件标识
  73. * @author 蔡伟明 <314013107@qq.com>
  74. */
  75. public function install($name = '')
  76. {
  77. // 设置最大执行时间和内存大小
  78. ini_set('max_execution_time', '0');
  79. ini_set('memory_limit', '1024M');
  80. $plug_name = trim($name);
  81. if ($plug_name == '') $this->error('插件不存在!');
  82. $plugin_class = get_plugin_class($plug_name);
  83. if (!class_exists($plugin_class)) {
  84. $this->error('插件不存在!');
  85. }
  86. // 实例化插件
  87. $plugin = new $plugin_class;
  88. // 插件预安装
  89. if(!$plugin->install()) {
  90. $this->error('插件预安装失败!原因:'. $plugin->getError());
  91. }
  92. // 添加钩子
  93. if (isset($plugin->hooks) && !empty($plugin->hooks)) {
  94. if (!HookPluginModel::addHooks($plugin->hooks, $name)) {
  95. $this->error('安装插件钩子时出现错误,请重新安装');
  96. }
  97. cache('hook_plugins', null);
  98. }
  99. // 执行安装插件sql文件
  100. $sql_file = realpath(config('plugin_path').$name.'/install.sql');
  101. if (file_exists($sql_file)) {
  102. if (isset($plugin->database_prefix) && $plugin->database_prefix != '') {
  103. $sql_statement = Sql::getSqlFromFile($sql_file, false, [$plugin->database_prefix => config('database.prefix')]);
  104. } else {
  105. $sql_statement = Sql::getSqlFromFile($sql_file);
  106. }
  107. if (!empty($sql_statement)) {
  108. foreach ($sql_statement as $value) {
  109. Db::execute($value);
  110. }
  111. }
  112. }
  113. // 插件配置信息
  114. $plugin_info = $plugin->info;
  115. // 验证插件信息
  116. $result = $this->validate($plugin_info, 'Plugin');
  117. // 验证失败 输出错误信息
  118. if(true !== $result) $this->error($result);
  119. // 并入插件配置值
  120. $plugin_info['config'] = $plugin->getConfigValue();
  121. // 将插件信息写入数据库
  122. if (PluginModel::create($plugin_info)) {
  123. cache('plugin_all', null);
  124. $this->success('插件安装成功');
  125. } else {
  126. $this->error('插件安装失败');
  127. }
  128. }
  129. /**
  130. * 卸载插件
  131. * @param string $name 插件标识
  132. * @author 蔡伟明 <314013107@qq.com>
  133. * @throws \think\Exception
  134. * @throws \think\exception\PDOException
  135. */
  136. public function uninstall($name = '')
  137. {
  138. $plug_name = trim($name);
  139. if ($plug_name == '') $this->error('插件不存在!');
  140. $class = get_plugin_class($plug_name);
  141. if (!class_exists($class)) {
  142. $this->error('插件不存在!');
  143. }
  144. // 实例化插件
  145. $plugin = new $class;
  146. // 插件预卸
  147. if(!$plugin->uninstall()) {
  148. $this->error('插件预卸载失败!原因:'. $plugin->getError());
  149. }
  150. // 卸载插件自带钩子
  151. if (isset($plugin->hooks) && !empty($plugin->hooks)) {
  152. if (false === HookPluginModel::deleteHooks($plug_name)) {
  153. $this->error('卸载插件钩子时出现错误,请重新卸载');
  154. }
  155. cache('hook_plugins', null);
  156. }
  157. // 执行卸载插件sql文件
  158. $sql_file = realpath(config('plugin_path').$plug_name.'/uninstall.sql');
  159. if (file_exists($sql_file)) {
  160. if (isset($plugin->database_prefix) && $plugin->database_prefix != '') {
  161. $sql_statement = Sql::getSqlFromFile($sql_file, true, [$plugin->database_prefix => config('database.prefix')]);
  162. } else {
  163. $sql_statement = Sql::getSqlFromFile($sql_file, true);
  164. }
  165. if (!empty($sql_statement)) {
  166. Db::execute($sql_statement);
  167. }
  168. }
  169. // 删除插件信息
  170. if (PluginModel::where('name', $plug_name)->delete()) {
  171. cache('plugin_all', null);
  172. $this->success('插件卸载成功');
  173. } else {
  174. $this->error('插件卸载失败');
  175. }
  176. }
  177. /**
  178. * 插件管理
  179. * @param string $name 插件名
  180. * @author 蔡伟明 <314013107@qq.com>
  181. * @return mixed
  182. * @throws \think\Exception
  183. */
  184. public function manage($name = '')
  185. {
  186. cookie('__forward__', $_SERVER['REQUEST_URI']);
  187. // 加载自定义后台页面
  188. if (plugin_action_exists($name, 'Admin', 'index')) {
  189. return plugin_action($name, 'Admin', 'index');
  190. }
  191. // 加载系统的后台页面
  192. $class = get_plugin_class($name);
  193. if (!class_exists($class)) {
  194. $this->error($name.'插件不存在!');
  195. }
  196. // 实例化插件
  197. $plugin = new $class;
  198. // 获取后台字段信息,并分析
  199. if (isset($plugin->admin)) {
  200. $admin = $this->parseAdmin($plugin->admin);
  201. } else {
  202. $admin = $this->parseAdmin();
  203. }
  204. if (!plugin_model_exists($name)) {
  205. $this->error('插件: '.$name.' 缺少模型文件!');
  206. }
  207. // 获取插件模型实例
  208. $PluginModel = get_plugin_model($name);
  209. $order = $this->getOrder();
  210. $map = $this->getMap();
  211. $data_list = $PluginModel->where($map)->order($order)->paginate();
  212. $page = $data_list->render();
  213. // 使用ZBuilder快速创建数据表格
  214. $builder = ZBuilder::make('table')
  215. ->setPageTitle($admin['title']) // 设置页面标题
  216. ->setPluginName($name)
  217. ->setTableName($admin['table_name'])
  218. ->setSearch($admin['search_field'], $admin['search_title']) // 设置搜索框
  219. ->addOrder($admin['order'])
  220. ->addTopButton('back', [
  221. 'title' => '返回插件列表',
  222. 'icon' => 'fa fa-reply',
  223. 'href' => url('index')
  224. ])
  225. ->addTopButtons($admin['top_buttons']) // 批量添加顶部按钮
  226. ->addRightButtons($admin['right_buttons']); // 批量添加右侧按钮
  227. // 自定义顶部按钮
  228. if (!empty($admin['custom_top_buttons'])) {
  229. foreach ($admin['custom_top_buttons'] as $custom) {
  230. $builder->addTopButton('custom', $custom);
  231. }
  232. }
  233. // 自定义右侧按钮
  234. if (!empty($admin['custom_right_buttons'])) {
  235. foreach ($admin['custom_right_buttons'] as $custom) {
  236. $builder->addRightButton('custom', $custom);
  237. }
  238. }
  239. // 表头筛选
  240. if (is_array($admin['filter'])) {
  241. foreach ($admin['filter'] as $column => $params) {
  242. $options = isset($params[0]) ? $params[0] : [];
  243. $default = isset($params[1]) ? $params[1] : [];
  244. $type = isset($params[2]) ? $params[2] : 'checkbox';
  245. $builder->addFilter($column, $options, $default, $type);
  246. }
  247. } else {
  248. $builder->addFilter($admin['filter']);
  249. }
  250. return $builder
  251. ->addColumns($admin['columns']) // 批量添加数据列
  252. ->setRowList($data_list) // 设置表格数据
  253. ->setPages($page) // 设置分页数据
  254. ->fetch(); // 渲染模板
  255. }
  256. /**
  257. * 插件新增方法
  258. * @param string $plugin_name 插件名称
  259. * @author 蔡伟明 <314013107@qq.com>
  260. * @return mixed
  261. * @throws \think\Exception
  262. */
  263. public function add($plugin_name = '')
  264. {
  265. // 如果存在自定义的新增方法,则优先执行
  266. if (plugin_action_exists($plugin_name, 'Admin', 'add')) {
  267. $params = $this->request->param();
  268. return plugin_action($plugin_name, 'Admin', 'add', $params);
  269. }
  270. // 保存数据
  271. if ($this->request->isPost()) {
  272. $data = $this->request->post();
  273. // 执行插件的验证器(如果存在的话)
  274. if (plugin_validate_exists($plugin_name)) {
  275. $plugin_validate = get_plugin_validate($plugin_name);
  276. if (!$plugin_validate->check($data)) {
  277. // 验证失败 输出错误信息
  278. $this->error($plugin_validate->getError());
  279. }
  280. }
  281. // 实例化模型并添加数据
  282. $PluginModel = get_plugin_model($plugin_name);
  283. if ($PluginModel->data($data)->save()) {
  284. $this->success('新增成功', cookie('__forward__'));
  285. } else {
  286. $this->error('新增失败');
  287. }
  288. }
  289. // 获取插件模型
  290. $class = get_plugin_class($plugin_name);
  291. if (!class_exists($class)) {
  292. $this->error('插件不存在!');
  293. }
  294. // 实例化插件
  295. $plugin = new $class;
  296. if (!isset($plugin->fields)) {
  297. $this->error('插件新增、编辑字段不存在!');
  298. }
  299. // 使用ZBuilder快速创建表单
  300. return ZBuilder::make('form')
  301. ->setPageTitle('新增')
  302. ->addFormItems($plugin->fields)
  303. ->fetch();
  304. }
  305. /**
  306. * 编辑插件方法
  307. * @param string $id 数据id
  308. * @param string $plugin_name 插件名称
  309. * @author 蔡伟明 <314013107@qq.com>
  310. * @return mixed
  311. * @throws \think\Exception
  312. */
  313. public function edit($id = '', $plugin_name = '')
  314. {
  315. // 如果存在自定义的编辑方法,则优先执行
  316. if (plugin_action_exists($plugin_name, 'Admin', 'edit')) {
  317. $params = $this->request->param();
  318. return plugin_action($plugin_name, 'Admin', 'edit', $params);
  319. }
  320. // 保存数据
  321. if ($this->request->isPost()) {
  322. $data = $this->request->post();
  323. // 执行插件的验证器(如果存在的话)
  324. if (plugin_validate_exists($plugin_name)) {
  325. $plugin_validate = get_plugin_validate($plugin_name);
  326. if (!$plugin_validate->check($data)) {
  327. // 验证失败 输出错误信息
  328. $this->error($plugin_validate->getError());
  329. }
  330. }
  331. // 实例化模型并添加数据
  332. $PluginModel = get_plugin_model($plugin_name);
  333. if (false !== $PluginModel->isUpdate(true)->save($data)) {
  334. $this->success('编辑成功', cookie('__forward__'));
  335. } else {
  336. $this->error('编辑失败');
  337. }
  338. }
  339. // 获取插件类名
  340. $class = get_plugin_class($plugin_name);
  341. if (!class_exists($class)) {
  342. $this->error('插件不存在!');
  343. }
  344. // 实例化插件
  345. $plugin = new $class;
  346. if (!isset($plugin->fields)) {
  347. $this->error('插件新增、编辑字段不存在!');
  348. }
  349. // 获取数据
  350. $PluginModel = get_plugin_model($plugin_name);
  351. $info = $PluginModel->find($id);
  352. if (!$info) {
  353. $this->error('找不到数据!');
  354. }
  355. // 使用ZBuilder快速创建表单
  356. return ZBuilder::make('form')
  357. ->setPageTitle('编辑')
  358. ->addHidden('id')
  359. ->addFormItems($plugin->fields)
  360. ->setFormData($info)
  361. ->fetch();
  362. }
  363. /**
  364. * 插件参数设置
  365. * @param string $name 插件名称
  366. * @author 蔡伟明 <314013107@qq.com>
  367. * @return mixed
  368. * @throws \think\Exception
  369. * @throws \think\db\exception\DataNotFoundException
  370. * @throws \think\db\exception\ModelNotFoundException
  371. * @throws \think\exception\DbException
  372. * @throws \think\exception\PDOException
  373. */
  374. public function config($name = '')
  375. {
  376. // 更新配置
  377. if ($this->request->isPost()) {
  378. $data = $this->request->post();
  379. $data = json_encode($data);
  380. if (false !== PluginModel::where('name', $name)->update(['config' => $data])) {
  381. $this->success('更新成功', 'index');
  382. } else {
  383. $this->error('更新失败');
  384. }
  385. }
  386. $plugin_class = get_plugin_class($name);
  387. // 实例化插件
  388. $plugin = new $plugin_class;
  389. $trigger = isset($plugin->trigger) ? $plugin->trigger : [];
  390. // 插件配置值
  391. $info = PluginModel::where('name', $name)->field('id,name,config')->find();
  392. $db_config = json_decode($info['config'], true);
  393. // 插件配置项
  394. $config = include config('plugin_path'). $name. '/config.php';
  395. // 使用ZBuilder快速创建表单
  396. return ZBuilder::make('form')
  397. ->setPageTitle('插件设置')
  398. ->addFormItems($config)
  399. ->setFormData($db_config)
  400. ->setTrigger($trigger)
  401. ->fetch();
  402. }
  403. /**
  404. * 设置状态
  405. * @param string $type 状态类型:enable/disable
  406. * @param array $record 行为日志内容
  407. * @author 蔡伟明 <314013107@qq.com>
  408. * @throws \think\Exception
  409. * @throws \think\exception\PDOException
  410. */
  411. public function setStatus($type = '', $record = [])
  412. {
  413. $_t = input('param._t', '');
  414. $ids = $this->request->isPost() ? input('post.ids/a') : input('param.ids');
  415. empty($ids) && $this->error('缺少主键');
  416. $status = $type == 'enable' ? 1 : 0;
  417. if ($_t != '') {
  418. parent::setStatus($type, $record);
  419. } else {
  420. $plugins = PluginModel::where('id', 'in', $ids)->value('name');
  421. if ($plugins) {
  422. HookPluginModel::$type($plugins);
  423. }
  424. if (false !== PluginModel::where('id', 'in', $ids)->setField('status', $status)) {
  425. $this->success('操作成功');
  426. } else {
  427. $this->error('操作失败');
  428. }
  429. }
  430. }
  431. /**
  432. * 禁用插件/禁用插件数据
  433. * @param array $record 行为日志内容
  434. * @author 蔡伟明 <314013107@qq.com>
  435. * @throws \think\Exception
  436. * @throws \think\exception\PDOException
  437. */
  438. public function disable($record = [])
  439. {
  440. $this->setStatus('disable');
  441. }
  442. /**
  443. * 启用插件/启用插件数据
  444. * @param array $record 行为日志内容
  445. * @author 蔡伟明 <314013107@qq.com>
  446. * @throws \think\Exception
  447. * @throws \think\exception\PDOException
  448. */
  449. public function enable($record = [])
  450. {
  451. $this->setStatus('enable');
  452. }
  453. /**
  454. * 删除插件数据
  455. * @param array $record
  456. * @author 蔡伟明 <314013107@qq.com>
  457. * @throws \think\Exception
  458. * @throws \think\exception\PDOException
  459. */
  460. public function delete($record = [])
  461. {
  462. $this->setStatus('delete');
  463. }
  464. /**
  465. * 执行插件内部方法
  466. * @author 蔡伟明 <314013107@qq.com>
  467. * @return mixed
  468. */
  469. public function execute()
  470. {
  471. $plugin = input('param._plugin');
  472. $controller = input('param._controller');
  473. $action = input('param._action');
  474. $params = $this->request->except(['_plugin', '_controller', '_action'], 'param');
  475. if (empty($plugin) || empty($controller) || empty($action)) {
  476. $this->error('没有指定插件名称、控制器名称或操作名称');
  477. }
  478. if (!plugin_action_exists($plugin, $controller, $action)) {
  479. $this->error("找不到方法:{$plugin}/{$controller}/{$action}");
  480. }
  481. return plugin_action($plugin, $controller, $action, $params);
  482. }
  483. /**
  484. * 分析后台字段信息
  485. * @param array $data 字段信息
  486. * @author 蔡伟明 <314013107@qq.com>
  487. * @return array
  488. */
  489. private function parseAdmin($data = [])
  490. {
  491. $admin = [
  492. 'title' => '数据列表',
  493. 'search_title' => '',
  494. 'search_field' => [],
  495. 'order' => '',
  496. 'filter' => '',
  497. 'table_name' => '',
  498. 'columns' => [],
  499. 'right_buttons' => [],
  500. 'top_buttons' => [],
  501. 'customs' => [],
  502. ];
  503. if (empty($data)) {
  504. return $admin;
  505. }
  506. // 处理工具栏按钮链接
  507. if (isset($data['top_buttons']) && !empty($data['top_buttons'])) {
  508. $this->parseButton('top_buttons', $data);
  509. }
  510. // 处理右侧按钮链接
  511. if (isset($data['right_buttons']) && !empty($data['right_buttons'])) {
  512. $this->parseButton('right_buttons', $data);
  513. }
  514. return array_merge($admin, $data);
  515. }
  516. /**
  517. * 解析按钮链接
  518. * @param string $button 按钮名称
  519. * @param array $data 字段信息
  520. * @author 蔡伟明 <314013107@qq.com>
  521. */
  522. private function parseButton($button, &$data)
  523. {
  524. foreach ($data[$button] as $key => &$value) {
  525. // 处理自定义按钮
  526. if ($key === 'customs') {
  527. if (!empty($value)) {
  528. foreach ($value as &$custom) {
  529. if (isset($custom['href']['url']) && $custom['href']['url'] != '') {
  530. $params = isset($custom['href']['params']) ? $custom['href']['params'] : [];
  531. $custom['href'] = plugin_url($custom['href']['url'], $params);
  532. $data['custom_'.$button][] = $custom;
  533. }
  534. }
  535. }
  536. unset($data[$button][$key]);
  537. }
  538. if (!is_numeric($key) && isset($value['href']['url']) && $value['href']['url'] != '') {
  539. $value['href'] = plugin_url($value['href']['url']);
  540. }
  541. }
  542. }
  543. }