1, 'name' => 'XunRui', 'version' => 'dev', ]; } define('DR_CMS', $app['id']); define('DR_NAME', $app['name']); define('DR_VERSION', $app['version']); define('SITE_NAME', $app['name']); define('CMF_UPDATE_TIME', SYS_TIME); \Phpcmf\Service::V()->init('pc'); \Phpcmf\Service::V()->admin(); } public function index() { $this->lock = WRITEPATH.'install.lock'; if (!IS_XRDEV && is_file($this->lock)) { exit(dr_lang('安装程序已经被锁定,重新安装请删除%s', 'cache/install.lock')); } $data = []; $step = intval($_GET['step']); if (!$step && SITE_LANGUAGE != 'zh-cn') { $step = 1; } $error = ''; $admin = 'admin'; switch ($step) { case 0: $gsname = '四川迅睿云软件开发有限公司'; if (is_file(MYPATH.'Config/License.php')) { $ls = require MYPATH.'Config/License.php'; if (isset($ls['name']) && $ls['name']) { $gsname = $ls['name']; } } \Phpcmf\Service::V()->assign([ 'gsname' => $gsname, ]); break; case 1: $error = 0; // 目录权限检查 $dir = [ WRITEPATH, WRITEPATH.'data/', WRITEPATH.'file/', WRITEPATH.'config/', WRITEPATH.'session/', WRITEPATH.'template/', WRITEPATH.'database/', CONFIGPATH, WEBPATH.'uploadfile/', ]; $path = []; foreach ($dir as $t) { $path[$t] = dr_check_put_path($t); if (!$path[$t]) { $error = 1; } } $php = [ [ 'name' => dr_lang('PHP扩展%s', 'mbstring'), 'code' => function_exists('mb_substr'), 'help' => 'http://www.xunruicms.com/doc/742.html', ], [ 'name' => dr_lang('PHP扩展%s', 'curl'), 'code' => function_exists('curl_init'), 'help' => 'http://www.xunruicms.com/doc/743.html', ], ]; $db_ext = [ [ 'name' => dr_lang('PHP扩展%s', 'mysqli').'(MySQLi)', 'code' => function_exists('mysqli_init'), 'help' => 'http://www.xunruicms.com/doc/741.html', ], [ 'name' => dr_lang('PHP扩展%s', 'sqlite3').'(SQLite3)', 'code' => extension_loaded('sqlite3'), 'help' => '', ], ]; // 数据库扩展:mysqli / sqlite3 至少启用一个即可 $has_db_ext = function_exists('mysqli_init') || extension_loaded('sqlite3'); if (!$has_db_ext) { $error = 1; } \Phpcmf\Service::V()->assign([ 'php' => $php, 'db_ext' => $db_ext, 'has_db_ext' => $has_db_ext, 'path' => $path, 'error' => $error, ]); break; case 2: // 安装信息填写 $is_demo = is_file(MYPATH.'Config/demo.sql') && !is_file(MYPATH.'Config/Install.sql'); if (IS_AJAX_POST) { $data = $_POST['data']; if (empty($data['name'])) { $this->_json(0, dr_lang('项目名称不能为空')); } elseif (empty($data['username'])) { $this->_json(0, dr_lang('创始人账号不能为空')); } elseif (empty($data['password'])) { $this->_json(0, dr_lang('创始人密码不能为空')); } elseif (empty($data['email'])) { $this->_json(0, dr_lang('创始人邮箱不能为空')); } // 数据库驱动:扫描 System/Database 下可用驱动(目录不存在则默认 MySQLi) $drivers = $this->_scan_database_drivers($this->_my_install_sql_requires_mysql()); $rawDriver = isset($data['db_driver']) ? trim((string) $data['db_driver']) : ''; $selected = null; foreach ($drivers as $d) { if (strcasecmp($d['name'], $rawDriver) === 0) { $selected = $d; break; } } if (!$selected) { // 默认优先 MySQLi foreach ($drivers as $d) { if ($d['enabled']) { $selected = $d; if ($d['name'] === 'MySQLi') { break; } } } } if (!$selected || !$selected['enabled']) { $this->_json(0, dr_lang('不支持的数据库驱动:%s', $rawDriver !== '' ? $rawDriver : '-')); } $data['db_driver'] = $selected['name']; // 自定义包 Install.sql 含 SQL 时仅允许 MySQLi if ($this->_my_install_sql_requires_mysql() && $data['db_driver'] !== 'MySQLi') { $this->_json(0, dr_lang('当前安装包存在自定义 Install.sql,仅支持 MySQLi 安装')); } if (empty($data['db_prefix'])) { $this->_json(0, dr_lang('数据表前缀不能为空')); } if ($data['db_driver'] === 'SQLite3') { // 路径与随机文件名由 _install_db_bootstrap 生成 $data['db_host'] = isset($data['db_host']) ? $data['db_host'] : ''; $data['db_user'] = isset($data['db_user']) ? $data['db_user'] : ''; $data['db_pass'] = isset($data['db_pass']) ? $data['db_pass'] : ''; } else { if (empty($data['db_host'])) { $this->_json(0, dr_lang('数据库地址不能为空')); } elseif (empty($data['db_user'])) { $this->_json(0, dr_lang('数据库账号不能为空')); } elseif (empty($data['db_name'])) { $this->_json(0, dr_lang('数据库名称不能为空')); } elseif (!isset($data['db_pass'])) { $this->_json(0, dr_lang('数据库密码不能为空')); } // 允许空密码(部分本地环境) if (is_numeric(substr($data['db_name'], 0, 1))) { $this->_json(0, dr_lang('数据库名称(%s)不能是数字开头', $data['db_name'])); } elseif (strpos($data['db_name'], '.') !== false) { $this->_json(0, dr_lang('数据库名(%s)不能存在.号', $data['db_name'])); } $data['db_name'] = dr_safe_filename($data['db_name']); } $rt = $this->_install_db_bootstrap($data); if (!$rt['code']) { $this->_json(0, $rt['msg']); } // SQLite 可能规范化了相对路径(如补全 database/ 前缀) if (!empty($rt['data']) && is_array($rt['data'])) { $data = array_merge($data, $rt['data']); } $data['db_prefix'] = strtolower($data['db_prefix']); // 存储缓存文件中 $size = file_put_contents(WRITEPATH.'install.info', dr_array2string($data)); if (!$size || $size < 10) { $this->_json(0, dr_lang('临时数据存储失败,cahce目录无法写入')); } // 写入数据库配置(显式 DBDriver,供驱动层 DDL/方言转换使用) $rt = $this->_write_database_config($data); if (!$rt['code']) { $this->_json(0, $rt['msg']); } $this->_json(1, 'index.php?c=install&m=index&is_install_db='.(!$is_demo ? 0 : intval($_POST['is_install_db'])).'&step='.($step+1)); } $force_mysqli = $this->_my_install_sql_requires_mysql(); $db_drivers = $this->_scan_database_drivers($force_mysqli); $default_driver = 'MySQLi'; $db_dir_missing = !is_dir(FRAMEPATH.'Database/'); foreach ($db_drivers as $d) { if (!empty($d['enabled'])) { $default_driver = $d['name']; if ($d['name'] === 'MySQLi') { break; } } } $data = [ 'do_url' => '/index.php?c=install&m=index&is_install_db='.(!$is_demo ? 0 : intval($_POST['is_install_db'])).'&step=2', 'is_demo' => $is_demo, 'db_driver' => $default_driver, 'db_drivers' => $db_drivers, 'db_dir_missing' => $db_dir_missing, 'db_host' => defined('INSTALL_DB_HOST') ? INSTALL_DB_HOST : '127.0.0.1', 'db_user' => defined('INSTALL_DB_USER') ? INSTALL_DB_USER : '', 'db_pass' => defined('INSTALL_DB_PASS') ? INSTALL_DB_PASS : '', 'db_name' => defined('INSTALL_DB_NAME') ? INSTALL_DB_NAME : '', 'db_prefix' => defined('INSTALL_DB_PREFIX') ? INSTALL_DB_PREFIX : 'dr_', 'sqlite_name' => 'database/SQLite3/'.dr_lang('安装时随机生成'), 'force_mysqli' => $force_mysqli, ]; \Phpcmf\Service::V()->assign($data); break; case 3: $page = intval($_GET['page']); $data = dr_string2array(file_get_contents(WRITEPATH.'install.info')); $error = ''; file_put_contents(WRITEPATH.'install.error', ''); if (empty($data)) { $error = dr_lang('临时数据获取失败,请返回前一页重新执行'); } else { $this->db = \Phpcmf\Service::M()->db; // 检查数据库是否存在 if (!$this->db->connect(false)) { // 链接失败,尝试创建数据库 $error = dr_lang('数据库连接失败,请返回前一页重新执行'); } elseif (!isset($_GET['doin']) && $this->db->tableExists($data['db_prefix'].'cron')) { // 判断是否安装过 if (isset($data['db_driver']) && $data['db_driver'] === 'SQLite3') { $error = dr_lang('指定的数据库文件(%s)已经安装过表前缀(%s),你可以尝试修改数据库文件名或者数据表前缀', $data['db_name'], $data['db_prefix']); } else { $error = dr_lang('指定的数据库(%s)已经安装过表前缀(%s),你可以尝试修改数据库名或者数据表前缀', $data['db_name'], $data['db_prefix']); } } else { // 导入数据结构 if ($page) { // 优先规范化 Schema(Config/Install.php);自定义包仍可用 MYPATH Install.sql $schema_file = ''; if (!is_file(MYPATH.'Config/Install.sql') && is_file(CMSPATH.'Config/Install.php')) { $schema_file = CMSPATH.'Config/Install.php'; } if ($schema_file) { if ((int)$page === 1) { $schema = require $schema_file; if (!is_array($schema) || empty($schema['tables'])) { $this->_json(0, dr_lang('安装 Schema 无效:%s', $schema_file)); } $rt = \Phpcmf\Service::M('table')->install_schema($schema); if (!$rt['code']) { $error = '**************************************************************************' .PHP_EOL.$rt['msg'].PHP_EOL .'**************************************************************************'.PHP_EOL; file_put_contents(WRITEPATH.'install.error', $error.PHP_EOL, FILE_APPEND); $this->_json(0, $rt['msg']); } $this->_json(1, dr_lang('表结构安装完成(%s张表)', intval($rt['data']['tables'] ?? 0)), ['page' => 99]); } $this->_json(1, dr_lang('执行完成,即将安装数据信息...'), ['page' => 99]); } if (is_file(MYPATH.'Config/Install.sql')) { $sql = file_get_contents(MYPATH.'Config/Install.sql'); if (strlen($sql) < 100) { $sql = file_get_contents(CMSPATH.'Config/Install.sql'); } } else { $sql = file_get_contents(CMSPATH.'Config/Install.sql'); } $sql = str_replace('{dbprefix}', $data['db_prefix'], $sql); // 每批条数过少会导致超大 Install.sql 需要数百次 AJAX,易中途超时;可按环境酌情调大 $rows = $this->query_rows($sql, 40); $key = $page - 1; if (isset($rows[$key]) && $rows[$key]) { // 安装本次结构 $last_sql = ''; foreach ($rows[$key] as $statement) { if (!$statement) { continue; } $statement = trim($statement); if ($statement === '') { continue; } $last_sql = $statement; if (!$this->db->simpleQuery($statement)) { $rt = $this->db->error(); $error = '**************************************************************************' .PHP_EOL.$statement.PHP_EOL.$rt['message'].PHP_EOL; $error.= '**************************************************************************'.PHP_EOL; file_put_contents(WRITEPATH.'install.error', $error.PHP_EOL, FILE_APPEND); $this->_json(0, 'SQL执行错误:'.$rt['message']); } } $this->_json(1, dr_lang('正在执行:%s', dr_strcut($last_sql, 70)), ['page' => $page + 1]); } else { $this->_json(1, dr_lang('执行完成,即将安装数据信息...'), ['page' => 99]); } } } } \Phpcmf\Service::V()->assign([ 'do_url' => 'index.php?c=install&m=index&step=3&doin=1&is_install_db='.intval($_GET['is_install_db']), ]); break; case 4: $data = dr_string2array(file_get_contents(WRITEPATH.'install.info')); if (empty($data)) { $error = dr_lang('临时数据获取失败,请返回前一页重新执行'); } else { $this->db = \Phpcmf\Service::M()->db; // 检查数据库是否存在 if (!$this->db->connect(false)) { // 链接失败,尝试创建数据库 $error = dr_lang('数据库连接失败,请返回前一页重新执行'); } else { // 导入默认安装数据 $errorlog = file_get_contents(WRITEPATH.'install.error'); if ($errorlog && strlen($errorlog) > 10) { // 出现错误了 $error = $errorlog; } else { if (!\Phpcmf\Service::M()->is_table_exists('member')) { if (is_file(MYPATH.'Config/Install.sql')) { $sql = MYPATH.'Config/Install.sql'; } elseif (is_file(CMSPATH.'Config/Install.php')) { $sql = CMSPATH.'Config/Install.php'; } else { $sql = CMSPATH.'Config/Install.sql'; } $error = dr_lang('安装文件:%s', $sql); $error.= '
'.dr_lang('关键数据表member结构不完整,检查安装包结构文件是否合理'); } else { // 创建账号 $pwd = md5(dr_safe_password($data['password'])); $salt = substr(md5(rand(0, 999)), 0, 10); $prefix = \Phpcmf\Service::M()->prefix; if (\Phpcmf\Service::M()->table('member')->get(1)) { \Phpcmf\Service::M()->table('member')->update(1, [ 'email' => $data['email'], 'username' => $data['username'], 'password' => md5($pwd.$salt.$pwd), 'salt' => $salt, 'name' => dr_lang('创始人'), 'phone' => '', 'money' => 1000000, 'freeze' => 0, 'spend' => 0, 'score' => 1000000, 'experience' => 1000000, 'regip' => '', 'regtime' => SYS_TIME, 'randcode' => 0, ]); $id = 1; } else { $this->db->table('member')->insert([ 'email' => $data['email'], 'username' => $data['username'], 'password' => md5($pwd.$salt.$pwd), 'salt' => $salt, 'name' => dr_lang('创始人'), 'phone' => '', 'money' => 1000000, 'freeze' => 0, 'spend' => 0, 'score' => 1000000, 'experience' => 1000000, 'regip' => '', 'regtime' => SYS_TIME, 'randcode' => 0, ]); $id = $this->db->insertID(); $this->db->table('member_data')->insert([ 'id' => $id, 'is_lock' => 0, 'is_admin' => 1, 'is_verify' => 1, 'is_mobile' => 1, 'is_complete' => 1, ]); } // 加入管理员表 if (!\Phpcmf\Service::M()->table('admin')->where('uid', $id)->counts()) { $this->db->table('admin')->insert([ 'uid' => $id, 'setting' => '', 'usermenu' => '', ]); // 加入角色表 $this->db->table('admin_role_index')->insert([ 'uid' => $id, 'roleid' => 1, ]); } // 创建站点 if (\Phpcmf\Service::M()->table('site')->get(1)) { \Phpcmf\Service::M()->table('site')->update(1, [ 'name' => $data['name'], 'domain' => DOMAIN_NAME, ]); } else { $this->db->table('site')->replace([ 'id' => 1, 'name' => $data['name'], 'domain' => DOMAIN_NAME, 'setting' => '', 'disabled' => 0, 'displayorder' => 0, ]); } \Phpcmf\Service::M()->site = $this->site = [ 1 => 1 ]; $ssl = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') || (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == '443') ? 1 : 0;; if (isset($_GET['protocol']) && trim($_GET['protocol'], ':') == 'https') { $ssl = 1; } // 写配置文件 $sys = [ 'SYS_DEBUG' => '1', 'SYS_ADMIN_CODE' => '0', 'SYS_ADMIN_LOG' => '0', 'SYS_AUTO_FORM' => '0', 'SYS_ADMIN_PAGESIZE' => '10', 'SYS_SMS_IMG_CODE' => '0', 'SYS_NOT_ADMIN_CACHE' => '0', 'SYS_URL_ONLY' => '0', 'SYS_THEME_ROOT_PATH' => '1', 'SYS_URL_REL' => '1', 'SYS_CRON_AUTH' => '0', 'SYS_CSRF' => '0', 'SYS_CSRF_TIME' => '0', 'SYS_301' => '1', 'SYS_NOT_UPDATE' => '1', 'SYS_KEY' => 'PHPCMF'.md5($data['name'].rand(1, 999999)), //安全密匙 'SYS_HTTPS' => $ssl, 'SYS_ATTACHMENT_DB' => '', 'SYS_ATTACHMENT_PATH' => '', 'SYS_ATTACHMENT_URL' => '', 'SYS_API_TOKEN' => '', ]; if (is_file(MYPATH.'Config/License.php')) { $ls = require MYPATH.'Config/License.php'; if (isset($ls['oem']) && $ls['oem']) { $sys['SYS_DEBUG'] = 0; } } \Phpcmf\Service::M('System')->save_config($sys, $sys); // 删除app的install.lock $local = \Phpcmf\Service::Apps(); foreach ($local as $dir => $path) { if (is_file($path.'install.lock')) { unlink($path.'install.lock'); } } // 执行安装程序废除 /* $sql = ''; if (is_file(MYPATH.'Config/Install.sql')) { $sql = file_get_contents(MYPATH.'Config/Install.sql'); $sql = str_replace('{dbprefix}', $data['db_prefix'], $sql); } if (is_file(MYPATH.'Config/Install_site.sql')) { $s = file_get_contents(MYPATH.'Config/Install_site.sql'); $sql.= PHP_EOL.str_replace('{dbprefix}', $data['db_prefix'].'1_', $s); } $this->query($sql);*/ // 运行自定义安装脚本 if (is_file(MYPATH.'Config/Install.php')) { require MYPATH.'Config/Install.php'; } // 运行自定义安装脚本② if (is_file(MYPATH.'Config/Install_tpl.php')) { require MYPATH.'Config/Install_tpl.php'; } $errorlog = file_get_contents(WRITEPATH.'install.error'); if ($errorlog && strlen($errorlog) > 10) { // 出现错误了 $error = $errorlog; } else { // 安装完成 file_put_contents($this->lock, time()); file_put_contents(WRITEPATH.'update.txt', time()); file_put_contents(WRITEPATH.'install.test', time()); unlink(WRITEPATH.'install.info'); unlink(WRITEPATH.'install.error'); // 重命名后台入口 $admin = 'admin'.substr(md5(SYS_TIME.rand(1, 999999)), 0, 12); $afile = WEBPATH.$admin.'.php'; if (is_file(WEBPATH.'admin.php')) { copy(WEBPATH.'admin.php', $afile); } if (!is_file($afile)) { file_put_contents($afile, "assign([ 'data' => $data, 'step' => $step, 'error' => $error, 'admin' => $admin, 'pre_url' => 'index.php?c=install&m=index&step='.($step-1).'&is_install_db='.intval($_GET['is_install_db']), 'next_url' => 'index.php?c=install&m=index&step='.($step+1).'&is_install_db='.intval($_GET['is_install_db']), ]); \Phpcmf\Service::V()->display('install/'.$step.'.html'); exit; } /** * 扫描 FRAMEPATH/Database 目录,列出可识别的数据库驱动 * * @param bool $forceMysqli 自定义 Install.sql 时仅启用 MySQLi * @return array [['name','label','ext_ok','installable','enabled','form','tip'], ...] */ private function _scan_database_drivers($forceMysqli = false) { $base = FRAMEPATH.'Database/'; $list = []; // 无 System/Database 目录时,默认仅 MySQL(MySQLi)模式 if (!is_dir($base)) { $extOk = function_exists('mysqli_init'); return [[ 'name' => 'MySQLi', 'label' => 'MySQLi', 'ext_ok' => $extOk, 'installable' => true, 'enabled' => $extOk, 'form' => 'server', 'tip' => $extOk ? '' : dr_lang('PHP 未启用对应扩展'), ]]; } // PHP 扩展检测 $extMap = [ 'MySQLi' => function_exists('mysqli_init'), 'SQLite3' => extension_loaded('sqlite3'), 'Postgre' => extension_loaded('pgsql'), 'SQLSRV' => extension_loaded('sqlsrv'), 'OCI8' => extension_loaded('oci8'), ]; // 表单类型:sqlite 用文件表单,其余用服务器表单 $formMap = [ 'SQLite3' => 'sqlite', ]; // 安装引导已实现的驱动(其余目录有驱动文件但安装流程未接好) $bootstrapOk = ['MySQLi' => true, 'SQLite3' => true]; $dirs = @scandir($base); if (!is_array($dirs) || $dirs === []) { $extOk = function_exists('mysqli_init'); return [[ 'name' => 'MySQLi', 'label' => 'MySQLi', 'ext_ok' => $extOk, 'installable' => true, 'enabled' => $extOk, 'form' => 'server', 'tip' => $extOk ? '' : dr_lang('PHP 未启用对应扩展'), ]]; } foreach ($dirs as $name) { if ($name === '.' || $name === '..' || !is_dir($base.$name)) { continue; } $conn = $base.$name.'/Connection.php'; if (!is_file($conn)) { continue; } // 迅睿安装要求驱动实现 formatCreateSql;未实现的不展示在安装界面 $code = @file_get_contents($conn); $hasDdl = is_string($code) && strpos($code, 'function formatCreateSql') !== false; if (!$hasDdl) { continue; } $extOk = isset($extMap[$name]) ? (bool) $extMap[$name] : true; $installable = !empty($bootstrapOk[$name]); $enabled = $installable && $extOk; if ($forceMysqli && $name !== 'MySQLi') { $enabled = false; } $tip = ''; if (empty($bootstrapOk[$name])) { $tip = dr_lang('安装引导暂未支持该驱动'); } elseif (!$extOk) { $tip = dr_lang('PHP 未启用对应扩展'); } elseif ($forceMysqli && $name !== 'MySQLi') { $tip = dr_lang('自定义 Install.sql 仅支持 MySQLi'); } $list[] = [ 'name' => $name, 'label' => $name, 'ext_ok' => $extOk, 'installable' => $installable, 'enabled' => $enabled, 'form' => isset($formMap[$name]) ? $formMap[$name] : 'server', 'tip' => $tip, ]; } // 目录存在但未扫到任何驱动时,同样回退 MySQLi if (!$list) { $extOk = function_exists('mysqli_init'); return [[ 'name' => 'MySQLi', 'label' => 'MySQLi', 'ext_ok' => $extOk, 'installable' => true, 'enabled' => $extOk, 'form' => 'server', 'tip' => $extOk ? '' : dr_lang('PHP 未启用对应扩展'), ]]; } // MySQLi / SQLite3 排前面,其余按名称 usort($list, static function ($a, $b) { $order = ['MySQLi' => 0, 'SQLite3' => 1]; $oa = isset($order[$a['name']]) ? $order[$a['name']] : 50; $ob = isset($order[$b['name']]) ? $order[$b['name']] : 50; if ($oa === $ob) { return strcmp($a['name'], $b['name']); } return $oa - $ob; }); return $list; } /** * 是否存在自定义 My/Config/Install.sql 且含有效 SQL(此时禁止选 SQLite3) */ private function _my_install_sql_requires_mysql() { $file = MYPATH.'Config/Install.sql'; if (!is_file($file)) { return false; } $sql = @file_get_contents($file); if ($sql === false || trim($sql) === '') { return false; } // 去掉注释与空白后判断是否还有语句 $lines = preg_split("/\r\n|\n|\r/", $sql); $buf = ''; if (is_array($lines)) { foreach ($lines as $line) { $trim = ltrim($line); if ($trim === '' || strpos($trim, '--') === 0 || strpos($trim, '#') === 0) { continue; } $buf .= $trim; } } $buf = trim(preg_replace('/\/\*.*?\*\//s', '', $buf) ?? ''); if ($buf === '') { return false; } // 至少像一条 SQL(含关键字或分号) return (bool) preg_match('/\b(CREATE|INSERT|REPLACE|ALTER|DROP|UPDATE|DELETE|SET)\b/i', $buf); } /** * 生成 SQLite3 库文件路径:cache/database/SQLite3/{随机}.db * * @return array{rel:string,abs:string,name:string} */ private function _sqlite3_make_db_file() { try { $name = bin2hex(random_bytes(16)).'.db'; } catch (\Throwable $e) { $name = md5(uniqid((string) mt_rand(), true).microtime(true)).'.db'; } $rel = 'database'.DIRECTORY_SEPARATOR.'SQLite3'.DIRECTORY_SEPARATOR.$name; // 配置与展示统一用正斜杠相对路径(WRITEPATH 拼接时再转系统分隔符) $rel_slash = 'database/SQLite3/'.$name; return [ 'name' => $name, 'rel' => $rel_slash, 'abs' => WRITEPATH.str_replace('/', DIRECTORY_SEPARATOR, $rel_slash), ]; } /** * 安装阶段数据库引导连接。 * 支持 MySQLi / SQLite3;配置写出后再由 CI 驱动执行 Schema/Install.sql。 * * @param array $data * @return array */ private function _install_db_bootstrap(array $data) { $driver = isset($data['db_driver']) ? $data['db_driver'] : 'MySQLi'; if ($driver === 'SQLite3') { if (!extension_loaded('sqlite3') || !class_exists('SQLite3', false)) { return dr_return_data(0, dr_lang('PHP环境必须启用SQLite3扩展')); } // cache/database/SQLite3/{随机名}.db,避免固定路径被猜测 $info = $this->_sqlite3_make_db_file(); $data['db_name'] = $info['rel']; $file = $info['abs']; $dir = dirname($file); if (!is_dir($dir) && !@mkdir($dir, 0755, true)) { return dr_return_data(0, dr_lang('无法创建数据库目录:%s', $dir)); } if (!is_writable($dir)) { return dr_return_data(0, dr_lang('数据库目录不可写:%s', $dir)); } // 禁止目录浏览 if (!is_file($dir.DIRECTORY_SEPARATOR.'index.html')) { @file_put_contents($dir.DIRECTORY_SEPARATOR.'index.html', ''); } try { $flags = SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE; $pass = isset($data['db_pass']) ? (string) $data['db_pass'] : ''; $db = ($pass !== '') ? new \SQLite3($file, $flags, $pass) : new \SQLite3($file, $flags); $db->close(); } catch (\Throwable $e) { return dr_return_data(0, dr_lang('无法创建或打开SQLite数据库:%s', $e->getMessage())); } return dr_return_data(1, '', $data); } if ($driver !== 'MySQLi') { return dr_return_data(0, dr_lang('暂不支持的数据库驱动:%s', $driver)); } if (!function_exists('mysqli_init')) { return dr_return_data(0, dr_lang('PHP环境必须启用Mysqli扩展')); } $mysqli = mysqli_init(); if (!$mysqli) { return dr_return_data(0, dr_lang('PHP环境必须启用Mysqli扩展')); } if (!@mysqli_real_connect($mysqli, $data['db_host'], $data['db_user'], $data['db_pass'])) { if (SITE_LANGUAGE == 'zh-cn') { return dr_return_data(0, '['.mysqli_connect_errno().'] - 无法连接到数据库服务器('.$data['db_host'].'),请检查用户名('.$data['db_user'].')和密码('.$data['db_pass'].')是否正确'); } return dr_return_data(0, mysqli_connect_errno().' '.mysqli_connect_error()); } if (!mysqli_select_db($mysqli, $data['db_name'])) { if (!mysqli_query($mysqli, 'CREATE DATABASE `'.$data['db_name'].'` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci')) { return dr_return_data(0, dr_lang('指定的数据库(%s)不存在,系统尝试创建失败,请通过其他方式建立数据库', $data['db_name'])); } if (!mysqli_select_db($mysqli, $data['db_name'])) { return dr_return_data(0, dr_lang('指定的数据库(%s)不存在,系统尝试创建失败,请通过其他方式建立数据库', $data['db_name'])); } } if (!mysqli_set_charset($mysqli, 'utf8mb4')) { return dr_return_data(0, dr_lang('当前MySQL不支持utf8mb4编码(%s)建议升级MySQL版本', mysqli_error($mysqli))); } return dr_return_data(1); } /** * 写出 config/database.php(显式包含 DBDriver) * * @param array $data * @return array */ private function _write_database_config(array $data) { $driver = isset($data['db_driver']) && $data['db_driver'] ? $data['db_driver'] : 'MySQLi'; $prefix = dr_safe_filename($data['db_prefix']); if ($driver === 'SQLite3') { // 相对 WRITEPATH:database/SQLite3/{随机}.db $rel = isset($data['db_name']) ? (string) $data['db_name'] : ''; if ($rel === '' || (strpos($rel, 'SQLite3/') === false && strpos($rel, 'SQLite3'.DIRECTORY_SEPARATOR) === false)) { $info = $this->_sqlite3_make_db_file(); $rel = $info['rel']; $data['db_name'] = $rel; } $rel_fs = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $rel); $rel_php = str_replace('\\', '\\\\', $rel_fs); $pass = str_replace("'", "\\'", (string) (isset($data['db_pass']) ? $data['db_pass'] : '')); $database = ' \'\', \'username\' => \'\', \'password\' => \''.$pass.'\', \'database\' => WRITEPATH.\''.$rel_php.'\', \'DBPrefix\' => \''.$prefix.'\', \'DBDriver\' => \'SQLite3\', \'charset\' => \'utf8\', \'DBCollat\' => \'\', ];'; } else { $database = ' \''.str_replace("'", "\\'", $data['db_host']).'\', \'username\' => \''.str_replace("'", "\\'", $data['db_user']).'\', \'password\' => \''.str_replace("'", "\\'", $data['db_pass']).'\', \'database\' => \''.str_replace("'", "\\'", $data['db_name']).'\', \'DBPrefix\' => \''.$prefix.'\', \'DBDriver\' => \'MySQLi\', \'charset\' => \'utf8mb4\', \'DBCollat\' => \'utf8mb4_general_ci\', ];'; } $size = file_put_contents(CONFIGPATH.'database.php', $database); if (!$size || $size < 10) { return dr_return_data(0, dr_lang('数据库配置文件创建失败,config目录无法写入')); } return dr_return_data(1); } /** * 将安装 SQL 拆成单条语句。 * Install.sql 以 MySQL 方言为源,经 dr_format_create_sql → 驱动 formatCreateSql 转换。 * 先统一为 \n 再占位,避免 Windows 换行在 Linux 下经 PHP_EOL 替换后出现双占位符,导致无法按 ; 拆分、多条语句粘成一条执行失败。 * 拆分后还原真实换行交给数据库解析,避免逐行剔除 #/-- 时误伤多行字符串内的合法内容。 */ private function split_install_sql_statements($sql) { if (!$sql) { return []; } $sql = dr_format_create_sql($sql); $sql = str_replace(["\r\n", "\r"], "\n", $sql); $ph = 'SQL_FINECMS_EOL'; $sql = str_replace("\n", $ph, $sql); $sql_data = explode(';'.$ph, trim($sql)); $rt = []; foreach ($sql_data as $chunk) { $chunk = trim($chunk); if ($chunk === '') { continue; } $rt[] = str_replace($ph, "\n", $chunk); } return $rt; } // 数据分组 private function query_rows($sql, $num = 0) { if (!$sql) { return []; } $rt = $this->split_install_sql_statements($sql); return $num ? array_chunk($rt, $num) : $rt; } // 数据执行 private function query($sql) { if (!$sql) { return ''; } foreach ($this->split_install_sql_statements($sql) as $ret) { if (!$this->db->simpleQuery($ret)) { $rt = $this->db->error(); $error = '**************************************************************************' .PHP_EOL.$ret.PHP_EOL.$rt['message'].PHP_EOL; $error.= '**************************************************************************'.PHP_EOL; file_put_contents(WRITEPATH.'install.error', $error.PHP_EOL, FILE_APPEND); } } } }