Server.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. <?php
  2. namespace app\admin\library\module;
  3. use Throwable;
  4. use ba\Depends;
  5. use ba\Exception;
  6. use ba\Filesystem;
  7. use think\facade\Db;
  8. use GuzzleHttp\Client;
  9. use FilesystemIterator;
  10. use think\facade\Config;
  11. use RecursiveIteratorIterator;
  12. use RecursiveDirectoryIterator;
  13. use think\db\exception\PDOException;
  14. use app\admin\library\crud\Helper;
  15. use GuzzleHttp\Exception\TransferException;
  16. /**
  17. * 模块服务类
  18. */
  19. class Server
  20. {
  21. private static ?Client $client = null;
  22. private static string $apiBaseUrl = '/api/v6.store/';
  23. /**
  24. * 下载
  25. * @throws Throwable
  26. */
  27. public static function download(string $uid, string $dir, array $extend = []): string
  28. {
  29. $tmpFile = $dir . $uid . ".zip";
  30. try {
  31. $client = self::getClient();
  32. $response = $client->get(self::$apiBaseUrl . 'download', ['query' => array_merge(['uid' => $uid, 'server' => 1], $extend)]);
  33. $body = $response->getBody();
  34. $content = $body->getContents();
  35. if ($content == '' || stripos($content, '<title>系统发生错误</title>') !== false) {
  36. throw new Exception('package download failed', 0);
  37. }
  38. if (str_starts_with($content, '{')) {
  39. $json = (array)json_decode($content, true);
  40. throw new Exception($json['msg'], $json['code'], $json['data'] ?? []);
  41. }
  42. } catch (TransferException $e) {
  43. throw new Exception('package download failed', 0, ['msg' => $e->getMessage()]);
  44. }
  45. if ($write = fopen($tmpFile, 'w')) {
  46. fwrite($write, $content);
  47. fclose($write);
  48. return $tmpFile;
  49. }
  50. throw new Exception("No permission to write temporary files");
  51. }
  52. /**
  53. * 安装预检
  54. * @throws Throwable
  55. */
  56. public static function installPreCheck(array $query = []): bool
  57. {
  58. try {
  59. $client = self::getClient();
  60. $response = $client->get(self::$apiBaseUrl . 'preCheck', ['query' => $query]);
  61. $body = $response->getBody();
  62. $statusCode = $response->getStatusCode();
  63. $content = $body->getContents();
  64. if ($content == '' || stripos($content, '<title>系统发生错误</title>') !== false || $statusCode != 200) {
  65. return true;
  66. }
  67. if (str_starts_with($content, '{')) {
  68. $json = json_decode($content, true);
  69. if ($json && $json['code'] == 0) {
  70. throw new Exception($json['msg'], $json['code'], $json['data'] ?? []);
  71. }
  72. }
  73. } catch (TransferException $e) {
  74. throw new Exception('package check failed', 0, ['msg' => $e->getMessage()]);
  75. }
  76. return true;
  77. }
  78. public static function getConfig(string $dir, $key = ''): array
  79. {
  80. $configFile = $dir . 'config.json';
  81. if (!is_dir($dir) || !is_file($configFile)) {
  82. return [];
  83. }
  84. $configContent = @file_get_contents($configFile);
  85. $configContent = json_decode($configContent, true);
  86. if (!$configContent) {
  87. return [];
  88. }
  89. if ($key) {
  90. return $configContent[$key] ?? [];
  91. }
  92. return $configContent;
  93. }
  94. public static function getDepend(string $dir, string $key = ''): array
  95. {
  96. if ($key) {
  97. return self::getConfig($dir, $key);
  98. }
  99. $configContent = self::getConfig($dir);
  100. $dependKey = ['require', 'require-dev', 'dependencies', 'devDependencies', 'nuxtDependencies', 'nuxtDevDependencies'];
  101. $dependArray = [];
  102. foreach ($dependKey as $item) {
  103. if (array_key_exists($item, $configContent) && $configContent[$item]) {
  104. $dependArray[$item] = $configContent[$item];
  105. }
  106. }
  107. return $dependArray;
  108. }
  109. /**
  110. * 依赖冲突检查
  111. * @throws Throwable
  112. */
  113. public static function dependConflictCheck(string $dir): array
  114. {
  115. $depend = self::getDepend($dir);
  116. $serverDep = new Depends(root_path() . 'composer.json', 'composer');
  117. $webDep = new Depends(root_path() . 'web' . DIRECTORY_SEPARATOR . 'package.json');
  118. $webNuxtDep = new Depends(root_path() . 'web-nuxt' . DIRECTORY_SEPARATOR . 'package.json');
  119. $sysDepend = [
  120. 'require' => $serverDep->getDepends(),
  121. 'require-dev' => $serverDep->getDepends(true),
  122. 'dependencies' => $webDep->getDepends(),
  123. 'devDependencies' => $webDep->getDepends(true),
  124. 'nuxtDependencies' => $webNuxtDep->getDepends(),
  125. 'nuxtDevDependencies' => $webNuxtDep->getDepends(true),
  126. ];
  127. $conflict = [];
  128. foreach ($depend as $key => $item) {
  129. $conflict[$key] = array_uintersect_assoc($item, $sysDepend[$key], function ($a, $b) {
  130. return $a == $b ? -1 : 0;
  131. });
  132. }
  133. return $conflict;
  134. }
  135. /**
  136. * 获取模块[冲突]文件列表
  137. * @param string $dir 模块目录
  138. * @param bool $onlyConflict 是否只获取冲突文件
  139. */
  140. public static function getFileList(string $dir, bool $onlyConflict = false): array
  141. {
  142. if (!is_dir($dir)) {
  143. return [];
  144. }
  145. $fileList = [];
  146. $overwriteDir = self::getOverwriteDir();
  147. $moduleFileList = self::getRuntime($dir, 'files');
  148. if ($moduleFileList) {
  149. // 有冲突的文件
  150. if ($onlyConflict) {
  151. // 排除的文件
  152. $excludeFile = [
  153. 'info.ini'
  154. ];
  155. foreach ($moduleFileList as $file) {
  156. // 如果是要安装到项目的文件,从项目根目录开始,如果不是,从模块根目录开始
  157. $path = Filesystem::fsFit(str_replace($dir, '', $file['path']));
  158. $paths = explode(DIRECTORY_SEPARATOR, $path);
  159. $overwriteFile = in_array($paths[0], $overwriteDir) ? root_path() . $path : $dir . $path;
  160. if (is_file($overwriteFile) && !in_array($path, $excludeFile) && (filesize($overwriteFile) != $file['size'] || md5_file($overwriteFile) != $file['md5'])) {
  161. $fileList[] = $path;
  162. }
  163. }
  164. } else {
  165. // 要安装的文件
  166. foreach ($overwriteDir as $item) {
  167. $baseDir = $dir . $item;
  168. foreach ($moduleFileList as $file) {
  169. if (!str_starts_with($file['path'], $baseDir)) continue;
  170. $fileList[] = Filesystem::fsFit(str_replace($dir, '', $file['path']));
  171. }
  172. }
  173. }
  174. return $fileList;
  175. }
  176. foreach ($overwriteDir as $item) {
  177. $baseDir = $dir . $item;
  178. if (!is_dir($baseDir)) {
  179. continue;
  180. }
  181. $files = new RecursiveIteratorIterator(
  182. new RecursiveDirectoryIterator($baseDir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST
  183. );
  184. foreach ($files as $file) {
  185. if ($file->isFile()) {
  186. $filePath = $file->getPathName();
  187. $path = str_replace($dir, '', $filePath);
  188. $path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path);
  189. if ($onlyConflict) {
  190. $overwriteFile = root_path() . $path;
  191. if (is_file($overwriteFile) && (filesize($overwriteFile) != filesize($filePath) || md5_file($overwriteFile) != md5_file($filePath))) {
  192. $fileList[] = $path;
  193. }
  194. } else {
  195. $fileList[] = $path;
  196. }
  197. }
  198. }
  199. }
  200. return $fileList;
  201. }
  202. public static function getOverwriteDir(): array
  203. {
  204. return [
  205. 'app',
  206. 'config',
  207. 'database',
  208. 'extend',
  209. 'public',
  210. 'vendor',
  211. 'web',
  212. 'web-nuxt',
  213. ];
  214. }
  215. public static function importSql(string $dir): bool
  216. {
  217. $sqlFile = $dir . 'install.sql';
  218. $tempLine = '';
  219. if (is_file($sqlFile)) {
  220. $lines = file($sqlFile);
  221. foreach ($lines as $line) {
  222. if (str_starts_with($line, '--') || $line == '' || str_starts_with($line, '/*')) {
  223. continue;
  224. }
  225. $tempLine .= $line;
  226. if (str_ends_with(trim($line), ';')) {
  227. $tempLine = str_ireplace('__PREFIX__', Config::get('database.connections.mysql.prefix'), $tempLine);
  228. $tempLine = str_ireplace('INSERT INTO ', 'INSERT IGNORE INTO ', $tempLine);
  229. try {
  230. Db::execute($tempLine);
  231. } catch (PDOException) {
  232. // $e->getMessage();
  233. }
  234. $tempLine = '';
  235. }
  236. }
  237. }
  238. return true;
  239. }
  240. public static function installedList(string $dir): array
  241. {
  242. if (!is_dir($dir)) {
  243. return [];
  244. }
  245. $installedDir = scandir($dir);
  246. $installedList = [];
  247. foreach ($installedDir as $item) {
  248. if ($item === '.' or $item === '..' || is_file($dir . $item)) {
  249. continue;
  250. }
  251. $tempDir = $dir . $item . DIRECTORY_SEPARATOR;
  252. if (!is_dir($tempDir)) {
  253. continue;
  254. }
  255. $info = self::getIni($tempDir);
  256. if (!isset($info['uid'])) {
  257. continue;
  258. }
  259. $installedList[] = $info;
  260. }
  261. return $installedList;
  262. }
  263. public static function getInstalledIds(string $dir): array
  264. {
  265. $installedIds = [];
  266. $installed = self::installedList($dir);
  267. foreach ($installed as $item) {
  268. $installedIds[] = $item['uid'];
  269. }
  270. return $installedIds;
  271. }
  272. /**
  273. * 获取模块ini
  274. * @param string $dir 模块目录路径
  275. */
  276. public static function getIni(string $dir): array
  277. {
  278. $infoFile = $dir . 'info.ini';
  279. $info = [];
  280. if (is_file($infoFile)) {
  281. $info = parse_ini_file($infoFile, true, INI_SCANNER_TYPED) ?: [];
  282. if (!$info) return [];
  283. }
  284. return $info;
  285. }
  286. /**
  287. * 设置模块ini
  288. * @param string $dir 模块目录路径
  289. * @param array $arr 新的ini数据
  290. * @return bool
  291. * @throws Throwable
  292. */
  293. public static function setIni(string $dir, array $arr): bool
  294. {
  295. $infoFile = $dir . 'info.ini';
  296. $ini = [];
  297. foreach ($arr as $key => $val) {
  298. if (is_array($val)) {
  299. $ini[] = "[$key]";
  300. foreach ($val as $ikey => $ival) {
  301. $ini[] = "$ikey = $ival";
  302. }
  303. } else {
  304. $ini[] = "$key = $val";
  305. }
  306. }
  307. if (!file_put_contents($infoFile, implode("\n", $ini) . "\n", LOCK_EX)) {
  308. throw new Exception("Configuration file has no write permission");
  309. }
  310. return true;
  311. }
  312. public static function getClass(string $uid, string $type = 'event', string $class = null): string
  313. {
  314. $name = parse_name($uid);
  315. if (!is_null($class) && strpos($class, '.')) {
  316. $class = explode('.', $class);
  317. $class[count($class) - 1] = parse_name(end($class), 1);
  318. $class = implode('\\', $class);
  319. } else {
  320. $class = parse_name(is_null($class) ? $name : $class, 1);
  321. }
  322. $namespace = match ($type) {
  323. 'controller' => '\\modules\\' . $name . '\\controller\\' . $class,
  324. default => '\\modules\\' . $name . '\\' . $class,
  325. };
  326. return class_exists($namespace) ? $namespace : '';
  327. }
  328. public static function execEvent(string $uid, string $event, array $params = []): void
  329. {
  330. $eventClass = self::getClass($uid);
  331. if (class_exists($eventClass)) {
  332. $handle = new $eventClass();
  333. if (method_exists($eventClass, $event)) {
  334. $handle->$event($params);
  335. }
  336. }
  337. }
  338. /**
  339. * 分析 WebBootstrap 代码
  340. */
  341. public static function analysisWebBootstrap(string $uid, string $dir): array
  342. {
  343. $bootstrapFile = $dir . 'webBootstrap.stub';
  344. if (!file_exists($bootstrapFile)) return [];
  345. $bootstrapContent = file_get_contents($bootstrapFile);
  346. $pregArr = [
  347. 'mainTsImport' => '/#main.ts import code start#([\s\S]*?)#main.ts import code end#/i',
  348. 'mainTsStart' => '/#main.ts start code start#([\s\S]*?)#main.ts start code end#/i',
  349. 'appVueImport' => '/#App.vue import code start#([\s\S]*?)#App.vue import code end#/i',
  350. 'appVueOnMounted' => '/#App.vue onMounted code start#([\s\S]*?)#App.vue onMounted code end#/i',
  351. ];
  352. $codeStrArr = [];
  353. foreach ($pregArr as $key => $item) {
  354. preg_match($item, $bootstrapContent, $matches);
  355. if (isset($matches[1]) && $matches[1]) {
  356. $mainImportCodeArr = array_filter(preg_split('/\r\n|\r|\n/', $matches[1]));
  357. if ($mainImportCodeArr) {
  358. $codeStrArr[$key] = "\n";
  359. if (count($mainImportCodeArr) == 1) {
  360. foreach ($mainImportCodeArr as $codeItem) {
  361. $codeStrArr[$key] .= $codeItem . self::buildMarkStr('module-line-mark', $uid, $key);
  362. }
  363. } else {
  364. $codeStrArr[$key] .= self::buildMarkStr('module-multi-line-mark-start', $uid, $key);
  365. foreach ($mainImportCodeArr as $codeItem) {
  366. $codeStrArr[$key] .= $codeItem . "\n";
  367. }
  368. $codeStrArr[$key] .= self::buildMarkStr('module-multi-line-mark-end', $uid, $key);
  369. }
  370. }
  371. }
  372. unset($matches);
  373. }
  374. return $codeStrArr;
  375. }
  376. /**
  377. * 安装 WebBootstrap
  378. */
  379. public static function installWebBootstrap(string $uid, string $dir): void
  380. {
  381. $mainTsKeys = ['mainTsImport', 'mainTsStart'];
  382. $bootstrapCode = self::analysisWebBootstrap($uid, $dir);
  383. $basePath = root_path() . 'web' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR;
  384. $marks = [
  385. 'mainTsImport' => self::buildMarkStr('import-root-mark'),
  386. 'mainTsStart' => self::buildMarkStr('start-root-mark'),
  387. 'appVueImport' => self::buildMarkStr('import-root-mark'),
  388. 'appVueOnMounted' => self::buildMarkStr('onMounted-root-mark'),
  389. ];
  390. foreach ($bootstrapCode as $key => $item) {
  391. if ($item && isset($marks[$key])) {
  392. $filePath = $basePath . (in_array($key, $mainTsKeys) ? 'main.ts' : 'App.vue');
  393. $content = file_get_contents($filePath);
  394. $markPos = stripos($content, $marks[$key]);
  395. if ($markPos && strripos($content, self::buildMarkStr('module-line-mark', $uid, $key)) === false && strripos($content, self::buildMarkStr('module-multi-line-mark-start', $uid, $key)) === false) {
  396. $content = substr_replace($content, $item, $markPos + strlen($marks[$key]), 0);
  397. file_put_contents($filePath, $content);
  398. }
  399. }
  400. }
  401. }
  402. /**
  403. * 卸载 WebBootstrap
  404. */
  405. public static function uninstallWebBootstrap(string $uid): void
  406. {
  407. $mainTsKeys = ['mainTsImport', 'mainTsStart'];
  408. $basePath = root_path() . 'web' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR;
  409. $marksKey = [
  410. 'mainTsImport',
  411. 'mainTsStart',
  412. 'appVueImport',
  413. 'appVueOnMounted',
  414. ];
  415. foreach ($marksKey as $item) {
  416. $filePath = $basePath . (in_array($item, $mainTsKeys) ? 'main.ts' : 'App.vue');
  417. $content = file_get_contents($filePath);
  418. $moduleLineMark = self::buildMarkStr('module-line-mark', $uid, $item);
  419. $moduleMultiLineMarkStart = self::buildMarkStr('module-multi-line-mark-start', $uid, $item);
  420. $moduleMultiLineMarkEnd = self::buildMarkStr('module-multi-line-mark-end', $uid, $item);
  421. // 寻找标记,找到则将其中内容删除
  422. $moduleLineMarkPos = strripos($content, $moduleLineMark);
  423. if ($moduleLineMarkPos !== false) {
  424. $delStartTemp = explode($moduleLineMark, $content);
  425. $delStartPos = strripos(rtrim($delStartTemp[0], "\n"), "\n");
  426. $delEndPos = stripos($content, "\n", $moduleLineMarkPos);
  427. $content = substr_replace($content, '', $delStartPos, $delEndPos - $delStartPos);
  428. }
  429. $moduleMultiLineMarkStartPos = stripos($content, $moduleMultiLineMarkStart);
  430. if ($moduleMultiLineMarkStartPos !== false) {
  431. $moduleMultiLineMarkStartPos--;
  432. $moduleMultiLineMarkEndPos = stripos($content, $moduleMultiLineMarkEnd);
  433. $delLang = ($moduleMultiLineMarkEndPos + strlen($moduleMultiLineMarkEnd)) - $moduleMultiLineMarkStartPos;
  434. $content = substr_replace($content, '', $moduleMultiLineMarkStartPos, $delLang);
  435. }
  436. if ($moduleLineMarkPos || $moduleMultiLineMarkStartPos) {
  437. file_put_contents($filePath, $content);
  438. }
  439. }
  440. }
  441. /**
  442. * 构建 WebBootstrap 需要的各种标记字符串
  443. * @param string $type
  444. * @param string $uid 模块UID
  445. * @param string $extend 扩展数据
  446. * @return string
  447. */
  448. public static function buildMarkStr(string $type, string $uid = '', string $extend = ''): string
  449. {
  450. $importKeys = ['mti', 'avi'];
  451. $extend = match ($extend) {
  452. 'mainTsImport' => 'mti',
  453. 'mainTsStart' => 'mts',
  454. 'appVueImport' => 'avi',
  455. 'appVueOnMounted' => 'avo',
  456. default => '',
  457. };
  458. return match ($type) {
  459. 'import-root-mark' => '// modules import mark, Please do not remove.',
  460. 'start-root-mark' => '// modules start mark, Please do not remove.',
  461. 'onMounted-root-mark' => '// Modules onMounted mark, Please do not remove.',
  462. 'module-line-mark' => ' // Code from module \'' . $uid . "'" . ($extend ? "($extend)" : ''),
  463. 'module-multi-line-mark-start' => (in_array($extend, $importKeys) ? '' : Helper::tab()) . "// Code from module '$uid' start" . ($extend ? "($extend)" : '') . "\n",
  464. 'module-multi-line-mark-end' => (in_array($extend, $importKeys) ? '' : Helper::tab()) . "// Code from module '$uid' end",
  465. default => '',
  466. };
  467. }
  468. public static function getNuxtVersion()
  469. {
  470. $nuxtPackageJsonPath = Filesystem::fsFit(root_path() . 'web-nuxt/package.json');
  471. if (is_file($nuxtPackageJsonPath)) {
  472. $nuxtPackageJson = file_get_contents($nuxtPackageJsonPath);
  473. $nuxtPackageJson = json_decode($nuxtPackageJson, true);
  474. if ($nuxtPackageJson && isset($nuxtPackageJson['version'])) {
  475. return $nuxtPackageJson['version'];
  476. }
  477. }
  478. return false;
  479. }
  480. /**
  481. * 创建 .runtime
  482. */
  483. public static function createRuntime(string $dir): void
  484. {
  485. $runtimeFilePath = $dir . '.runtime';
  486. $files = new RecursiveIteratorIterator(
  487. new RecursiveDirectoryIterator($dir), RecursiveIteratorIterator::LEAVES_ONLY
  488. );
  489. $filePaths = [];
  490. foreach ($files as $file) {
  491. if (!$file->isDir()) {
  492. $pathName = $file->getPathName();
  493. if ($pathName == $runtimeFilePath) continue;
  494. $filePaths[] = [
  495. 'path' => Filesystem::fsFit($pathName),
  496. 'size' => filesize($pathName),
  497. 'md5' => md5_file($pathName),
  498. ];
  499. }
  500. }
  501. file_put_contents($runtimeFilePath, json_encode([
  502. 'files' => $filePaths,
  503. 'pure' => Config::get('buildadmin.module_pure_install'),
  504. ]));
  505. }
  506. /**
  507. * 读取 .runtime
  508. */
  509. public static function getRuntime(string $dir, string $key = ''): mixed
  510. {
  511. $runtimeFilePath = $dir . '.runtime';
  512. $runtimeContent = @file_get_contents($runtimeFilePath);
  513. $runtimeContentArr = json_decode($runtimeContent, true);
  514. if (!$runtimeContentArr) return [];
  515. if ($key) {
  516. return $runtimeContentArr[$key] ?? [];
  517. } else {
  518. return $runtimeContentArr;
  519. }
  520. }
  521. /**
  522. * 获取请求对象
  523. * @return Client
  524. */
  525. protected static function getClient(): Client
  526. {
  527. $options = [
  528. 'base_uri' => Config::get('buildadmin.api_url'),
  529. 'timeout' => 30,
  530. 'connect_timeout' => 30,
  531. 'verify' => false,
  532. 'http_errors' => false,
  533. 'headers' => [
  534. 'X-REQUESTED-WITH' => 'XMLHttpRequest',
  535. 'Referer' => dirname(request()->root(true)),
  536. 'User-Agent' => 'BuildAdminClient',
  537. ]
  538. ];
  539. if (is_null(self::$client)) {
  540. self::$client = new Client($options);
  541. }
  542. return self::$client;
  543. }
  544. }