InstallController.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. <?php
  2. namespace plugin\admin\app\controller;
  3. use Illuminate\Database\Capsule\Manager;
  4. use plugin\admin\app\common\Util;
  5. use plugin\admin\app\model\Admin;
  6. use support\exception\BusinessException;
  7. use support\Request;
  8. use support\Response;
  9. use Webman\Captcha\CaptchaBuilder;
  10. /**
  11. * 安装
  12. */
  13. class InstallController extends Base
  14. {
  15. /**
  16. * 不需要登录的方法
  17. * @var string[]
  18. */
  19. protected $noNeedLogin = ['step1', 'step2'];
  20. /**
  21. * 设置数据库
  22. * @param Request $request
  23. * @return Response
  24. * @throws BusinessException|\Throwable
  25. */
  26. public function step1(Request $request): Response
  27. {
  28. $database_config_file = base_path() . '/plugin/admin/config/database.php';
  29. clearstatcache();
  30. if (is_file($database_config_file)) {
  31. return $this->json(1, '管理后台已经安装!如需重新安装,请删除该插件数据库配置文件并重启');
  32. }
  33. if (!class_exists(CaptchaBuilder::class) || !class_exists(Manager::class)) {
  34. return $this->json(1, '请运行 composer require -W illuminate/database 安装illuminate/database组件并重启');
  35. }
  36. $user = $request->post('user');
  37. $password = $request->post('password');
  38. $database = $request->post('database');
  39. $host = $request->post('host');
  40. $port = (int)$request->post('port') ?: 3306;
  41. $overwrite = $request->post('overwrite');
  42. try {
  43. $db = $this->getPdo($host, $user, $password, $port);
  44. $smt = $db->query("show databases like '$database'");
  45. if (empty($smt->fetchAll())) {
  46. $db->exec("create database $database");
  47. }
  48. $db->exec("use $database");
  49. $smt = $db->query("show tables");
  50. $tables = $smt->fetchAll();
  51. } catch (\Throwable $e) {
  52. if (stripos($e, 'Access denied for user')) {
  53. return $this->json(1, '数据库用户名或密码错误');
  54. }
  55. if (stripos($e, 'Connection refused')) {
  56. return $this->json(1, 'Connection refused. 请确认数据库IP端口是否正确,数据库已经启动');
  57. }
  58. if (stripos($e, 'timed out')) {
  59. return $this->json(1, '数据库连接超时,请确认数据库IP端口是否正确,安全组及防火墙已经放行端口');
  60. }
  61. throw $e;
  62. }
  63. $tables_to_install = [
  64. 'wa_admins',
  65. 'wa_admin_roles',
  66. 'wa_roles',
  67. 'wa_rules',
  68. 'wa_options',
  69. 'wa_users',
  70. 'wa_uploads',
  71. ];
  72. $tables_exist = [];
  73. foreach ($tables as $table) {
  74. $tables_exist[] = current($table);
  75. }
  76. $tables_conflict = array_intersect($tables_to_install, $tables_exist);
  77. if (!$overwrite) {
  78. if ($tables_conflict) {
  79. return $this->json(1, '以下表' . implode(',', $tables_conflict) . '已经存在,如需覆盖请选择强制覆盖');
  80. }
  81. } else {
  82. foreach ($tables_conflict as $table) {
  83. $db->exec("DROP TABLE `$table`");
  84. }
  85. }
  86. $sql_file = base_path() . '/plugin/admin/install.sql';
  87. if (!is_file($sql_file)) {
  88. return $this->json(1, '数据库SQL文件不存在');
  89. }
  90. $sql_query = file_get_contents($sql_file);
  91. $sql_query = $this->removeComments($sql_query);
  92. $sql_query = $this->splitSqlFile($sql_query, ';');
  93. foreach ($sql_query as $sql) {
  94. $db->exec($sql);
  95. }
  96. // 导入菜单
  97. $menus = include base_path() . '/plugin/admin/config/menu.php';
  98. // 安装过程中没有数据库配置,无法使用api\Menu::import()方法
  99. $this->importMenu($menus, $db);
  100. $config_content = <<<EOF
  101. <?php
  102. return [
  103. 'default' => 'mysql',
  104. 'connections' => [
  105. 'mysql' => [
  106. 'driver' => 'mysql',
  107. 'host' => '$host',
  108. 'port' => '$port',
  109. 'database' => '$database',
  110. 'username' => '$user',
  111. 'password' => '$password',
  112. 'charset' => 'utf8mb4',
  113. 'collation' => 'utf8mb4_general_ci',
  114. 'prefix' => '',
  115. 'strict' => true,
  116. 'engine' => null,
  117. ],
  118. ],
  119. ];
  120. EOF;
  121. file_put_contents($database_config_file, $config_content);
  122. $think_orm_config = <<<EOF
  123. <?php
  124. return [
  125. 'default' => 'mysql',
  126. 'connections' => [
  127. 'mysql' => [
  128. // 数据库类型
  129. 'type' => 'mysql',
  130. // 服务器地址
  131. 'hostname' => '$host',
  132. // 数据库名
  133. 'database' => '$database',
  134. // 数据库用户名
  135. 'username' => '$user',
  136. // 数据库密码
  137. 'password' => '$password',
  138. // 数据库连接端口
  139. 'hostport' => $port,
  140. // 数据库连接参数
  141. 'params' => [
  142. // 连接超时3秒
  143. \PDO::ATTR_TIMEOUT => 3,
  144. ],
  145. // 数据库编码默认采用utf8
  146. 'charset' => 'utf8mb4',
  147. // 数据库表前缀
  148. 'prefix' => '',
  149. // 断线重连
  150. 'break_reconnect' => true,
  151. // 关闭SQL监听日志
  152. 'trigger_sql' => true,
  153. // 自定义分页类
  154. 'bootstrap' => ''
  155. ],
  156. ],
  157. ];
  158. EOF;
  159. file_put_contents(base_path() . '/plugin/admin/config/thinkorm.php', $think_orm_config);
  160. // 尝试reload
  161. if (function_exists('posix_kill')) {
  162. set_error_handler(function () {});
  163. posix_kill(posix_getppid(), SIGUSR1);
  164. restore_error_handler();
  165. }
  166. return $this->json(0);
  167. }
  168. /**
  169. * 设置管理员
  170. * @param Request $request
  171. * @return Response
  172. * @throws BusinessException
  173. */
  174. public function step2(Request $request): Response
  175. {
  176. $username = $request->post('username');
  177. $password = $request->post('password');
  178. $password_confirm = $request->post('password_confirm');
  179. if ($password != $password_confirm) {
  180. return $this->json(1, '两次密码不一致');
  181. }
  182. if (!is_file($config_file = base_path() . '/plugin/admin/config/database.php')) {
  183. return $this->json(1, '请先完成第一步数据库配置');
  184. }
  185. $config = include $config_file;
  186. $connection = $config['connections']['mysql'];
  187. $pdo = $this->getPdo($connection['host'], $connection['username'], $connection['password'], $connection['port'], $connection['database']);
  188. if ($pdo->query('select * from `wa_admins`')->fetchAll()) {
  189. return $this->json(1, '后台已经安装完毕,无法通过此页面创建管理员');
  190. }
  191. $smt = $pdo->prepare("insert into `wa_admins` (`username`, `password`, `nickname`, `created_at`, `updated_at`) values (:username, :password, :nickname, :created_at, :updated_at)");
  192. $time = date('Y-m-d H:i:s');
  193. $data = [
  194. 'username' => $username,
  195. 'password' => Util::passwordHash($password),
  196. 'nickname' => '超级管理员',
  197. 'created_at' => $time,
  198. 'updated_at' => $time
  199. ];
  200. foreach ($data as $key => $value) {
  201. $smt->bindValue($key, $value);
  202. }
  203. $smt->execute();
  204. $admin_id = $pdo->lastInsertId();
  205. $smt = $pdo->prepare("insert into `wa_admin_roles` (`role_id`, `admin_id`) values (:role_id, :admin_id)");
  206. $smt->bindValue('role_id', 1);
  207. $smt->bindValue('admin_id', $admin_id);
  208. $smt->execute();
  209. $request->session()->flush();
  210. return $this->json(0);
  211. }
  212. /**
  213. * 添加菜单
  214. * @param array $menu
  215. * @param \PDO $pdo
  216. * @return int
  217. */
  218. protected function addMenu(array $menu, \PDO $pdo): int
  219. {
  220. $allow_columns = ['title', 'key', 'icon', 'href', 'pid', 'weight', 'type'];
  221. $data = [];
  222. foreach ($allow_columns as $column) {
  223. if (isset($menu[$column])) {
  224. $data[$column] = $menu[$column];
  225. }
  226. }
  227. $time = date('Y-m-d H:i:s');
  228. $data['created_at'] = $data['updated_at'] = $time;
  229. $values = [];
  230. foreach ($data as $k => $v) {
  231. $values[] = ":$k";
  232. }
  233. $columns = array_keys($data);
  234. foreach ($columns as $k => $column) {
  235. $columns[$k] = "`$column`";
  236. }
  237. $sql = "insert into wa_rules (" .implode(',', $columns). ") values (" . implode(',', $values) . ")";
  238. $smt = $pdo->prepare($sql);
  239. foreach ($data as $key => $value) {
  240. $smt->bindValue($key, $value);
  241. }
  242. $smt->execute();
  243. return $pdo->lastInsertId();
  244. }
  245. /**
  246. * 导入菜单
  247. * @param array $menu_tree
  248. * @param \PDO $pdo
  249. * @return void
  250. */
  251. protected function importMenu(array $menu_tree, \PDO $pdo)
  252. {
  253. if (is_numeric(key($menu_tree)) && !isset($menu_tree['key'])) {
  254. foreach ($menu_tree as $item) {
  255. $this->importMenu($item, $pdo);
  256. }
  257. return;
  258. }
  259. $children = $menu_tree['children'] ?? [];
  260. unset($menu_tree['children']);
  261. $smt = $pdo->prepare("select * from wa_rules where `key`=:key limit 1");
  262. $smt->execute(['key' => $menu_tree['key']]);
  263. $old_menu = $smt->fetch();
  264. if ($old_menu) {
  265. $pid = $old_menu['id'];
  266. $params = [
  267. 'title' => $menu_tree['title'],
  268. 'icon' => $menu_tree['icon'] ?? '',
  269. 'key' => $menu_tree['key'],
  270. ];
  271. $sql = "update wa_rules set title=:title, icon=:icon where `key`=:key";
  272. $smt = $pdo->prepare($sql);
  273. $smt->execute($params);
  274. } else {
  275. $pid = $this->addMenu($menu_tree, $pdo);
  276. }
  277. foreach ($children as $menu) {
  278. $menu['pid'] = $pid;
  279. $this->importMenu($menu, $pdo);
  280. }
  281. }
  282. /**
  283. * 去除sql文件中的注释
  284. * @param $sql
  285. * @return string
  286. */
  287. protected function removeComments($sql): string
  288. {
  289. return preg_replace("/(\n--[^\n]*)/","", $sql);
  290. }
  291. /**
  292. * 分割sql文件
  293. * @param $sql
  294. * @param $delimiter
  295. * @return array
  296. */
  297. function splitSqlFile($sql, $delimiter): array
  298. {
  299. $tokens = explode($delimiter, $sql);
  300. $output = array();
  301. $matches = array();
  302. $token_count = count($tokens);
  303. for ($i = 0; $i < $token_count; $i++) {
  304. if (($i != ($token_count - 1)) || (strlen($tokens[$i] > 0))) {
  305. $total_quotes = preg_match_all("/'/", $tokens[$i], $matches);
  306. $escaped_quotes = preg_match_all("/(?<!\\\\)(\\\\\\\\)*\\\\'/", $tokens[$i], $matches);
  307. $unescaped_quotes = $total_quotes - $escaped_quotes;
  308. if (($unescaped_quotes % 2) == 0) {
  309. $output[] = $tokens[$i];
  310. $tokens[$i] = "";
  311. } else {
  312. $temp = $tokens[$i] . $delimiter;
  313. $tokens[$i] = "";
  314. $complete_stmt = false;
  315. for ($j = $i + 1; (!$complete_stmt && ($j < $token_count)); $j++) {
  316. $total_quotes = preg_match_all("/'/", $tokens[$j], $matches);
  317. $escaped_quotes = preg_match_all("/(?<!\\\\)(\\\\\\\\)*\\\\'/", $tokens[$j], $matches);
  318. $unescaped_quotes = $total_quotes - $escaped_quotes;
  319. if (($unescaped_quotes % 2) == 1) {
  320. $output[] = $temp . $tokens[$j];
  321. $tokens[$j] = "";
  322. $temp = "";
  323. $complete_stmt = true;
  324. $i = $j;
  325. } else {
  326. $temp .= $tokens[$j] . $delimiter;
  327. $tokens[$j] = "";
  328. }
  329. }
  330. }
  331. }
  332. }
  333. return $output;
  334. }
  335. /**
  336. * 获取pdo连接
  337. * @param $host
  338. * @param $username
  339. * @param $password
  340. * @param $port
  341. * @param $database
  342. * @return \PDO
  343. */
  344. protected function getPdo($host, $username, $password, $port, $database = null): \PDO
  345. {
  346. $dsn = "mysql:host=$host;port=$port;";
  347. if ($database) {
  348. $dsn .= "dbname=$database";
  349. }
  350. $params = [
  351. \PDO::MYSQL_ATTR_INIT_COMMAND => "set names utf8mb4",
  352. \PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
  353. \PDO::ATTR_EMULATE_PREPARES => false,
  354. \PDO::ATTR_TIMEOUT => 5,
  355. \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
  356. ];
  357. return new \PDO($dsn, $username, $password, $params);
  358. }
  359. }