Service.php 35 KB

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