PluginController.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. <?php
  2. namespace plugin\admin\app\controller;
  3. use GuzzleHttp\Client;
  4. use GuzzleHttp\Exception\GuzzleException;
  5. use plugin\admin\app\common\Util;
  6. use plugin\admin\app\controller\Base;
  7. use process\Monitor;
  8. use support\exception\BusinessException;
  9. use support\Log;
  10. use support\Request;
  11. use support\Response;
  12. use ZIPARCHIVE;
  13. use function array_diff;
  14. use function ini_get;
  15. use function scandir;
  16. use const DIRECTORY_SEPARATOR;
  17. use const PATH_SEPARATOR;
  18. class PluginController extends Base
  19. {
  20. /**
  21. * 不需要鉴权的方法
  22. * @var string[]
  23. */
  24. protected $noNeedAuth = ['schema', 'captcha'];
  25. /**
  26. * @param Request $request
  27. * @return string
  28. * @throws GuzzleException
  29. */
  30. public function index(Request $request)
  31. {
  32. $client = $this->httpClient();
  33. $response = $client->get("/webman-admin/apps");
  34. return (string)$response->getBody();
  35. }
  36. /**
  37. * 列表
  38. * @param Request $request
  39. * @return Response
  40. * @throws GuzzleException
  41. */
  42. public function list(Request $request): Response
  43. {
  44. $installed = $this->getLocalPlugins();
  45. $client = $this->httpClient();
  46. $query = $request->get();
  47. $query['version'] = $this->getAdminVersion();
  48. $response = $client->get('/api/app/list', ['query' => $query]);
  49. $content = $response->getBody()->getContents();
  50. $data = json_decode($content, true);
  51. if (!$data) {
  52. $msg = "/api/app/list return $content";
  53. echo "msg\r\n";
  54. Log::error($msg);
  55. return $this->json(1, '获取数据出错');
  56. }
  57. $disabled = is_phar();
  58. foreach ($data['data']['items'] as $key => $item) {
  59. $name = $item['name'];
  60. $data['data']['items'][$key]['installed'] = $installed[$name] ?? 0;
  61. $data['data']['items'][$key]['disabled'] = $disabled;
  62. }
  63. $items = $data['data']['items'];
  64. $count = $data['data']['total'];
  65. return json(['code' => 0, 'msg' => 'ok', 'data' => $items, 'count' => $count]);
  66. }
  67. /**
  68. * 安装
  69. * @param Request $request
  70. * @return Response
  71. * @throws GuzzleException|BusinessException
  72. */
  73. public function install(Request $request): Response
  74. {
  75. $name = $request->post('name');
  76. $version = $request->post('version');
  77. $installed_version = $this->getPluginVersion($name);
  78. if (!$name || !$version) {
  79. return $this->json(1, '缺少参数');
  80. }
  81. $user = session('app-plugin-user');
  82. if (!$user) {
  83. return $this->json(-1, '请登录');
  84. }
  85. // 获取下载zip文件url
  86. $data = $this->getDownloadUrl($name, $version);
  87. if ($data['code'] != 0) {
  88. return $this->json($data['code'], $data['msg'], $data['data'] ?? []);
  89. }
  90. // 下载zip文件
  91. $base_path = base_path() . "/plugin/$name";
  92. $zip_file = "$base_path.zip";
  93. $extract_to = base_path() . '/plugin/';
  94. $this->downloadZipFile($data['data']['url'], $zip_file);
  95. $has_zip_archive = class_exists(ZipArchive::class, false);
  96. if (!$has_zip_archive) {
  97. $cmd = $this->getUnzipCmd($zip_file, $extract_to);
  98. if (!$cmd) {
  99. throw new BusinessException('请给php安装zip模块或者给系统安装unzip命令');
  100. }
  101. if (!function_exists('proc_open')) {
  102. throw new BusinessException('请解除proc_open函数的禁用或者给php安装zip模块');
  103. }
  104. }
  105. Util::pauseFileMonitor();
  106. try {
  107. // 解压zip到plugin目录
  108. if ($has_zip_archive) {
  109. $zip = new ZipArchive;
  110. $zip->open($zip_file, ZIPARCHIVE::CHECKCONS);
  111. }
  112. $context = null;
  113. $install_class = "\\plugin\\$name\\api\\Install";
  114. if ($installed_version) {
  115. // 执行beforeUpdate
  116. if (class_exists($install_class) && method_exists($install_class, 'beforeUpdate')) {
  117. $context = call_user_func([$install_class, 'beforeUpdate'], $installed_version, $version);
  118. }
  119. }
  120. if (!empty($zip)) {
  121. $zip->extractTo(base_path() . '/plugin/');
  122. unset($zip);
  123. } else {
  124. $this->unzipWithCmd($cmd);
  125. }
  126. unlink($zip_file);
  127. if ($installed_version) {
  128. // 执行update更新
  129. if (class_exists($install_class) && method_exists($install_class, 'update')) {
  130. call_user_func([$install_class, 'update'], $installed_version, $version, $context);
  131. }
  132. } else {
  133. // 执行install安装
  134. if (class_exists($install_class) && method_exists($install_class, 'install')) {
  135. call_user_func([$install_class, 'install'], $version);
  136. }
  137. }
  138. } finally {
  139. Util::resumeFileMonitor();
  140. }
  141. Util::reloadWebman();
  142. return $this->json(0);
  143. }
  144. /**
  145. * 卸载
  146. * @param Request $request
  147. * @return Response
  148. */
  149. public function uninstall(Request $request): Response
  150. {
  151. $name = $request->post('name');
  152. $version = $request->post('version');
  153. if (!$name || !preg_match('/^[a-zA-Z0-9_]+$/', $name)) {
  154. return $this->json(1, '参数错误');
  155. }
  156. // 获得插件路径
  157. clearstatcache();
  158. $path = get_realpath(base_path() . "/plugin/$name");
  159. if (!$path || !is_dir($path)) {
  160. return $this->json(1, '已经删除');
  161. }
  162. // 执行uninstall卸载
  163. $install_class = "\\plugin\\$name\\api\\Install";
  164. if (class_exists($install_class) && method_exists($install_class, 'uninstall')) {
  165. call_user_func([$install_class, 'uninstall'], $version);
  166. }
  167. // 删除目录
  168. clearstatcache();
  169. if (is_dir($path)) {
  170. $monitor_support_pause = method_exists(Monitor::class, 'pause');
  171. if ($monitor_support_pause) {
  172. Monitor::pause();
  173. }
  174. try {
  175. $this->rmDir($path);
  176. } finally {
  177. if ($monitor_support_pause) {
  178. Monitor::resume();
  179. }
  180. }
  181. }
  182. clearstatcache();
  183. Util::reloadWebman();
  184. return $this->json(0);
  185. }
  186. /**
  187. * 支付
  188. * @param Request $request
  189. * @return string|Response
  190. * @throws GuzzleException
  191. */
  192. public function pay(Request $request)
  193. {
  194. $app = $request->get('app');
  195. if (!$app) {
  196. return response('app not found');
  197. }
  198. $token = session('app-plugin-token');
  199. if (!$token) {
  200. return 'Please login workerman.net';
  201. }
  202. $client = $this->httpClient();
  203. $response = $client->get("/payment/app/$app/$token");
  204. return (string)$response->getBody();
  205. }
  206. /**
  207. * 登录验证码
  208. * @param Request $request
  209. * @return Response
  210. * @throws GuzzleException
  211. */
  212. public function captcha(Request $request): Response
  213. {
  214. $client = $this->httpClient();
  215. $response = $client->get('/user/captcha?type=login');
  216. $sid_str = $response->getHeaderLine('Set-Cookie');
  217. if (preg_match('/PHPSID=([a-zA-Z_0-9]+?);/', $sid_str, $match)) {
  218. $sid = $match[1];
  219. session()->set('app-plugin-token', $sid);
  220. }
  221. return response($response->getBody()->getContents())->withHeader('Content-Type', 'image/jpeg');
  222. }
  223. /**
  224. * 登录官网
  225. * @param Request $request
  226. * @return Response|string
  227. * @throws GuzzleException
  228. */
  229. public function login(Request $request)
  230. {
  231. $client = $this->httpClient();
  232. if ($request->method() === 'GET') {
  233. $response = $client->get("/webman-admin/login");
  234. return (string)$response->getBody();
  235. }
  236. $response = $client->post('/api/user/login', [
  237. 'form_params' => [
  238. 'email' => $request->post('username'),
  239. 'password' => $request->post('password'),
  240. 'captcha' => $request->post('captcha')
  241. ]
  242. ]);
  243. $content = $response->getBody()->getContents();
  244. $data = json_decode($content, true);
  245. if (!$data) {
  246. $msg = "/api/user/login return $content";
  247. echo "msg\r\n";
  248. Log::error($msg);
  249. return $this->json(1, '发生错误');
  250. }
  251. if ($data['code'] != 0) {
  252. return $this->json($data['code'], $data['msg']);
  253. }
  254. session()->set('app-plugin-user', [
  255. 'uid' => $data['data']['uid']
  256. ]);
  257. return $this->json(0);
  258. }
  259. /**
  260. * 获取zip下载url
  261. * @param $name
  262. * @param $version
  263. * @return mixed
  264. * @throws BusinessException
  265. * @throws GuzzleException
  266. */
  267. protected function getDownloadUrl($name, $version)
  268. {
  269. $client = $this->httpClient();
  270. $response = $client->get("/app/download/$name?version=$version");
  271. $content = $response->getBody()->getContents();
  272. $data = json_decode($content, true);
  273. if (!$data) {
  274. $msg = "/api/app/download return $content";
  275. Log::error($msg);
  276. throw new BusinessException('访问官方接口失败 ' . $response->getStatusCode() . ' ' . $response->getReasonPhrase());
  277. }
  278. if ($data['code'] && $data['code'] != -1 && $data['code'] != -2) {
  279. throw new BusinessException($data['msg']);
  280. }
  281. if ($data['code'] == 0 && !isset($data['data']['url'])) {
  282. throw new BusinessException('官方接口返回数据错误');
  283. }
  284. return $data;
  285. }
  286. /**
  287. * 下载zip
  288. * @param $url
  289. * @param $file
  290. * @return void
  291. * @throws BusinessException
  292. * @throws GuzzleException
  293. */
  294. protected function downloadZipFile($url, $file)
  295. {
  296. $client = $this->downloadClient();
  297. $response = $client->get($url);
  298. $body = $response->getBody();
  299. $status = $response->getStatusCode();
  300. if ($status == 404) {
  301. throw new BusinessException('安装包不存在');
  302. }
  303. $zip_content = $body->getContents();
  304. if (empty($zip_content)) {
  305. throw new BusinessException('安装包不存在');
  306. }
  307. file_put_contents($file, $zip_content);
  308. }
  309. /**
  310. * 获取系统支持的解压命令
  311. * @param $zip_file
  312. * @param $extract_to
  313. * @return mixed|string|null
  314. */
  315. protected function getUnzipCmd($zip_file, $extract_to)
  316. {
  317. if ($cmd = $this->findCmd('unzip')) {
  318. $cmd = "$cmd -o -qq $zip_file -d $extract_to";
  319. } else if ($cmd = $this->findCmd('7z')) {
  320. $cmd = "$cmd x -bb0 -y $zip_file -o$extract_to";
  321. } else if ($cmd = $this->findCmd('7zz')) {
  322. $cmd = "$cmd x -bb0 -y $zip_file -o$extract_to";
  323. }
  324. return $cmd;
  325. }
  326. /**
  327. * 使用解压命令解压
  328. * @param $cmd
  329. * @return void
  330. * @throws BusinessException
  331. */
  332. protected function unzipWithCmd($cmd)
  333. {
  334. $desc = [
  335. 0 => ["pipe", "r"],
  336. 1 => ["pipe", "w"],
  337. 2 => ["pipe", "w"],
  338. ];
  339. $handler = proc_open($cmd, $desc, $pipes);
  340. if (!is_resource($handler)) {
  341. throw new BusinessException("解压zip时出错:proc_open调用失败");
  342. }
  343. $err = fread($pipes[2], 1024);
  344. fclose($pipes[2]);
  345. proc_close($handler);
  346. if ($err) {
  347. throw new BusinessException("解压zip时出错:$err");
  348. }
  349. }
  350. /**
  351. * 获取已安装的插件列表
  352. * @return array
  353. */
  354. protected function getLocalPlugins(): array
  355. {
  356. clearstatcache();
  357. $installed = [];
  358. $plugin_names = array_diff(scandir(base_path() . '/plugin/'), array('.', '..')) ?: [];
  359. foreach ($plugin_names as $plugin_name) {
  360. if (is_dir(base_path() . "/plugin/$plugin_name") && $version = $this->getPluginVersion($plugin_name)) {
  361. $installed[$plugin_name] = $version;
  362. }
  363. }
  364. return $installed;
  365. }
  366. /**
  367. * 获取已安装的插件列表
  368. * @param Request $request
  369. * @return Response
  370. */
  371. public function getInstalledPlugins(Request $request): Response
  372. {
  373. return $this->json(0, 'ok', $this->getLocalPlugins());
  374. }
  375. /**
  376. * 获取本地插件版本
  377. * @param $name
  378. * @return array|mixed|null
  379. */
  380. protected function getPluginVersion($name)
  381. {
  382. if (!is_file($file = base_path() . "/plugin/$name/config/app.php")) {
  383. return null;
  384. }
  385. $config = include $file;
  386. return $config['version'] ?? null;
  387. }
  388. /**
  389. * 获取webman/admin版本
  390. * @return string
  391. */
  392. protected function getAdminVersion(): string
  393. {
  394. return config('plugin.admin.app.version', '');
  395. }
  396. /**
  397. * 删除目录
  398. * @param $src
  399. * @return void
  400. */
  401. protected function rmDir($src)
  402. {
  403. $dir = opendir($src);
  404. while (false !== ($file = readdir($dir))) {
  405. if (($file != '.') && ($file != '..')) {
  406. $full = $src . '/' . $file;
  407. if (is_dir($full)) {
  408. $this->rmDir($full);
  409. } else {
  410. unlink($full);
  411. }
  412. }
  413. }
  414. closedir($dir);
  415. rmdir($src);
  416. }
  417. /**
  418. * 获取httpclient
  419. * @return Client
  420. */
  421. protected function httpClient(): Client
  422. {
  423. // 下载zip
  424. $options = [
  425. 'base_uri' => config('plugin.admin.app.plugin_market_host'),
  426. 'timeout' => 60,
  427. 'connect_timeout' => 5,
  428. 'verify' => false,
  429. 'http_errors' => false,
  430. 'headers' => [
  431. 'Referer' => \request()->fullUrl(),
  432. 'User-Agent' => 'webman-app-plugin',
  433. 'Accept' => 'application/json;charset=UTF-8',
  434. ]
  435. ];
  436. if ($token = session('app-plugin-token')) {
  437. $options['headers']['Cookie'] = "PHPSID=$token;";
  438. }
  439. return new Client($options);
  440. }
  441. /**
  442. * 获取下载httpclient
  443. * @return Client
  444. */
  445. protected function downloadClient(): Client
  446. {
  447. // 下载zip
  448. $options = [
  449. 'timeout' => 59,
  450. 'connect_timeout' => 5,
  451. 'verify' => false,
  452. 'http_errors' => false,
  453. 'headers' => [
  454. 'Referer' => \request()->fullUrl(),
  455. 'User-Agent' => 'webman-app-plugin',
  456. ]
  457. ];
  458. if ($token = session('app-plugin-token')) {
  459. $options['headers']['Cookie'] = "PHPSID=$token;";
  460. }
  461. return new Client($options);
  462. }
  463. /**
  464. * 查找系统命令
  465. * @param string $name
  466. * @param string|null $default
  467. * @param array $extraDirs
  468. * @return mixed|string|null
  469. */
  470. protected function findCmd(string $name, string $default = null, array $extraDirs = [])
  471. {
  472. if (ini_get('open_basedir')) {
  473. $searchPath = array_merge(explode(PATH_SEPARATOR, ini_get('open_basedir')), $extraDirs);
  474. $dirs = [];
  475. foreach ($searchPath as $path) {
  476. if (@is_dir($path)) {
  477. $dirs[] = $path;
  478. } else {
  479. if (basename($path) == $name && @is_executable($path)) {
  480. return $path;
  481. }
  482. }
  483. }
  484. } else {
  485. $dirs = array_merge(
  486. explode(PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')),
  487. $extraDirs
  488. );
  489. }
  490. $suffixes = [''];
  491. if ('\\' === DIRECTORY_SEPARATOR) {
  492. $pathExt = getenv('PATHEXT');
  493. $suffixes = array_merge($pathExt ? explode(PATH_SEPARATOR, $pathExt) : ['.exe', '.bat', '.cmd', '.com'], $suffixes);
  494. }
  495. foreach ($suffixes as $suffix) {
  496. foreach ($dirs as $dir) {
  497. if (@is_file($file = $dir . DIRECTORY_SEPARATOR . $name . $suffix) && ('\\' === DIRECTORY_SEPARATOR || @is_executable($file))) {
  498. return $file;
  499. }
  500. }
  501. }
  502. return $default;
  503. }
  504. }