Initial project files: BESCMS full source

This commit is contained in:
bes
2026-08-13 23:37:46 +08:00
parent fa6cddb540
commit 2e26f85723
2166 changed files with 396625 additions and 0 deletions
+987
View File
@@ -0,0 +1,987 @@
<?php namespace Phpcmf\Control;
/**
* https://www.besyun.com
* BESCMS
* 本文件是框架系统文件,二次开发时不可以修改本文件
**/
define('IS_INSTALL', 1);
set_time_limit(0);
// 安装程序
class Install extends \Phpcmf\Common {
public $db;
private $lock;
/**
* 初始化共享控制器
*/
public function __construct()
{
parent::__construct();
define('SITE_LANGUAGE', is_file(WRITEPATH.'lang.default') ? file_get_contents(WRITEPATH.'lang.default') : 'zh-cn');
define('SITE_ID', 1);
define('SITE_URL', '/');
define('SITE_MURL', '/');
define('IS_API_HTTP', 0);
define('SITE_TEMPLATE', 'default');
define('THEME_PATH', '/static/');
define('ROOT_THEME_PATH', '/static/');
define('LANG_PATH', '/api/language/'.SITE_LANGUAGE.'/');
if (is_file(MYPATH.'Config/Version.php')) {
$app = require MYPATH.'Config/Version.php';
} else {
$app = [
'id' => 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) {
// 优先规范化 SchemaConfig/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.= '<br>'.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, "<?php
define('IS_ADMIN', TRUE);
define('SELF', pathinfo(__FILE__, PATHINFO_BASENAME));
require('index.php');");
}
if (is_file($afile)) {
unlink(WEBPATH.'admin.php');
} else {
$admin = 'admin';
}
}
}
}
}
}
break;
}
\Phpcmf\Service::V()->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 目录时,默认仅 MySQLMySQLi)模式
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') {
// 相对 WRITEPATHdatabase/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 = '<?php
/**
* 数据库配置文件(SQLite3)
* 库文件目录:cache/database/SQLite3/(文件名安装时随机生成)
*/
$db[\'default\'] = [
\'hostname\' => \'\',
\'username\' => \'\',
\'password\' => \''.$pass.'\',
\'database\' => WRITEPATH.\''.$rel_php.'\',
\'DBPrefix\' => \''.$prefix.'\',
\'DBDriver\' => \'SQLite3\',
\'charset\' => \'utf8\',
\'DBCollat\' => \'\',
];';
} else {
$database = '<?php
/**
* 数据库配置文件
*/
$db[\'default\'] = [
\'hostname\' => \''.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);
}
}
}
}