Server.php 22 KB

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