Files

988 lines
45 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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);
}
}
}
}