Service.php 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150
  1. <?php
  2. namespace think\addons;
  3. use fast\Http;
  4. use GuzzleHttp\Client;
  5. use GuzzleHttp\Exception\TransferException;
  6. use GuzzleHttp\TransferStats;
  7. use PhpZip\Exception\ZipException;
  8. use PhpZip\ZipFile;
  9. use RecursiveDirectoryIterator;
  10. use RecursiveIteratorIterator;
  11. use Symfony\Component\VarExporter\VarExporter;
  12. use think\Cache;
  13. use think\Db;
  14. use think\Exception;
  15. use think\Log;
  16. /**
  17. * 插件服务
  18. * @package think\addons
  19. */
  20. class Service
  21. {
  22. /**
  23. * 插件列表
  24. */
  25. public static function addons($params = [])
  26. {
  27. $params['domain'] = request()->host(true);
  28. return self::sendRequest('/addon/index', $params, 'GET');
  29. }
  30. /**
  31. * 检测插件是否购买授权
  32. */
  33. public static function isBuy($name, $extend = [])
  34. {
  35. $params = array_merge(['name' => $name, 'domain' => request()->host(true)], $extend);
  36. return self::sendRequest('/addon/isbuy', $params, 'POST');
  37. }
  38. /**
  39. * 检测插件是否授权
  40. *
  41. * @param string $name 插件名称
  42. * @param string $domain 验证域名
  43. */
  44. public static function isAuthorization($name, $domain = '')
  45. {
  46. $config = self::config($name);
  47. $request = request();
  48. $domain = self::getRootDomain($domain ? $domain : $request->host(true));
  49. if (isset($config['domains']) && isset($config['domains']) && isset($config['validations']) && isset($config['licensecodes'])) {
  50. $index = array_search($domain, $config['domains']);
  51. if ((in_array($domain, $config['domains']) && in_array(md5(md5($domain) . ($config['licensecodes'][$index] ?? '')), $config['validations'])) || $request->isCli()) {
  52. return true;
  53. }
  54. }
  55. return false;
  56. }
  57. /**
  58. * 远程下载插件
  59. *
  60. * @param string $name 插件名称
  61. * @param array $extend 扩展参数
  62. * @return string
  63. */
  64. public static function download($name, $extend = [])
  65. {
  66. $addonsTempDir = self::getAddonsBackupDir();
  67. $tmpFile = $addonsTempDir . $name . ".zip";
  68. try {
  69. $client = self::getClient();
  70. $response = $client->get('/addon/download', ['query' => array_merge(['name' => $name], $extend)]);
  71. $body = $response->getBody();
  72. $content = $body->getContents();
  73. if (substr($content, 0, 1) === '{') {
  74. $json = (array)json_decode($content, true);
  75. //如果传回的是一个下载链接,则再次下载
  76. if ($json['data'] && isset($json['data']['url'])) {
  77. $response = $client->get($json['data']['url']);
  78. $body = $response->getBody();
  79. $content = $body->getContents();
  80. } else {
  81. //下载返回错误,抛出异常
  82. throw new AddonException($json['msg'], $json['code'], $json['data']);
  83. }
  84. }
  85. } catch (TransferException $e) {
  86. throw new Exception("Addon package download failed");
  87. }
  88. if ($write = fopen($tmpFile, 'w')) {
  89. fwrite($write, $content);
  90. fclose($write);
  91. return $tmpFile;
  92. }
  93. throw new Exception("No permission to write temporary files");
  94. }
  95. /**
  96. * 解压插件
  97. *
  98. * @param string $name 插件名称
  99. * @return string
  100. * @throws Exception
  101. */
  102. public static function unzip($name)
  103. {
  104. if (!$name) {
  105. throw new Exception('Invalid parameters');
  106. }
  107. $addonsBackupDir = self::getAddonsBackupDir();
  108. $file = $addonsBackupDir . $name . '.zip';
  109. // 打开插件压缩包
  110. $zip = new ZipFile();
  111. try {
  112. $zip->openFile($file);
  113. } catch (ZipException $e) {
  114. $zip->close();
  115. throw new Exception('Unable to open the zip file');
  116. }
  117. $dir = self::getAddonDir($name);
  118. if (!is_dir($dir)) {
  119. @mkdir($dir, 0755);
  120. }
  121. // 解压插件压缩包
  122. try {
  123. $zip->extractTo($dir);
  124. } catch (ZipException $e) {
  125. throw new Exception('Unable to extract the file');
  126. } finally {
  127. $zip->close();
  128. }
  129. return $dir;
  130. }
  131. /**
  132. * 离线安装
  133. * @param string $file 插件压缩包
  134. * @param array $extend
  135. */
  136. public static function local($file, $extend = [])
  137. {
  138. $addonsTempDir = self::getAddonsBackupDir();
  139. if (!$file || !$file instanceof \think\File) {
  140. throw new Exception('No file upload or server upload limit exceeded');
  141. }
  142. $uploadFile = $file->rule('uniqid')->validate(['size' => 102400000, 'ext' => 'zip,fastaddon'])->move($addonsTempDir);
  143. if (!$uploadFile) {
  144. // 上传失败获取错误信息
  145. throw new Exception(__($file->getError()));
  146. }
  147. $tmpFile = $addonsTempDir . $uploadFile->getSaveName();
  148. $info = [];
  149. $zip = new ZipFile();
  150. try {
  151. // 打开插件压缩包
  152. try {
  153. $zip->openFile($tmpFile);
  154. } catch (ZipException $e) {
  155. @unlink($tmpFile);
  156. throw new Exception('Unable to open the zip file');
  157. }
  158. $config = self::getInfoIni($zip);
  159. // 判断插件标识
  160. $name = isset($config['name']) ? $config['name'] : '';
  161. if (!$name) {
  162. throw new Exception('Addon info file data incorrect');
  163. }
  164. // 判断插件是否存在
  165. if (!preg_match("/^[a-zA-Z0-9]+$/", $name)) {
  166. throw new Exception('Addon name incorrect');
  167. }
  168. // 判断新插件是否存在
  169. $newAddonDir = self::getAddonDir($name);
  170. if (is_dir($newAddonDir)) {
  171. throw new Exception('Addon already exists');
  172. }
  173. // 追加MD5和Data数据
  174. $extend['md5'] = md5_file($tmpFile);
  175. $extend['data'] = $zip->getArchiveComment();
  176. $extend['unknownsources'] = config('app_debug') && config('fastadmin.unknownsources');
  177. $extend['faversion'] = config('fastadmin.version');
  178. $params = array_merge($config, $extend);
  179. // 压缩包验证、版本依赖判断
  180. Service::valid($params);
  181. //创建插件目录
  182. @mkdir($newAddonDir, 0755, true);
  183. // 解压到插件目录
  184. try {
  185. $zip->extractTo($newAddonDir);
  186. } catch (ZipException $e) {
  187. @unlink($newAddonDir);
  188. throw new Exception('Unable to extract the file');
  189. }
  190. Db::startTrans();
  191. try {
  192. //默认禁用该插件
  193. $info = get_addon_info($name);
  194. if ($info['state']) {
  195. $info['state'] = 0;
  196. set_addon_info($name, $info);
  197. }
  198. //执行插件的安装方法
  199. $class = get_addon_class($name);
  200. if (class_exists($class)) {
  201. $addon = new $class();
  202. $addon->install();
  203. }
  204. Db::commit();
  205. } catch (\Exception $e) {
  206. Db::rollback();
  207. @rmdirs($newAddonDir);
  208. throw new Exception(__($e->getMessage()));
  209. }
  210. //导入SQL
  211. Service::importsql($name);
  212. } catch (AddonException $e) {
  213. throw new AddonException($e->getMessage(), $e->getCode(), $e->getData());
  214. } catch (Exception $e) {
  215. throw new Exception(__($e->getMessage()));
  216. } finally {
  217. $zip->close();
  218. unset($uploadFile);
  219. @unlink($tmpFile);
  220. }
  221. $info['config'] = get_addon_config($name) ? 1 : 0;
  222. $info['bootstrap'] = is_file(Service::getBootstrapFile($name));
  223. $info['testdata'] = is_file(Service::getTestdataFile($name));
  224. return $info;
  225. }
  226. /**
  227. * 验证压缩包、依赖验证
  228. * @param array $params
  229. * @return bool
  230. * @throws Exception
  231. */
  232. public static function valid($params = [])
  233. {
  234. $json = self::sendRequest('/addon/valid', $params, 'POST');
  235. if ($json && isset($json['code'])) {
  236. if ($json['code']) {
  237. return true;
  238. } else {
  239. throw new Exception($json['msg'] ?? "Invalid addon package");
  240. }
  241. } else {
  242. throw new Exception("Unknown data format");
  243. }
  244. }
  245. /**
  246. * 备份插件
  247. * @param string $name 插件名称
  248. * @return bool
  249. * @throws Exception
  250. */
  251. public static function backup($name)
  252. {
  253. $addonsBackupDir = self::getAddonsBackupDir();
  254. $file = $addonsBackupDir . $name . '-backup-' . date("YmdHis") . '.zip';
  255. $zipFile = new ZipFile();
  256. try {
  257. $zipFile
  258. ->addDirRecursive(self::getAddonDir($name))
  259. ->saveAsFile($file)
  260. ->close();
  261. } catch (ZipException $e) {
  262. } finally {
  263. $zipFile->close();
  264. }
  265. return true;
  266. }
  267. /**
  268. * 检测插件是否完整
  269. *
  270. * @param string $name 插件名称
  271. * @return boolean
  272. * @throws Exception
  273. */
  274. public static function check($name)
  275. {
  276. if (!$name || !is_dir(ADDON_PATH . $name)) {
  277. throw new Exception('Addon not exists');
  278. }
  279. $addonClass = get_addon_class($name);
  280. if (!$addonClass) {
  281. throw new Exception("The addon file does not exist");
  282. }
  283. $addon = new $addonClass();
  284. if (!$addon->checkInfo()) {
  285. throw new Exception("The configuration file content is incorrect");
  286. }
  287. return true;
  288. }
  289. /**
  290. * 是否有冲突
  291. *
  292. * @param string $name 插件名称
  293. * @return boolean
  294. * @throws AddonException
  295. */
  296. public static function noconflict($name)
  297. {
  298. // 检测冲突文件
  299. $list = self::getGlobalFiles($name, true);
  300. if ($list) {
  301. //发现冲突文件,抛出异常
  302. throw new AddonException(__("Conflicting file found"), -3, ['conflictlist' => $list]);
  303. }
  304. return true;
  305. }
  306. /**
  307. * 导入SQL
  308. *
  309. * @param string $name 插件名称
  310. * @param string $fileName SQL文件名称
  311. * @return boolean
  312. */
  313. public static function importsql($name, $fileName = null)
  314. {
  315. $fileName = is_null($fileName) ? 'install.sql' : $fileName;
  316. $sqlFile = self::getAddonDir($name) . $fileName;
  317. if (is_file($sqlFile)) {
  318. $lines = file($sqlFile);
  319. $templine = '';
  320. foreach ($lines as $line) {
  321. if (substr($line, 0, 2) == '--' || $line == '' || substr($line, 0, 2) == '/*') {
  322. continue;
  323. }
  324. $templine .= $line;
  325. if (substr(trim($line), -1, 1) == ';') {
  326. $templine = str_ireplace('__PREFIX__', config('database.prefix'), $templine);
  327. $templine = str_ireplace('INSERT INTO ', 'INSERT IGNORE INTO ', $templine);
  328. try {
  329. Db::getPdo()->exec($templine);
  330. } catch (\PDOException $e) {
  331. //$e->getMessage();
  332. }
  333. $templine = '';
  334. }
  335. }
  336. }
  337. return true;
  338. }
  339. /**
  340. * 刷新插件缓存文件
  341. *
  342. * @return boolean
  343. * @throws Exception
  344. */
  345. public static function refresh()
  346. {
  347. //刷新addons.js
  348. $addons = get_addon_list();
  349. $bootstrapArr = [];
  350. foreach ($addons as $name => $addon) {
  351. $bootstrapFile = self::getBootstrapFile($name);
  352. if ($addon['state'] && is_file($bootstrapFile)) {
  353. $bootstrapArr[] = file_get_contents($bootstrapFile);
  354. }
  355. }
  356. $addonsFile = ROOT_PATH . str_replace("/", DS, "public/assets/js/addons.js");
  357. if ($handle = fopen($addonsFile, 'w')) {
  358. $tpl = <<<EOD
  359. define([], function () {
  360. {__JS__}
  361. });
  362. EOD;
  363. fwrite($handle, str_replace("{__JS__}", implode("\n", $bootstrapArr), $tpl));
  364. fclose($handle);
  365. } else {
  366. throw new Exception(__("Unable to open file '%s' for writing", "addons.js"));
  367. }
  368. Cache::rm("addons");
  369. Cache::rm("hooks");
  370. $file = self::getExtraAddonsFile();
  371. $config = get_addon_autoload_config(true);
  372. if ($config['autoload']) {
  373. return;
  374. }
  375. if (!is_really_writable($file)) {
  376. throw new Exception(__("Unable to open file '%s' for writing", "addons.php"));
  377. }
  378. file_put_contents($file, "<?php\n\n" . "return " . VarExporter::export($config) . ";\n", LOCK_EX);
  379. return true;
  380. }
  381. /**
  382. * 安装插件
  383. *
  384. * @param string $name 插件名称
  385. * @param boolean $force 是否覆盖
  386. * @param array $extend 扩展参数
  387. * @return boolean
  388. * @throws Exception
  389. * @throws AddonException
  390. */
  391. public static function install($name, $force = false, $extend = [])
  392. {
  393. if (!$name || (is_dir(ADDON_PATH . $name) && !$force)) {
  394. throw new Exception('Addon already exists');
  395. }
  396. $extend['domain'] = request()->host(true);
  397. // 远程下载插件
  398. $tmpFile = Service::download($name, $extend);
  399. $addonDir = self::getAddonDir($name);
  400. try {
  401. // 解压插件压缩包到插件目录
  402. Service::unzip($name);
  403. // 检查插件是否完整
  404. Service::check($name);
  405. if (!$force) {
  406. Service::noconflict($name);
  407. }
  408. } catch (AddonException $e) {
  409. @rmdirs($addonDir);
  410. throw new AddonException($e->getMessage(), $e->getCode(), $e->getData());
  411. } catch (Exception $e) {
  412. @rmdirs($addonDir);
  413. throw new Exception($e->getMessage());
  414. } finally {
  415. // 移除临时文件
  416. @unlink($tmpFile);
  417. }
  418. // 默认启用该插件
  419. $info = get_addon_info($name);
  420. Db::startTrans();
  421. try {
  422. if (!$info['state']) {
  423. $info['state'] = 1;
  424. set_addon_info($name, $info);
  425. }
  426. // 执行安装脚本
  427. $class = get_addon_class($name);
  428. if (class_exists($class)) {
  429. $addon = new $class();
  430. $addon->install();
  431. }
  432. Db::commit();
  433. } catch (Exception $e) {
  434. @rmdirs($addonDir);
  435. Db::rollback();
  436. throw new Exception($e->getMessage());
  437. }
  438. // 导入
  439. Service::importsql($name);
  440. // 启用插件
  441. Service::enable($name, true);
  442. $info['config'] = get_addon_config($name) ? 1 : 0;
  443. $info['bootstrap'] = is_file(Service::getBootstrapFile($name));
  444. $info['testdata'] = is_file(Service::getTestdataFile($name));
  445. return $info;
  446. }
  447. /**
  448. * 卸载插件
  449. *
  450. * @param string $name
  451. * @param boolean $force 是否强制卸载
  452. * @return boolean
  453. * @throws Exception
  454. */
  455. public static function uninstall($name, $force = false)
  456. {
  457. if (!$name || !is_dir(ADDON_PATH . $name)) {
  458. throw new Exception('Addon not exists');
  459. }
  460. if (!$force) {
  461. Service::noconflict($name);
  462. }
  463. // 移除插件全局资源文件
  464. if ($force) {
  465. $list = Service::getGlobalFiles($name);
  466. foreach ($list as $k => $v) {
  467. @unlink(ROOT_PATH . $v);
  468. }
  469. }
  470. // 执行卸载脚本
  471. try {
  472. $class = get_addon_class($name);
  473. if (class_exists($class)) {
  474. $addon = new $class();
  475. $addon->uninstall();
  476. }
  477. } catch (Exception $e) {
  478. throw new Exception($e->getMessage());
  479. }
  480. // 移除插件目录
  481. rmdirs(ADDON_PATH . $name);
  482. // 刷新
  483. Service::refresh();
  484. return true;
  485. }
  486. /**
  487. * 启用
  488. * @param string $name 插件名称
  489. * @param boolean $force 是否强制覆盖
  490. * @return boolean
  491. */
  492. public static function enable($name, $force = false)
  493. {
  494. if (!$name || !is_dir(ADDON_PATH . $name)) {
  495. throw new Exception('Addon not exists');
  496. }
  497. if (!$force) {
  498. Service::noconflict($name);
  499. }
  500. //备份冲突文件
  501. if (config('fastadmin.backup_global_files')) {
  502. $conflictFiles = self::getGlobalFiles($name, true);
  503. if ($conflictFiles) {
  504. $zip = new ZipFile();
  505. try {
  506. foreach ($conflictFiles as $k => $v) {
  507. $zip->addFile(ROOT_PATH . $v, $v);
  508. }
  509. $addonsBackupDir = self::getAddonsBackupDir();
  510. $zip->saveAsFile($addonsBackupDir . $name . "-conflict-enable-" . date("YmdHis") . ".zip");
  511. } catch (Exception $e) {
  512. } finally {
  513. $zip->close();
  514. }
  515. }
  516. }
  517. $addonDir = self::getAddonDir($name);
  518. $sourceAssetsDir = self::getSourceAssetsDir($name);
  519. $destAssetsDir = self::getDestAssetsDir($name);
  520. $files = self::getGlobalFiles($name);
  521. if ($files) {
  522. //刷新插件配置缓存
  523. Service::config($name, ['files' => $files]);
  524. }
  525. // 复制文件
  526. if (is_dir($sourceAssetsDir)) {
  527. copydirs($sourceAssetsDir, $destAssetsDir);
  528. }
  529. // 复制application和public到全局
  530. foreach (self::getCheckDirs() as $k => $dir) {
  531. if (is_dir($addonDir . $dir)) {
  532. copydirs($addonDir . $dir, ROOT_PATH . $dir);
  533. }
  534. }
  535. //插件纯净模式时将插件目录下的application、public和assets删除
  536. if (config('fastadmin.addon_pure_mode')) {
  537. // 删除插件目录已复制到全局的文件
  538. @rmdirs($sourceAssetsDir);
  539. foreach (self::getCheckDirs() as $k => $dir) {
  540. @rmdirs($addonDir . $dir);
  541. }
  542. }
  543. //执行启用脚本
  544. try {
  545. $class = get_addon_class($name);
  546. if (class_exists($class)) {
  547. $addon = new $class();
  548. if (method_exists($class, "enable")) {
  549. $addon->enable();
  550. }
  551. }
  552. } catch (Exception $e) {
  553. throw new Exception($e->getMessage());
  554. }
  555. $info = get_addon_info($name);
  556. $info['state'] = 1;
  557. unset($info['url']);
  558. set_addon_info($name, $info);
  559. // 刷新
  560. Service::refresh();
  561. return true;
  562. }
  563. /**
  564. * 禁用
  565. *
  566. * @param string $name 插件名称
  567. * @param boolean $force 是否强制禁用
  568. * @return boolean
  569. * @throws Exception
  570. */
  571. public static function disable($name, $force = false)
  572. {
  573. if (!$name || !is_dir(ADDON_PATH . $name)) {
  574. throw new Exception('Addon not exists');
  575. }
  576. $file = self::getExtraAddonsFile();
  577. if (!is_really_writable($file)) {
  578. throw new Exception(__("Unable to open file '%s' for writing", "addons.php"));
  579. }
  580. if (!$force) {
  581. Service::noconflict($name);
  582. }
  583. if (config('fastadmin.backup_global_files')) {
  584. //仅备份修改过的文件
  585. $conflictFiles = Service::getGlobalFiles($name, true);
  586. if ($conflictFiles) {
  587. $zip = new ZipFile();
  588. try {
  589. foreach ($conflictFiles as $k => $v) {
  590. $zip->addFile(ROOT_PATH . $v, $v);
  591. }
  592. $addonsBackupDir = self::getAddonsBackupDir();
  593. $zip->saveAsFile($addonsBackupDir . $name . "-conflict-disable-" . date("YmdHis") . ".zip");
  594. } catch (Exception $e) {
  595. } finally {
  596. $zip->close();
  597. }
  598. }
  599. }
  600. $config = Service::config($name);
  601. $addonDir = self::getAddonDir($name);
  602. //插件资源目录
  603. $destAssetsDir = self::getDestAssetsDir($name);
  604. // 移除插件全局文件
  605. $list = Service::getGlobalFiles($name);
  606. //插件纯净模式时将原有的文件复制回插件目录
  607. //当无法获取全局文件列表时也将列表复制回插件目录
  608. if (config('fastadmin.addon_pure_mode') || !$list) {
  609. if ($config && isset($config['files']) && is_array($config['files'])) {
  610. foreach ($config['files'] as $index => $item) {
  611. //避免切换不同服务器后导致路径不一致
  612. $item = str_replace(['/', '\\'], DS, $item);
  613. //插件资源目录,无需重复复制
  614. if (stripos($item, str_replace(ROOT_PATH, '', $destAssetsDir)) === 0) {
  615. continue;
  616. }
  617. //检查目录是否存在,不存在则创建
  618. $itemBaseDir = dirname($addonDir . $item);
  619. if (!is_dir($itemBaseDir)) {
  620. @mkdir($itemBaseDir, 0755, true);
  621. }
  622. if (is_file(ROOT_PATH . $item)) {
  623. @copy(ROOT_PATH . $item, $addonDir . $item);
  624. }
  625. }
  626. $list = $config['files'];
  627. }
  628. //复制插件目录资源
  629. if (is_dir($destAssetsDir)) {
  630. @copydirs($destAssetsDir, $addonDir . 'assets' . DS);
  631. }
  632. }
  633. $dirs = [];
  634. foreach ($list as $k => $v) {
  635. $file = ROOT_PATH . $v;
  636. $dirs[] = dirname($file);
  637. @unlink($file);
  638. }
  639. // 移除插件空目录
  640. $dirs = array_filter(array_unique($dirs));
  641. foreach ($dirs as $k => $v) {
  642. remove_empty_folder($v);
  643. }
  644. $info = get_addon_info($name);
  645. $info['state'] = 0;
  646. unset($info['url']);
  647. set_addon_info($name, $info);
  648. // 执行禁用脚本
  649. try {
  650. $class = get_addon_class($name);
  651. if (class_exists($class)) {
  652. $addon = new $class();
  653. if (method_exists($class, "disable")) {
  654. $addon->disable();
  655. }
  656. }
  657. } catch (Exception $e) {
  658. throw new Exception($e->getMessage());
  659. }
  660. // 刷新
  661. Service::refresh();
  662. return true;
  663. }
  664. /**
  665. * 升级插件
  666. *
  667. * @param string $name 插件名称
  668. * @param array $extend 扩展参数
  669. */
  670. public static function upgrade($name, $extend = [])
  671. {
  672. $info = get_addon_info($name);
  673. if ($info['state']) {
  674. throw new Exception(__('Please disable addon first'));
  675. }
  676. $config = get_addon_config($name);
  677. if ($config) {
  678. //备份配置
  679. }
  680. // 远程下载插件
  681. $tmpFile = Service::download($name, $extend);
  682. // 备份插件文件
  683. Service::backup($name);
  684. $addonDir = self::getAddonDir($name);
  685. // 删除插件目录下的application和public
  686. $files = self::getCheckDirs();
  687. foreach ($files as $index => $file) {
  688. @rmdirs($addonDir . $file);
  689. }
  690. try {
  691. // 解压插件
  692. Service::unzip($name);
  693. } catch (Exception $e) {
  694. throw new Exception($e->getMessage());
  695. } finally {
  696. // 移除临时文件
  697. @unlink($tmpFile);
  698. }
  699. if ($config) {
  700. // 还原配置
  701. set_addon_config($name, $config);
  702. }
  703. // 导入
  704. Service::importsql($name);
  705. // 执行升级脚本
  706. try {
  707. $addonName = ucfirst($name);
  708. //创建临时类用于调用升级的方法
  709. $sourceFile = $addonDir . $addonName . ".php";
  710. $destFile = $addonDir . $addonName . "Upgrade.php";
  711. $classContent = str_replace("class {$addonName} extends", "class {$addonName}Upgrade extends", file_get_contents($sourceFile));
  712. //创建临时的类文件
  713. file_put_contents($destFile, $classContent);
  714. $className = "\\addons\\" . $name . "\\" . $addonName . "Upgrade";
  715. $addon = new $className($name);
  716. //调用升级的方法
  717. if (method_exists($addon, "upgrade")) {
  718. $addon->upgrade();
  719. }
  720. //移除临时文件
  721. @unlink($destFile);
  722. } catch (Exception $e) {
  723. throw new Exception($e->getMessage());
  724. }
  725. // 刷新
  726. Service::refresh();
  727. //必须变更版本号
  728. $info['version'] = isset($extend['version']) ? $extend['version'] : $info['version'];
  729. $info['config'] = get_addon_config($name) ? 1 : 0;
  730. $info['bootstrap'] = is_file(Service::getBootstrapFile($name));
  731. return $info;
  732. }
  733. /**
  734. * 读取或修改插件配置
  735. * @param string $name
  736. * @param array $changed
  737. * @return array
  738. */
  739. public static function config($name, $changed = [])
  740. {
  741. $addonDir = self::getAddonDir($name);
  742. $addonConfigFile = $addonDir . '.addonrc';
  743. $config = [];
  744. if (is_file($addonConfigFile)) {
  745. $config = (array)json_decode(file_get_contents($addonConfigFile), true);
  746. }
  747. $config = array_merge($config, $changed);
  748. if ($changed) {
  749. file_put_contents($addonConfigFile, json_encode($config, JSON_UNESCAPED_UNICODE));
  750. }
  751. return $config;
  752. }
  753. /**
  754. * 获取插件在全局的文件
  755. *
  756. * @param string $name 插件名称
  757. * @param boolean $onlyconflict 是否只返回冲突文件
  758. * @return array
  759. */
  760. public static function getGlobalFiles($name, $onlyconflict = false)
  761. {
  762. $list = [];
  763. $addonDir = self::getAddonDir($name);
  764. $checkDirList = self::getCheckDirs();
  765. $checkDirList = array_merge($checkDirList, ['assets']);
  766. $assetDir = self::getDestAssetsDir($name);
  767. // 扫描插件目录是否有覆盖的文件
  768. foreach ($checkDirList as $k => $dirName) {
  769. //检测目录是否存在
  770. if (!is_dir($addonDir . $dirName)) {
  771. continue;
  772. }
  773. //匹配出所有的文件
  774. $files = new RecursiveIteratorIterator(
  775. new RecursiveDirectoryIterator($addonDir . $dirName, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST
  776. );
  777. foreach ($files as $fileinfo) {
  778. if ($fileinfo->isFile()) {
  779. $filePath = $fileinfo->getPathName();
  780. //如果名称为assets需要做特殊处理
  781. if ($dirName === 'assets') {
  782. $path = str_replace(ROOT_PATH, '', $assetDir) . str_replace($addonDir . $dirName . DS, '', $filePath);
  783. } else {
  784. $path = str_replace($addonDir, '', $filePath);
  785. }
  786. if ($onlyconflict) {
  787. $destPath = ROOT_PATH . $path;
  788. if (is_file($destPath)) {
  789. if (filesize($filePath) != filesize($destPath) || md5_file($filePath) != md5_file($destPath)) {
  790. $list[] = $path;
  791. }
  792. }
  793. } else {
  794. $list[] = $path;
  795. }
  796. }
  797. }
  798. }
  799. $list = array_filter(array_unique($list));
  800. return $list;
  801. }
  802. /**
  803. * 更新本地应用插件授权
  804. */
  805. public static function authorization($params = [])
  806. {
  807. $addonList = get_addon_list();
  808. $result = [];
  809. $domain = request()->host(true);
  810. $addons = [];
  811. foreach ($addonList as $name => $item) {
  812. $config = self::config($name);
  813. $addons[] = ['name' => $name, 'domains' => $config['domains'] ?? [], 'licensecodes' => $config['licensecodes'] ?? [], 'validations' => $config['validations'] ?? []];
  814. }
  815. $params = array_merge($params, [
  816. 'faversion' => config('fastadmin.version'),
  817. 'domain' => $domain,
  818. 'addons' => $addons
  819. ]);
  820. $result = self::sendRequest('/addon/authorization', $params, 'POST');
  821. if (isset($result['code']) && $result['code'] == 1) {
  822. $json = $result['data']['addons'] ?? [];
  823. foreach ($addonList as $name => $item) {
  824. self::config($name, ['domains' => $json[$name]['domains'] ?? [], 'licensecodes' => $json[$name]['licensecodes'] ?? [], 'validations' => $json[$name]['validations'] ?? []]);
  825. }
  826. return true;
  827. } else {
  828. throw new Exception($result['msg'] ?? __('Network error'));
  829. }
  830. }
  831. /**
  832. * 验证插件授权,应用插件需要授权使用,移除或绕过授权验证,保留追究法律责任的权利
  833. * @param $name
  834. * @return bool
  835. */
  836. public static function checkAddonAuthorization($name)
  837. {
  838. $request = request();
  839. $config = self::config($name);
  840. $domain = self::getRootDomain($request->host(true));
  841. //应用插件需要授权使用,移除或绕过授权验证,保留追究法律责任的权利
  842. if (isset($config['domains']) && isset($config['domains']) && isset($config['validations']) && isset($config['licensecodes'])) {
  843. $index = array_search($domain, $config['domains']);
  844. if ((in_array($domain, $config['domains']) && in_array(md5(md5($domain) . ($config['licensecodes'][$index] ?? '')), $config['validations'])) || $request->isCli()) {
  845. $request->bind('authorized', $domain ?: 'cli');
  846. return true;
  847. } elseif ($config['domains']) {
  848. foreach ($config['domains'] as $index => $item) {
  849. if (substr_compare($domain, "." . $item, -strlen("." . $item)) === 0 && in_array(md5(md5($item) . ($config['licensecodes'][$index] ?? '')), $config['validations'])) {
  850. $request->bind('authorized', $domain);
  851. return true;
  852. }
  853. }
  854. }
  855. }
  856. return false;
  857. }
  858. /**
  859. * 获取顶级域名
  860. * @param $domain
  861. * @return string
  862. */
  863. public static function getRootDomain($domain)
  864. {
  865. $host = strtolower(trim($domain));
  866. $hostArr = explode('.', $host);
  867. $hostCount = count($hostArr);
  868. $cnRegex = '/\w+\.(gov|org|ac|mil|net|edu|com|bj|tj|sh|cq|he|sx|nm|ln|jl|hl|js|zj|ah|fj|jx|sd|ha|hb|hn|gd|gx|hi|sc|gz|yn|xz|sn|gs|qh|nx|xj|tw|hk|mo)\.cn$/i';
  869. $countryRegex = '/\w+\.(\w{2}|com|net)\.\w{2}$/i';
  870. if ($hostCount > 2 && (preg_match($cnRegex, $host) || preg_match($countryRegex, $host))) {
  871. $host = implode('.', array_slice($hostArr, -3, 3, true));
  872. } else {
  873. $host = implode('.', array_slice($hostArr, -2, 2, true));
  874. }
  875. return $host;
  876. }
  877. /**
  878. * 获取插件行为、路由配置文件
  879. * @return string
  880. */
  881. public static function getExtraAddonsFile()
  882. {
  883. return CONF_PATH . 'extra' . DS . 'addons.php';
  884. }
  885. /**
  886. * 获取bootstrap.js路径
  887. * @return string
  888. */
  889. public static function getBootstrapFile($name)
  890. {
  891. return ADDON_PATH . $name . DS . 'bootstrap.js';
  892. }
  893. /**
  894. * 获取testdata.sql路径
  895. * @return string
  896. */
  897. public static function getTestdataFile($name)
  898. {
  899. return ADDON_PATH . $name . DS . 'testdata.sql';
  900. }
  901. /**
  902. * 获取指定插件的目录
  903. */
  904. public static function getAddonDir($name)
  905. {
  906. $dir = ADDON_PATH . $name . DS;
  907. return $dir;
  908. }
  909. /**
  910. * 获取插件备份目录
  911. */
  912. public static function getAddonsBackupDir()
  913. {
  914. $dir = RUNTIME_PATH . 'addons' . DS;
  915. if (!is_dir($dir)) {
  916. @mkdir($dir, 0755, true);
  917. }
  918. return $dir;
  919. }
  920. /**
  921. * 获取插件源资源文件夹
  922. * @param string $name 插件名称
  923. * @return string
  924. */
  925. protected static function getSourceAssetsDir($name)
  926. {
  927. return ADDON_PATH . $name . DS . 'assets' . DS;
  928. }
  929. /**
  930. * 获取插件目标资源文件夹
  931. * @param string $name 插件名称
  932. * @return string
  933. */
  934. protected static function getDestAssetsDir($name)
  935. {
  936. $assetsDir = ROOT_PATH . str_replace("/", DS, "public/assets/addons/{$name}/");
  937. return $assetsDir;
  938. }
  939. /**
  940. * 获取远程服务器
  941. * @return string
  942. */
  943. protected static function getServerUrl()
  944. {
  945. return config('fastadmin.api_url');
  946. }
  947. /**
  948. * 获取检测的全局文件夹目录
  949. * @return array
  950. */
  951. protected static function getCheckDirs()
  952. {
  953. return [
  954. 'application',
  955. 'public'
  956. ];
  957. }
  958. /**
  959. * 获取请求对象
  960. * @return Client
  961. */
  962. public static function getClient()
  963. {
  964. $options = [
  965. 'base_uri' => self::getServerUrl(),
  966. 'timeout' => 30,
  967. 'connect_timeout' => 30,
  968. 'verify' => false,
  969. 'http_errors' => false,
  970. 'headers' => [
  971. 'X-REQUESTED-WITH' => 'XMLHttpRequest',
  972. 'Referer' => dirname(request()->root(true)),
  973. 'User-Agent' => 'FastAddon',
  974. ]
  975. ];
  976. static $client;
  977. if (empty($client)) {
  978. $client = new Client($options);
  979. }
  980. return $client;
  981. }
  982. /**
  983. * 发送请求
  984. * @return array
  985. * @throws Exception
  986. * @throws \GuzzleHttp\Exception\GuzzleException
  987. */
  988. public static function sendRequest($url, $params = [], $method = 'POST')
  989. {
  990. $json = [];
  991. try {
  992. $client = self::getClient();
  993. $options = strtoupper($method) == 'POST' ? ['form_params' => $params] : ['query' => $params];
  994. $response = $client->request($method, $url, $options);
  995. $body = $response->getBody();
  996. $content = $body->getContents();
  997. $json = (array)json_decode($content, true);
  998. } catch (TransferException $e) {
  999. throw new Exception(__('Network error'));
  1000. } catch (\Exception $e) {
  1001. throw new Exception(__('Unknown data format'));
  1002. }
  1003. return $json;
  1004. }
  1005. /**
  1006. * 匹配配置文件中info信息
  1007. * @param ZipFile $zip
  1008. * @return array|false
  1009. * @throws Exception
  1010. */
  1011. protected static function getInfoIni($zip)
  1012. {
  1013. $config = [];
  1014. // 读取插件信息
  1015. try {
  1016. $info = $zip->getEntryContents('info.ini');
  1017. $config = parse_ini_string($info);
  1018. } catch (ZipException $e) {
  1019. throw new Exception('Unable to extract the file');
  1020. }
  1021. return $config;
  1022. }
  1023. }