Plugins.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. <?php
  2. namespace app\service\admin;
  3. use app\model\admin\Menu;
  4. use laytp\library\DirFile;
  5. use laytp\library\Http;
  6. use laytp\traits\Error;
  7. use app\model\Migrations;
  8. use think\facade\Config;
  9. use think\facade\Filesystem;
  10. /**
  11. * 插件市场服务实现者
  12. * Class Auth
  13. * @package app\service\admin
  14. */
  15. class Plugins
  16. {
  17. use Error;
  18. protected $ids=[];
  19. // 离线安装
  20. public function offLineInstall()
  21. {
  22. if (!class_exists('ZipArchive')) {
  23. $this->setError('PHP扩展ZipArchive没有正确安装');
  24. return false;
  25. }
  26. $file = request()->file('laytpUploadFile'); // 获取上传的文件
  27. if (!$file) {
  28. $this->setError('上传失败,请选择需要上传的文件');
  29. return false;
  30. }
  31. $fileExt = strtolower($file->getOriginalExtension());
  32. if($fileExt != 'zip'){
  33. $this->setError('仅允许上传zip文件');
  34. return false;
  35. }
  36. $fileName = $file->getOriginalName();
  37. $pathinfo = pathinfo($fileName);
  38. $plugin = $pathinfo['filename'];
  39. try{
  40. // 将文件上传到指定目录
  41. Filesystem::disk('local')->putFileAs('plugins', $file, $fileName );
  42. $this->insideInstall($plugin);
  43. }catch (\Exception $e){
  44. $this->setError($e->getMessage());
  45. return false;
  46. }
  47. return true;
  48. }
  49. // 安装
  50. public function install($plugin, $laytpGwToken)
  51. {
  52. if (!class_exists('ZipArchive')) {
  53. $this->setError('PHP扩展ZipArchive没有正确安装');
  54. return false;
  55. }
  56. $download = $this->download($plugin, $laytpGwToken);
  57. if(!$download){
  58. return false;
  59. }
  60. $install = $this->insideInstall($plugin);
  61. if(!$install){
  62. return false;
  63. }
  64. return true;
  65. }
  66. // 卸载
  67. public function unInstall($plugin)
  68. {
  69. $pluginDir = $this->getPluginPath($plugin) . DS;
  70. // 删除数据库文件
  71. $migrationsFile = DirFile::recurDir($pluginDir . 'database' . DS . 'migrations');
  72. if($migrationsFile){
  73. foreach($migrationsFile as $file){
  74. $baseNameArr = explode('_', $file['baseName']);
  75. $baseNameArr = explode('.', $baseNameArr[1]);
  76. $migration = Migrations::where('migration_name', '=', ucfirst($baseNameArr[0]))->find();
  77. if($migration) $migration->delete();
  78. @unlink(root_path() . 'database' . DS . 'migrations' . DS . $file['baseName']);
  79. }
  80. }
  81. // 删除菜单
  82. $info = $this->getPluginInfo($plugin);
  83. $menuIds = $info['menu_ids'];
  84. if($menuIds){
  85. Menu::destroy(function($query) use ($menuIds){
  86. $query->where('id', 'in', explode(',', $menuIds));
  87. });
  88. }
  89. // 删除public目录下的文件
  90. $this->removePublicFile($plugin);
  91. // 删除插件目录
  92. DirFile::rmDirs($pluginDir);
  93. // 修改系统插件配置文件config/plugin.php
  94. $this->unInstallPluginConf($plugin, $info);
  95. return true;
  96. }
  97. // 获取上传zip文件所在目录
  98. public function getPluginRuntimeDir()
  99. {
  100. $dir = runtime_path() . 'storage' . DS . 'plugins';
  101. DirFile::createDir($dir);
  102. return $dir;
  103. }
  104. // 下载zip文件到本地
  105. public function download($plugin, $laytpGwToken)
  106. {
  107. $ltVersion = request()->param('ltVersion');
  108. $pluginVersion = request()->param('pluginVersion');
  109. $res = Http::post(Config::get('plugin.apiUrl') . "/plugins/install", [
  110. 'plugin'=>$plugin,
  111. 'ltVersion'=>$ltVersion,
  112. 'pluginVersion'=>$pluginVersion,
  113. ], array(
  114. CURLOPT_HTTPHEADER => array(
  115. "token: ".$laytpGwToken
  116. ),
  117. ));
  118. $resArr = json_decode($res, true);
  119. if($resArr['code'] > 0 ){
  120. $this->setError(['msg'=>$resArr['msg'],'code'=>$resArr['code']]);
  121. return false;
  122. }
  123. $url = $resArr['data']['url'];
  124. $zipSteam = Http::get($url, [], [
  125. CURLOPT_CONNECTTIMEOUT => 30,
  126. CURLOPT_TIMEOUT => 30,
  127. CURLOPT_SSL_VERIFYPEER => false,
  128. CURLOPT_HTTPHEADER => [
  129. 'X-REQUESTED-WITH: XMLHttpRequest'
  130. ]
  131. ]);
  132. $file = $this->getPluginRuntimeDir() . DS . $plugin . '.zip';
  133. if(file_exists($file)){
  134. @unlink($file);
  135. }
  136. if ($write = fopen($file, 'w')) {
  137. fwrite($write, $zipSteam);
  138. fclose($write);
  139. }
  140. return true;
  141. }
  142. // 内部安装过程
  143. public function insideInstall($plugin)
  144. {
  145. try{
  146. // 解压zip文件
  147. $this->unzip($plugin);
  148. // 复制database文件并执行php think migrate:run命令
  149. $this->migrate($plugin);
  150. // 复制静态文件
  151. if(!function_exists('exec')){
  152. $this->setError('php函数exec不允许执行');
  153. return false;
  154. }
  155. $this->copyPublicFile($plugin);
  156. // 生成菜单,同时将新增的菜单id写入info.ini配置文件中,便于卸载时同时删除菜单
  157. $this->createMenu($plugin);
  158. // 修改系统插件配置文件config/plugin.php
  159. $this->installPluginConf($plugin);
  160. return true;
  161. }catch (\Exception $e){
  162. $this->setError($e->getMessage(). $e->getLine() . $e->getFile());
  163. return false;
  164. }
  165. }
  166. /**
  167. * 解压插件zip文件
  168. * @param $plugin
  169. * @return bool
  170. * @throws \Exception
  171. */
  172. public function unzip($plugin)
  173. {
  174. $file = $this->getPluginRuntimeDir() . DS . $plugin . '.zip';
  175. $zip = new \ZipArchive;
  176. if ($zip->open($file) !== TRUE) {
  177. @unlink($file);
  178. throw new \Exception('不能打开zip文件');
  179. }
  180. if(strtolower(trim($zip->getNameIndex(0),'/')) == strtolower($plugin)){
  181. $dir = root_path() . 'plugin' . DS;
  182. }else{
  183. $dir = root_path() . 'plugin' . DS . $plugin . DS;
  184. }
  185. if (!$zip->extractTo($dir)) {
  186. $zip->close();
  187. @unlink($file);
  188. throw new \Exception('不能提取zip文件');
  189. }
  190. $zip->close();
  191. @unlink($file);
  192. return true;
  193. }
  194. // 插件代码文件解压后,执行php run:migrate命令,安装数据库文件
  195. public function migrate($plugin)
  196. {
  197. $pluginDir = $this->getPluginPath($plugin) . DS;
  198. if(is_dir($pluginDir . 'database' . DS . 'migrations')){
  199. DirFile::copyDirs($pluginDir . 'database' . DS . 'migrations', root_path() . 'database' . DS . 'migrations');
  200. // 删除数据库migrations表,已经安装过的版本
  201. $list = scandir($pluginDir . 'database' . DS . 'migrations');
  202. $migrationNameArr = [];
  203. foreach ($list as $value) {
  204. $pathinfo = pathinfo($value);
  205. if ($pathinfo['extension'] == 'php') {
  206. $tempArr = explode('_', $pathinfo['filename']);
  207. $migrationNameArr[] = ucfirst($tempArr[1]);
  208. }
  209. }
  210. Migrations::where('migration_name', 'in', $migrationNameArr)->delete();
  211. sleep(1);
  212. exec('php ' . app()->getRootPath() . '\think migrate:run');
  213. }
  214. return true;
  215. }
  216. // 获取插件路径
  217. public function getPluginPath($plugin)
  218. {
  219. return app()->getRootPath() . DS . 'plugin' . DS . $plugin;
  220. }
  221. // 获取插件信息
  222. public function getPluginInfo($plugin)
  223. {
  224. $pluginPath = $this->getPluginPath($plugin);
  225. $infoFile = $pluginPath . DS . 'info.ini';
  226. $info = [];
  227. if (is_file($infoFile)) {
  228. $info = parse_ini_file($infoFile, true, INI_SCANNER_TYPED) ?: [];
  229. }
  230. return $info;
  231. }
  232. /**
  233. * 设置插件配置信息
  234. * @param $plugin
  235. * @param $array
  236. * @return bool
  237. */
  238. public function setPluginInfo($plugin, $array)
  239. {
  240. $pluginPath = $this->getPluginPath($plugin);
  241. $file = $pluginPath . DS . 'info.ini';
  242. if (!isset($array['name'])) {
  243. $this->setError("插件配置写入失败");
  244. return false;
  245. }
  246. $res = array();
  247. foreach ($array as $key => $val) {
  248. if (is_array($val)) {
  249. $res[] = "[$key]";
  250. foreach ($val as $sKey => $sVal)
  251. $res[] = "$sKey = " . (is_numeric($sVal) ? $sVal : $sVal);
  252. } else
  253. $res[] = "$key = " . (is_numeric($val) ? $val : $val);
  254. }
  255. if ($handle = fopen($file, 'w')) {
  256. fwrite($handle, implode("\n", $res) . "\n");
  257. fclose($handle);
  258. } else {
  259. $this->setError("文件没有写入权限");
  260. return false;
  261. }
  262. return true;
  263. }
  264. // 生成插件菜单
  265. public function createMenu($plugin)
  266. {
  267. $menuFile = root_path() . 'plugin' . DS . $plugin . DS . 'menu.php';
  268. if(!is_file($menuFile)){
  269. return true;
  270. }
  271. $menus = include_once $menuFile;
  272. $info = $this->getPluginInfo($plugin);
  273. if(isset($info['parent_menu']) && $info['parent_menu'] === 'first'){
  274. $firstMenuId = Menu::where(['pid' => 0, 'is_show' => 1])->order(['sort'=>'desc', 'id'=>'asc'])->value('id');
  275. $ids = $this->createMenuIds($menus, $firstMenuId);
  276. }else{
  277. $ids = $this->createMenuIds($menus, 0);
  278. }
  279. $info['menu_ids'] = implode(',', $ids);
  280. if(!$this->setPluginInfo($plugin, $info)) return false;
  281. return true;
  282. }
  283. public function createMenuIds($menus, $pid=0)
  284. {
  285. foreach($menus as $menu){
  286. $id = Menu::insertGetId([
  287. 'name' => $menu['name'],
  288. 'des' => isset($menu['des']) ? $menu['des'] : '',
  289. 'href' => isset($menu['href']) ? $menu['href'] : '',
  290. 'rule' => isset($menu['rule']) ? $menu['rule'] : '',
  291. 'is_menu' => $menu['is_menu'],
  292. 'pid' => $pid,
  293. 'icon' => isset($menu['icon']) ? $menu['icon'] : ''
  294. ]);
  295. $this->ids[] = $id;
  296. if(isset($menu['children'])){
  297. self::createMenuIds($menu['children'],$id);
  298. }
  299. }
  300. return $this->ids;
  301. }
  302. // 复制静态文件,包括html css js
  303. public function copyPublicFile($plugin)
  304. {
  305. $pluginDir = $this->getPluginPath($plugin) . DS;
  306. if(is_dir($pluginDir . 'public')){
  307. DirFile::copyDirs($pluginDir . 'public', root_path() . 'public');
  308. }
  309. return true;
  310. }
  311. // 删除静态文件,包括html css js
  312. public function removePublicFile($plugin)
  313. {
  314. $pluginDir = $this->getPluginPath($plugin) . DS;
  315. $pluginHtmlDir = $pluginDir . 'public' . DS . 'admin' . DS . 'plugin' . DS . $plugin;
  316. $publicHtmlDir = root_path() . 'public' . DS . 'admin' . DS . 'plugin' . DS . $plugin;
  317. if(is_dir($pluginHtmlDir) && $publicHtmlDir){
  318. DirFile::rmDirs($publicHtmlDir);
  319. }
  320. $pluginStaticDir = $pluginDir . 'public' . DS . 'static' . DS . 'plugin' . DS . $plugin;
  321. $publicStaticDir = root_path() . 'public' . DS . 'static' . DS . 'plugin' . DS . $plugin;
  322. if(is_dir($pluginStaticDir) && $publicStaticDir){
  323. DirFile::rmDirs($publicStaticDir);
  324. }
  325. return true;
  326. }
  327. // 重新生成config/plugin.php文件
  328. public function installPluginConf($plugin)
  329. {
  330. $pluginConf = Config::get('plugin');
  331. $pluginConf['installed'][] = $plugin;
  332. $pluginConf['installed'] = array_unique($pluginConf['installed']);
  333. sort($pluginConf['installed']);
  334. $info = $this->getPluginInfo($plugin);
  335. if(isset($info['is_editor']) && $info['is_editor']){
  336. $pluginConf['installedEditor'][] = $plugin;
  337. $pluginConf['installedEditor'] = array_unique($pluginConf['installedEditor']);
  338. sort($pluginConf['installedEditor']);
  339. }
  340. $fileName = root_path() . DS . 'config' . DS . 'plugin.php';
  341. file_put_contents($fileName,"<?php\nreturn " . var_export($pluginConf,true) . ';');
  342. return true;
  343. }
  344. // 重新生成config/plugin.php文件
  345. public function unInstallPluginConf($plugin, $info)
  346. {
  347. $pluginConf = Config::get('plugin');
  348. foreach($pluginConf['installed'] as $k=>$installed){
  349. if($installed === $plugin) unset($pluginConf['installed'][$k]);
  350. }
  351. $pluginConf['installed'] = array_unique($pluginConf['installed']);
  352. sort($pluginConf['installed']);
  353. if(isset($info['is_editor']) && $info['is_editor']){
  354. foreach($pluginConf['installedEditor'] as $k=>$installedEditor){
  355. if($installedEditor === $plugin) unset($pluginConf['installedEditor'][$k]);
  356. }
  357. $pluginConf['installedEditor'] = array_unique($pluginConf['installedEditor']);
  358. sort($pluginConf['installedEditor']);
  359. }
  360. $fileName = root_path() . DS . 'config' . DS . 'plugin.php';
  361. file_put_contents($fileName,"<?php\nreturn " . var_export($pluginConf,true) . ';');
  362. return true;
  363. }
  364. }