988 lines
45 KiB
PHP
988 lines
45 KiB
PHP
<?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) {
|
||
// 优先规范化 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.= '<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 目录时,默认仅 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 = '<?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);
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|