'8.2.0', 'npm' => '9.8.1', 'cnpm' => '7.1.0', 'node' => '20.14.0', 'yarn' => '1.2.0', 'pnpm' => '6.32.13', ]; /** * 安装完成标记 */ static string $InstallationCompletionMark = 'install-end'; /** * 命令执行窗口(exec 为 SSE 长连接,不会返回) * @throws Throwable */ public function terminal(Request $request): Response { $this->setRequest($request); if ($this->isInstallComplete()) { return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %file% file first', ['%file%' => 'public/' . self::$lockFileName])); } (new Terminal())->exec(false); return $this->success(); // unreachable: exec() blocks with SSE stream } public function changePackageManager(Request $request): Response { $this->setRequest($request); if ($this->isInstallComplete()) { return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %file% file first', ['%file%' => 'public/' . self::$lockFileName])); } $newPackageManager = $request->post('manager', config('terminal.npm_package_manager')); if (Terminal::changeTerminalConfig()) { return $this->success('', [ 'manager' => $newPackageManager ]); } return $this->error(__('Failed to switch package manager. Please modify the configuration file manually:%s', ['config/terminal.php'])); } /** * 环境基础检查 */ public function envBaseCheck(Request $request): Response { $this->setRequest($request); if ($this->isInstallComplete()) { return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %file% file first', ['%file%' => 'public/' . self::$lockFileName]), []); } if (($_ENV['DATABASE_TYPE'] ?? getenv('DATABASE_TYPE'))) { return $this->error(__('The .env file with database configuration was detected. Please clean up and try again!')); } // php版本-start $phpVersion = phpversion(); $phpBit = PHP_INT_SIZE == 8 ? self::X64 : self::X86; $phpVersionCompare = Version::compare(self::$needDependentVersion['php'], $phpVersion); if (!$phpVersionCompare) { $phpVersionLink = [ [ 'name' => __('need') . ' >= ' . self::$needDependentVersion['php'], 'type' => 'text' ], [ 'name' => __('How to solve?'), 'title' => __('Click to see how to solve it'), 'type' => 'faq', 'url' => 'https://doc.buildadmin.com/guide/install/preparePHP.html' ] ]; } elseif ($phpBit != self::X64) { $phpVersionLink = [ [ 'name' => __('need') . ' x64 PHP', 'type' => 'text' ], [ 'name' => __('How to solve?'), 'title' => __('Click to see how to solve it'), 'type' => 'faq', 'url' => 'https://doc.buildadmin.com/guide/install/preparePHP.html' ] ]; } // php版本-end // 配置文件-start(分别检测目录和文件,便于定位问题) $configDir = rtrim(config_path(), '/\\'); $dbConfigFile = $configDir . DIRECTORY_SEPARATOR . self::$dbConfigFileName; $configDirWritable = Filesystem::pathIsWritable($configDir); $dbConfigWritable = Filesystem::pathIsWritable($dbConfigFile); $configIsWritable = $configDirWritable && $dbConfigWritable; if (!$configIsWritable) { $configIsWritableLink = [ [ 'name' => __('View reason'), 'title' => __('Click to view the reason'), 'type' => 'faq', 'url' => 'https://doc.buildadmin.com/guide/install/dirNoPermission.html' ] ]; } // 配置文件-end // public-start $publicIsWritable = Filesystem::pathIsWritable(public_path()); if (!$publicIsWritable) { $publicIsWritableLink = [ [ 'name' => __('View reason'), 'title' => __('Click to view the reason'), 'type' => 'faq', 'url' => 'https://doc.buildadmin.com/guide/install/dirNoPermission.html' ] ]; } // public-end // PDO-start $phpPdo = extension_loaded("PDO") && extension_loaded('pdo_mysql'); if (!$phpPdo) { $phpPdoLink = [ [ 'name' => __('PDO extensions need to be installed'), 'type' => 'text' ], [ 'name' => __('How to solve?'), 'title' => __('Click to see how to solve it'), 'type' => 'faq', 'url' => 'https://doc.buildadmin.com/guide/install/missingExtension.html' ] ]; } // PDO-end // GD2和freeType-start $phpGd2 = extension_loaded('gd') && function_exists('imagettftext'); if (!$phpGd2) { $phpGd2Link = [ [ 'name' => __('The gd extension and freeType library need to be installed'), 'type' => 'text' ], [ 'name' => __('How to solve?'), 'title' => __('Click to see how to solve it'), 'type' => 'faq', 'url' => 'https://doc.buildadmin.com/guide/install/gdFail.html' ] ]; } // GD2和freeType-end // proc_open $phpProc = function_exists('proc_open') && function_exists('proc_close') && function_exists('proc_get_status'); if (!$phpProc) { $phpProcLink = [ [ 'name' => __('View reason'), 'title' => __('proc_open or proc_close functions in PHP Ini is disabled'), 'type' => 'faq', 'url' => 'https://doc.buildadmin.com/guide/install/disablement.html' ], [ 'name' => __('How to modify'), 'title' => __('Click to view how to modify'), 'type' => 'faq', 'url' => 'https://doc.buildadmin.com/guide/install/disablement.html' ], [ 'name' => __('Security assurance?'), 'title' => __('Using the installation service correctly will not cause any potential security problems. Click to view the details'), 'type' => 'faq', 'url' => 'https://doc.buildadmin.com/guide/install/senior.html' ], ]; } // proc_open-end return $this->success('', [ 'php_version' => [ 'describe' => $phpVersion . " ($phpBit)", 'state' => $phpVersionCompare && $phpBit == self::X64 ? self::$ok : self::$fail, 'link' => $phpVersionLink ?? [], ], 'config_is_writable' => [ 'describe' => $configIsWritable ? self::writableStateDescribe(true) : (self::writableStateDescribe(false) . ' [' . $configDir . ']'), 'state' => $configIsWritable ? self::$ok : self::$fail, 'link' => $configIsWritableLink ?? [] ], 'public_is_writable' => [ 'describe' => self::writableStateDescribe($publicIsWritable), 'state' => $publicIsWritable ? self::$ok : self::$fail, 'link' => $publicIsWritableLink ?? [] ], 'php_pdo' => [ 'describe' => $phpPdo ? __('already installed') : __('Not installed'), 'state' => $phpPdo ? self::$ok : self::$fail, 'link' => $phpPdoLink ?? [] ], 'php_gd2' => [ 'describe' => $phpGd2 ? __('already installed') : __('Not installed'), 'state' => $phpGd2 ? self::$ok : self::$fail, 'link' => $phpGd2Link ?? [] ], 'php_proc' => [ 'describe' => $phpProc ? __('Allow execution') : __('disabled'), 'state' => $phpProc ? self::$ok : self::$warn, 'link' => $phpProcLink ?? [] ], ]); } /** * npm环境检查 */ public function envNpmCheck(Request $request): Response { $this->setRequest($request); if ($this->isInstallComplete()) { return $this->error('', [], 2); } $packageManager = $request->post('manager', 'none'); // npm $npmVersion = Version::getVersion('npm'); $npmVersionCompare = Version::compare(self::$needDependentVersion['npm'], $npmVersion); if (!$npmVersionCompare || !$npmVersion) { $npmVersionLink = [ [ 'name' => __('need') . ' >= ' . self::$needDependentVersion['npm'], 'type' => 'text' ], [ 'name' => __('How to solve?'), 'title' => __('Click to see how to solve it'), 'type' => 'faq', 'url' => 'https://doc.buildadmin.com/guide/install/prepareNpm.html' ] ]; } // 包管理器 $pmVersionLink = []; $pmVersion = __('nothing'); $pmVersionCompare = false; if (in_array($packageManager, ['npm', 'cnpm', 'pnpm', 'yarn'])) { $pmVersion = Version::getVersion($packageManager); $pmVersionCompare = Version::compare(self::$needDependentVersion[$packageManager], $pmVersion); if (!$pmVersion) { $pmVersionLink[] = [ 'name' => __('need') . ' >= ' . self::$needDependentVersion[$packageManager], 'type' => 'text' ]; if ($npmVersionCompare) { $pmVersionLink[] = [ 'name' => __('Click Install %s', [$packageManager]), 'title' => '', 'type' => 'install-package-manager' ]; } else { $pmVersionLink[] = [ 'name' => __('Please install NPM first'), 'type' => 'text' ]; } } elseif (!$pmVersionCompare) { $pmVersionLink[] = [ 'name' => __('need') . ' >= ' . self::$needDependentVersion[$packageManager], 'type' => 'text' ]; $pmVersionLink[] = [ 'name' => __('Please upgrade %s version', [$packageManager]), 'type' => 'text' ]; } } elseif ($packageManager == 'ni') { $pmVersionCompare = true; } // nodejs $nodejsVersion = Version::getVersion('node'); $nodejsVersionCompare = Version::compare(self::$needDependentVersion['node'], $nodejsVersion); if (!$nodejsVersionCompare || !$nodejsVersion) { $nodejsVersionLink = [ [ 'name' => __('need') . ' >= ' . self::$needDependentVersion['node'], 'type' => 'text' ], [ 'name' => __('How to solve?'), 'title' => __('Click to see how to solve it'), 'type' => 'faq', 'url' => 'https://doc.buildadmin.com/guide/install/prepareNodeJs.html' ] ]; } return $this->success('', [ 'npm_version' => [ 'describe' => $npmVersion ?: __('Acquisition failed'), 'state' => $npmVersionCompare ? self::$ok : self::$warn, 'link' => $npmVersionLink ?? [], ], 'nodejs_version' => [ 'describe' => $nodejsVersion ?: __('Acquisition failed'), 'state' => $nodejsVersionCompare ? self::$ok : self::$warn, 'link' => $nodejsVersionLink ?? [] ], 'npm_package_manager' => [ 'describe' => $pmVersion ?: __('Acquisition failed'), 'state' => $pmVersionCompare ? self::$ok : self::$warn, 'link' => $pmVersionLink ?? [], ] ]); } /** * 测试数据库连接 */ public function testDatabase(Request $request): Response { $this->setRequest($request); $database = [ 'hostname' => $request->post('hostname'), 'username' => $request->post('username'), 'password' => $request->post('password'), 'hostport' => $request->post('hostport'), 'database' => '', ]; $conn = $this->connectDb($database); if ($conn['code'] == 0) { return $this->error($conn['msg']); } return $this->success('', [ 'databases' => $conn['databases'] ]); } /** * 系统基础配置 * post请求=开始安装 */ public function baseConfig(Request $request): Response { $this->setRequest($request); if ($this->isInstallComplete()) { return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %file% file first', ['%file%' => 'public/' . self::$lockFileName])); } $envOk = $this->commandExecutionCheck(); $rootPath = str_replace('\\', '/', root_path()); $migrateCommand = 'php vendor/bin/phinx migrate'; if ($request->isGet()) { return $this->success('', [ 'rootPath' => $rootPath, 'executionWebCommand' => $envOk, 'migrateCommand' => $migrateCommand, ]); } $connectData = $databaseParam = $request->only(['hostname', 'username', 'password', 'hostport', 'database', 'prefix']); // 数据库配置测试 $connectData['database'] = ''; $connect = $this->connectDb($connectData, true); if ($connect['code'] == 0) { return $this->error($connect['msg']); } // 建立数据库 if (!in_array($databaseParam['database'], $connect['databases'])) { $sql = "CREATE DATABASE IF NOT EXISTS `{$databaseParam['database']}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"; $connect['pdo']->exec($sql); } // 写入数据库配置文件(thinkorm.php 使用 $env('database.xxx', 'default') 格式) $dbConfigFile = config_path(self::$dbConfigFileName); $dbConfigContent = @file_get_contents($dbConfigFile); if ($dbConfigContent === false || $dbConfigContent === '') { return $this->error(__('File has no write permission:%s', ['config/' . self::$dbConfigFileName])); } $callback = function ($matches) use ($databaseParam) { $key = $matches[1]; $value = (string) ($databaseParam[$key] ?? ''); return "\$env('database.{$key}', '" . addslashes($value) . "')"; }; $dbConfigText = preg_replace_callback("/\\\$env\('database\.(hostname|database|username|password|hostport|prefix)',\s*'[^']*'\)/", $callback, $dbConfigContent); $result = @file_put_contents($dbConfigFile, $dbConfigText); if (!$result) { return $this->error(__('File has no write permission:%s', ['config/' . self::$dbConfigFileName])); } // 写入 .env 和 .env-example(仅使用 Dotenv 可解析的 DATABASE_XXX 格式,避免 [DATABASE] 导致解析失败) $databaseBlock = "\n# Database\n" . 'DATABASE_TYPE = mysql' . "\n" . 'DATABASE_HOSTNAME = ' . $databaseParam['hostname'] . "\n" . 'DATABASE_DATABASE = ' . $databaseParam['database'] . "\n" . 'DATABASE_USERNAME = ' . $databaseParam['username'] . "\n" . 'DATABASE_PASSWORD = ' . $databaseParam['password'] . "\n" . 'DATABASE_HOSTPORT = ' . $databaseParam['hostport'] . "\n" . 'DATABASE_CHARSET = utf8mb4' . "\n" . 'DATABASE_PREFIX = ' . ($databaseParam['prefix'] ?? '') . "\n"; foreach (['.env', '.env-example'] as $envName) { $envFile = root_path() . $envName; $envFileContent = is_file($envFile) ? @file_get_contents($envFile) : ''; if ($envFileContent !== false) { $cutPos = strlen($envFileContent); foreach (['[DATABASE]', "\n# Database\n", "\n# 数据库", "\nDATABASE_DRIVER", "\nDATABASE_TYPE"] as $marker) { $pos = stripos($envFileContent, $marker); if ($pos !== false && $pos < $cutPos) { $cutPos = $pos; } } $envFileContent = rtrim(substr($envFileContent, 0, $cutPos)) . $databaseBlock; $result = @file_put_contents($envFile, $envFileContent); if (!$result && is_file($envFile)) { return $this->error(__('File has no write permission:%s', ['%s' => $envName])); } } } // 设置新的Token随机密钥key $oldTokenKey = config('buildadmin.token.key'); $newTokenKey = Random::build('alnum', 32); $buildConfigFile = config_path(self::$buildConfigFileName); $buildConfigContent = @file_get_contents($buildConfigFile); if ($buildConfigContent === false || $buildConfigContent === '') { return $this->error(__('File has no write permission:%s', ['config/' . self::$buildConfigFileName])); } $buildConfigContent = preg_replace("/'key'(\s+)=>(\s+)'$oldTokenKey'/", "'key'\$1=>\$2'$newTokenKey'", $buildConfigContent); $result = @file_put_contents($buildConfigFile, $buildConfigContent); if (!$result) { return $this->error(__('File has no write permission:%s', ['config/' . self::$buildConfigFileName])); } // 建立安装锁文件 $result = @file_put_contents(public_path(self::$lockFileName), date('Y-m-d H:i:s')); if (!$result) { return $this->error(__('File has no write permission:%s', ['public/' . self::$lockFileName])); } // 自动执行数据库迁移(无需手动运行 phinx 命令) $migrateResult = $this->runPhinxMigrate($databaseParam); if ($migrateResult !== true) { return $this->error($migrateResult); } return $this->success('', [ 'rootPath' => $rootPath, 'executionWebCommand' => $envOk, 'migrateCommand' => $migrateCommand, 'migrationCompleted' => true, ]); } /** * 程序化执行 Phinx 数据库迁移 * @param array $databaseParam 数据库连接参数 * @return true|string 成功返回 true,失败返回错误信息 */ private function runPhinxMigrate(array $databaseParam): true|string { try { $baseDir = root_path(); $phinxConfigPath = $baseDir . 'phinx.php'; if (!is_file($phinxConfigPath)) { return __('Failed to install SQL execution:%msg%', ['%msg%' => 'phinx.php not found']); } // 临时设置环境变量,供 phinx 读取数据库配置 $_ENV['DATABASE_HOSTNAME'] = $databaseParam['hostname'] ?? '127.0.0.1'; $_ENV['DATABASE_DATABASE'] = $databaseParam['database'] ?? ''; $_ENV['DATABASE_USERNAME'] = $databaseParam['username'] ?? 'root'; $_ENV['DATABASE_PASSWORD'] = $databaseParam['password'] ?? ''; $_ENV['DATABASE_HOSTPORT'] = $databaseParam['hostport'] ?? '3306'; $_ENV['DATABASE_PREFIX'] = $databaseParam['prefix'] ?? ''; putenv('DATABASE_HOSTNAME=' . $_ENV['DATABASE_HOSTNAME']); putenv('DATABASE_DATABASE=' . $_ENV['DATABASE_DATABASE']); putenv('DATABASE_USERNAME=' . $_ENV['DATABASE_USERNAME']); putenv('DATABASE_PASSWORD=' . $_ENV['DATABASE_PASSWORD']); putenv('DATABASE_HOSTPORT=' . $_ENV['DATABASE_HOSTPORT']); putenv('DATABASE_PREFIX=' . $_ENV['DATABASE_PREFIX']); $config = PhinxConfig::fromPhp($phinxConfigPath); $input = new ArrayInput([]); $output = new NullOutput(); $manager = new PhinxManager($config, $input, $output); $environment = $config->getDefaultEnvironment(); $manager->migrate($environment); return true; } catch (Throwable $e) { $msg = $e->getMessage(); if ($e->getPrevious()) { $msg .= ' | ' . $e->getPrevious()->getMessage(); } return __('Failed to install SQL execution:%msg%', ['%msg%' => $msg]); } } protected function isInstallComplete(): bool { if (is_file(public_path(self::$lockFileName))) { $contents = @file_get_contents(public_path(self::$lockFileName)); if ($contents == self::$InstallationCompletionMark) { return true; } } return false; } /** * 标记命令执行完毕 * @throws Throwable */ public function commandExecComplete(Request $request): Response { $this->setRequest($request); if ($this->isInstallComplete()) { return $this->error(__('The system has completed installation. If you need to reinstall, please delete the %file% file first', ['%file%' => 'public/' . self::$lockFileName])); } $param = $request->only(['type', 'adminname', 'adminpassword', 'sitename']); if ($param['type'] == 'web') { $result = @file_put_contents(public_path(self::$lockFileName), self::$InstallationCompletionMark); if (!$result) { return $this->error(__('File has no write permission:%s', ['public/' . self::$lockFileName])); } } else { // 管理员配置入库 $adminModel = new AdminModel(); $defaultAdmin = $adminModel->where('username', 'admin')->find(); $defaultAdmin->username = $param['adminname']; $defaultAdmin->nickname = ucfirst($param['adminname']); $defaultAdmin->save(); if (isset($param['adminpassword']) && $param['adminpassword']) { $adminModel->resetPassword($defaultAdmin->id, $param['adminpassword']); } // 默认用户密码修改 $user = new UserModel(); $user->resetPassword(1, Random::build()); // 修改站点名称 if (class_exists(\app\admin\model\Config::class)) { \app\admin\model\Config::where('name', 'site_name')->update([ 'value' => $param['sitename'] ]); } } return $this->success(); } /** * 获取命令执行检查的结果 * @return bool 是否拥有执行命令的条件 */ private function commandExecutionCheck(): bool { $pm = config('terminal.npm_package_manager'); if ($pm == 'none') { return false; } $check['phpPopen'] = function_exists('proc_open') && function_exists('proc_close'); $check['npmVersionCompare'] = Version::compare(self::$needDependentVersion['npm'], Version::getVersion('npm')); $check['pmVersionCompare'] = Version::compare(self::$needDependentVersion[$pm], Version::getVersion($pm)); $check['nodejsVersionCompare'] = Version::compare(self::$needDependentVersion['node'], Version::getVersion('node')); $envOk = true; foreach ($check as $value) { if (!$value) { $envOk = false; break; } } return $envOk; } /** * 获取安装完成后的访问地址(根据请求来源区分 API 与前端开发模式) * - 通过 API 访问(8787):index.html#/admin、index.html#/ * - 通过前端开发服务访问(1818):/#/admin、/#/ */ public function accessUrls(Request $request): Response { $this->setRequest($request); $host = $request->header('host', '127.0.0.1:8787'); $port = '8787'; if (str_contains($host, ':')) { $port = substr($host, strrpos($host, ':') + 1); } $scheme = $request->header('x-forwarded-proto', 'http'); $base = rtrim($scheme . '://' . $host, '/'); if ($port === '1818') { $adminUrl = $base . '/#/admin'; $frontUrl = $base . '/#/'; } else { $adminUrl = $base . '/index.html#/admin'; $frontUrl = $base . '/index.html#/'; } return $this->success('', [ 'adminUrl' => $adminUrl, 'frontUrl' => $frontUrl, ]); } /** * 安装指引 */ public function manualInstall(Request $request): Response { $this->setRequest($request); return $this->success('', [ 'webPath' => str_replace('\\', '/', root_path() . 'web') ]); } public function mvDist(Request $request): Response { $this->setRequest($request); if (!is_file(root_path() . self::$distDir . DIRECTORY_SEPARATOR . 'index.html')) { return $this->error(__('No built front-end file found, please rebuild manually!')); } if (Terminal::mvDist()) { return $this->success(); } return $this->error(__('Failed to move the front-end file, please move it manually!')); } /** * 目录是否可写 * @param $writable * @return string */ private static function writableStateDescribe($writable): string { return $writable ? __('Writable') : __('No write permission'); } /** * 数据库连接-获取数据表列表(使用 raw PDO) * @param array $database hostname, hostport, username, password, database * @param bool $returnPdo * @return array */ private function connectDb(array $database, bool $returnPdo = false): array { $host = $database['hostname'] ?? '127.0.0.1'; $port = $database['hostport'] ?? '3306'; $user = $database['username'] ?? ''; $pass = $database['password'] ?? ''; $db = $database['database'] ?? ''; $dsn = "mysql:host={$host};port={$port};charset=utf8mb4"; if ($db) { $dsn .= ";dbname={$db}"; } try { $pdo = new \PDO($dsn, $user, $pass, [ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true, ]); $pdo->query("SELECT 1")->fetchAll(\PDO::FETCH_ASSOC); } catch (\PDOException $e) { $errorMsg = mb_convert_encoding($e->getMessage() ?: 'unknown', 'UTF-8', 'UTF-8,GBK,GB2312,BIG5'); $template = __('Database connection failed:%s'); return [ 'code' => 0, 'msg' => strpos($template, '%s') !== false ? sprintf($template, $errorMsg) : $template . $errorMsg, ]; } $databases = []; $databasesExclude = ['information_schema', 'mysql', 'performance_schema', 'sys']; $stmt = $pdo->query("SHOW DATABASES"); $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC); $stmt->closeCursor(); foreach ($rows as $row) { $dbName = $row['Database'] ?? $row['database'] ?? ''; if ($dbName && !in_array($dbName, $databasesExclude)) { $databases[] = $dbName; } } return [ 'code' => 1, 'msg' => '', 'databases' => $databases, 'pdo' => $returnPdo ? $pdo : null, ]; } }