AddonService.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | Yzncms [ 御宅男工作室 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2018 http://yzncms.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
  8. // +----------------------------------------------------------------------
  9. // | Author: 御宅男 <530765310@qq.com>
  10. // +----------------------------------------------------------------------
  11. // +----------------------------------------------------------------------
  12. // | 插件服务类
  13. // +----------------------------------------------------------------------
  14. namespace think;
  15. use app\admin\model\Addons as AddonsModel;
  16. use app\common\model\Cache as CacheModel;
  17. use RecursiveDirectoryIterator;
  18. use RecursiveIteratorIterator;
  19. use think\Exception;
  20. use think\facade\Cache;
  21. use think\facade\Db;
  22. use util\File;
  23. use util\Sql;
  24. use ZipArchive;
  25. class AddonService
  26. {
  27. /**
  28. * 安装插件.
  29. *
  30. * @param string $name 插件名称
  31. * @param bool $force 是否覆盖
  32. * @param array $extend 扩展参数
  33. *
  34. * @throws Exception
  35. *
  36. * @return bool
  37. */
  38. public static function install($name, $force = false, $extend = [])
  39. {
  40. try {
  41. // 检查插件是否完整
  42. self::check($name);
  43. if (!$force) {
  44. self::noconflict($name);
  45. }
  46. } catch (Exception $e) {
  47. throw new Exception($e->getMessage());
  48. }
  49. /*if (!$name || (is_dir(ADDON_PATH . $name) && !$force)) {
  50. throw new Exception('插件已存在!');
  51. }*/
  52. foreach (self::getCheckDirs() as $k => $dir) {
  53. if (is_dir(ADDON_PATH . $name . DS . $dir)) {
  54. File::copy_dir(ADDON_PATH . $name . DS . $dir, app()->getRootPath() . $dir);
  55. }
  56. }
  57. //前台模板
  58. $installdir = ADDON_PATH . "{$name}" . DS . "install" . DS;
  59. if (is_dir($installdir . "template" . DS)) {
  60. //拷贝模板到前台模板目录中去
  61. File::copy_dir($installdir . "template" . DS, TEMPLATE_PATH . 'default' . DS);
  62. }
  63. //静态资源文件
  64. if (file_exists(ADDON_PATH . $name . DS . "install" . DS . "public" . DS)) {
  65. //拷贝模板到前台模板目录中去
  66. File::copy_dir(ADDON_PATH . $name . DS . "install" . DS . "public" . DS, app()->getRootPath() . 'public' . DS . 'static' . DS . 'addons' . DS . strtolower($name) . '/');
  67. }
  68. try {
  69. // 默认启用该插件
  70. $info = get_addon_info($name);
  71. if (!$info['status']) {
  72. $info['status'] = 1;
  73. set_addon_info($name, $info);
  74. }
  75. // 执行安装脚本
  76. $class = get_addon_class($name);
  77. if (class_exists($class)) {
  78. $addon = new $class();
  79. $addon->install();
  80. //缓存
  81. if (isset($addon->cache) && is_array($addon->cache)) {
  82. self::installAddonCache($addon->cache, $name);
  83. }
  84. }
  85. self::runSQL($name);
  86. AddonsModel::create($info);
  87. } catch (Exception $e) {
  88. throw new Exception($e->getMessage());
  89. }
  90. // 刷新
  91. self::refresh();
  92. return true;
  93. }
  94. /**
  95. * 卸载插件.
  96. *
  97. * @param string $name
  98. * @param bool $force 是否强制卸载
  99. *
  100. * @throws Exception
  101. *
  102. * @return bool
  103. */
  104. public static function uninstall($name, $force = false)
  105. {
  106. if (!$name || !is_dir(ADDON_PATH . $name)) {
  107. throw new Exception('插件不存在!');
  108. }
  109. // 移除插件全局资源文件
  110. if ($force) {
  111. $list = self::getGlobalFiles($name);
  112. foreach ($list as $k => $v) {
  113. @unlink(app()->getRootPath() . $v);
  114. }
  115. }
  116. //删除模块前台模板
  117. if (is_dir(TEMPLATE_PATH . 'default' . DS . $name . DS)) {
  118. File::del_dir(TEMPLATE_PATH . 'default' . DS . $name . DS);
  119. }
  120. //静态资源移除
  121. if (is_dir(app()->getRootPath() . 'public' . DS . 'static' . DS . 'addons' . DS . strtolower($name) . DS)) {
  122. File::del_dir(app()->getRootPath() . 'public' . DS . 'static' . DS . 'addons' . DS . strtolower($name) . DS);
  123. }
  124. // 执行卸载脚本
  125. try {
  126. // 默认禁用该插件
  127. $info = get_addon_info($name);
  128. if ($info['status']) {
  129. $info['status'] = 0;
  130. set_addon_info($name, $info);
  131. }
  132. $class = get_addon_class($name);
  133. if (class_exists($class)) {
  134. $addon = new $class();
  135. $addon->uninstall();
  136. //缓存
  137. if (isset($addon->cache) && is_array($addon->cache)) {
  138. CacheModel::where(['module' => $name, 'system' => 0])->delete();
  139. }
  140. };
  141. self::runSQL($name, 'uninstall');
  142. AddonsModel::where('name', $name)->delete();
  143. } catch (Exception $e) {
  144. throw new Exception($e->getMessage());
  145. }
  146. // 刷新
  147. self::refresh();
  148. return true;
  149. }
  150. /**
  151. * 启用.
  152. *
  153. * @param string $name 插件名称
  154. * @param bool $force 是否强制覆盖
  155. *
  156. * @return bool
  157. */
  158. public static function enable($name, $force = false)
  159. {
  160. if (!$name || !is_dir(ADDON_PATH . $name)) {
  161. throw new Exception('插件不存在!');
  162. }
  163. $info = get_addon_info($name);
  164. $info['status'] = 1;
  165. unset($info['url']);
  166. set_addon_info($name, $info);
  167. //执行启用脚本
  168. try {
  169. AddonsModel::update(['status' => 1], ['name' => $name]);
  170. $class = get_addon_class($name);
  171. if (class_exists($class)) {
  172. $addon = new $class();
  173. if (method_exists($class, 'enable')) {
  174. $addon->enable();
  175. }
  176. }
  177. } catch (Exception $e) {
  178. throw new Exception($e->getMessage());
  179. }
  180. // 刷新
  181. self::refresh();
  182. return true;
  183. }
  184. /**
  185. * 禁用.
  186. *
  187. * @param string $name 插件名称
  188. * @param bool $force 是否强制禁用
  189. *
  190. * @throws Exception
  191. *
  192. * @return bool
  193. */
  194. public static function disable($name, $force = false)
  195. {
  196. if (!$name || !is_dir(ADDON_PATH . $name)) {
  197. throw new Exception('插件不存在!');
  198. }
  199. $info = get_addon_info($name);
  200. $info['status'] = 0;
  201. unset($info['url']);
  202. set_addon_info($name, $info);
  203. // 执行禁用脚本
  204. try {
  205. AddonsModel::update(['status' => 0], ['name' => $name]);
  206. $class = get_addon_class($name);
  207. if (class_exists($class)) {
  208. $addon = new $class();
  209. if (method_exists($class, 'disable')) {
  210. $addon->disable();
  211. }
  212. }
  213. } catch (Exception $e) {
  214. throw new Exception($e->getMessage());
  215. }
  216. // 刷新
  217. self::refresh();
  218. return true;
  219. }
  220. /**
  221. * 刷新插件缓存文件.
  222. *
  223. * @throws Exception
  224. *
  225. * @return bool
  226. */
  227. public static function refresh()
  228. {
  229. $file = app()->getRootPath() . 'config' . DS . 'addons.php';
  230. $config = get_addon_autoload_config(true);
  231. if ($config['autoload']) {
  232. return;
  233. }
  234. if (!\util\File::is_really_writable($file)) {
  235. throw new Exception('addons.php文件没有写入权限');
  236. }
  237. if ($handle = fopen($file, 'w')) {
  238. fwrite($handle, "<?php\n\n" . 'return ' . var_export($config, true) . ';');
  239. fclose($handle);
  240. } else {
  241. throw new Exception('文件没有写入权限');
  242. }
  243. return true;
  244. }
  245. /**
  246. * 解压插件.
  247. *
  248. * @param string $name 插件名称
  249. *
  250. * @throws Exception
  251. *
  252. * @return string
  253. */
  254. public static function unzip($name)
  255. {
  256. $file = app()->getRootPath() . 'runtime' . DS . 'addons' . DS . $name . '.zip';
  257. $dir = ADDON_PATH . $name . DS;
  258. if (class_exists('ZipArchive')) {
  259. $zip = new ZipArchive();
  260. if ($zip->open($file) !== true) {
  261. throw new Exception('Unable to open the zip file');
  262. }
  263. if (!$zip->extractTo($dir)) {
  264. $zip->close();
  265. throw new Exception('Unable to extract the file');
  266. }
  267. $zip->close();
  268. return $dir;
  269. }
  270. throw new Exception('无法执行解压操作,请确保ZipArchive安装正确');
  271. }
  272. /**
  273. * 注册插件缓存
  274. * @return boolean
  275. */
  276. public static function installAddonCache(array $cache, $name)
  277. {
  278. $data = array();
  279. foreach ($cache as $key => $rs) {
  280. $add = array(
  281. 'key' => $key,
  282. 'name' => $rs['name'],
  283. 'module' => isset($rs['module']) ? $rs['module'] : $name,
  284. 'model' => $rs['model'],
  285. 'action' => $rs['action'],
  286. //'param' => isset($rs['param']) ? $rs['param'] : '',
  287. 'system' => 0,
  288. );
  289. CacheModel::create($add);
  290. }
  291. return true;
  292. }
  293. /**
  294. * 执行安装数据库脚本
  295. * @param type $name 模块名(目录名)
  296. * @return boolean
  297. */
  298. public static function runSQL($name = '', $Dir = 'install')
  299. {
  300. $sql_file = ADDON_PATH . "{$name}" . DS . "{$Dir}" . DS . "{$Dir}.sql";
  301. if (file_exists($sql_file)) {
  302. $sql_statement = Sql::getSqlFromFile($sql_file);
  303. if (!empty($sql_statement)) {
  304. foreach ($sql_statement as $value) {
  305. try {
  306. Db::execute($value);
  307. } catch (\Exception $e) {
  308. throw new Exception('导入SQL失败,请检查{$name}.sql的语句是否正确');
  309. }
  310. }
  311. }
  312. }
  313. return true;
  314. }
  315. /**
  316. * 是否有冲突
  317. *
  318. * @param string $name 插件名称
  319. * @return boolean
  320. * @throws AddonException
  321. */
  322. public static function noconflict($name)
  323. {
  324. // 检测冲突文件
  325. $list = self::getGlobalFiles($name, true);
  326. if ($list) {
  327. //发现冲突文件,抛出异常
  328. throw new Exception("发现冲突文件");
  329. }
  330. return true;
  331. }
  332. /**
  333. * 获取插件在全局的文件
  334. *
  335. * @param string $name 插件名称
  336. * @return array
  337. */
  338. public static function getGlobalFiles($name, $onlyconflict = false)
  339. {
  340. $list = [];
  341. $addonDir = ADDON_PATH . $name . DS;
  342. // 扫描插件目录是否有覆盖的文件
  343. foreach (self::getCheckDirs() as $k => $dir) {
  344. $checkDir = app()->getRootPath() . DS . $dir . DS;
  345. if (!is_dir($checkDir)) {
  346. continue;
  347. }
  348. //检测到存在插件外目录
  349. if (is_dir($addonDir . $dir)) {
  350. //匹配出所有的文件
  351. $files = new RecursiveIteratorIterator(
  352. new RecursiveDirectoryIterator($addonDir . $dir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST
  353. );
  354. foreach ($files as $fileinfo) {
  355. if ($fileinfo->isFile()) {
  356. $filePath = $fileinfo->getPathName();
  357. $path = str_replace($addonDir, '', $filePath);
  358. if ($onlyconflict) {
  359. $destPath = app()->getRootPath() . $path;
  360. if (is_file($destPath)) {
  361. if (filesize($filePath) != filesize($destPath) || md5_file($filePath) != md5_file($destPath)) {
  362. $list[] = $path;
  363. }
  364. }
  365. } else {
  366. $list[] = $path;
  367. }
  368. }
  369. }
  370. }
  371. }
  372. return $list;
  373. }
  374. /**
  375. * 获取检测的全局文件夹目录
  376. * @return array
  377. */
  378. protected static function getCheckDirs()
  379. {
  380. return [
  381. 'app',
  382. 'public',
  383. ];
  384. }
  385. /**
  386. * 检测插件是否完整.
  387. *
  388. * @param string $name 插件名称
  389. *
  390. * @throws Exception
  391. *
  392. * @return bool
  393. */
  394. public static function check($name)
  395. {
  396. if (!$name || !is_dir(ADDON_PATH . $name)) {
  397. throw new Exception('插件不存在!');
  398. }
  399. $addonClass = get_addon_class($name);
  400. if (!$addonClass) {
  401. throw new Exception('插件主启动程序不存在');
  402. }
  403. $addon = new $addonClass();
  404. if (!$addon->checkInfo()) {
  405. throw new Exception('配置文件不完整');
  406. }
  407. return true;
  408. }
  409. }