Server.php 18 KB

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