refactor: 优化多模块代码,修复安全与体验问题
8 个文件变更
+192
-98
13172193298@163.com
| •api/api.php | +31 -12 |
| •api/docker.php | +4 -2 |
| •mf_modules/servers/mnbtdocker/README.md | +7 -10 |
| •mf_modules/servers/mnbtdocker/mnbtdocker.php | +50 -21 |
| •mf_modules/servers/mnbtdocker/templates/console.html | +15 -9 |
| •mf_modules/servers/mnbthost/README.md | +1 -1 |
| •mf_modules/servers/mnbthost/mnbthost.php | +57 -38 |
| •mf_modules/servers/mnbthost/templates/console.html | +27 -5 |
变更内容
diff --git a/api/api.php b/api/api.php
index 0e07c4c..82092ad 100644
--- a/api/api.php
+++ b/api/api.php
@@ -37,8 +37,9 @@ $et_zj=$DB->get_row_prepare("SELECT * FROM MN_zj WHERE user=? limit 1", [$user])
if($cert=='' || $cert['qk']=='false')api_json_exit(100, '错误!该宝塔不存在或该宝塔已经被关闭');
$adyjm=$cert['ktmy'].$cert['qmk'];$mdjm=md5($adyjm);
if($keye!=$mdjm){
- mnbt_log('外部API','API鉴权','API-'.$bh.' '.$user.' 宝塔调用密钥错误(发送:'.substr($keye,0,8).',期望:'.substr($mdjm,0,8).',btdh='.$bh.')','鉴权失败',$DB);
- api_json_exit(100, '调用密钥不匹配!正确的调用密钥为:'.$mdjm);
+ // 不回显正确密钥,避免密钥泄露
+ mnbt_log('外部API','API鉴权','API-'.$bh.' '.$user.' 宝塔调用密钥错误(发送:'.substr($keye,0,8).',btdh='.$bh.')','鉴权失败',$DB);
+ api_json_exit(100, '调用密钥不匹配!');
}
$btipe=($cert['ptl']=='true'?'https':'http').'://'.$cert['btip'].':'.$cert['btdk'];
$btkeye=$cert['btmy'];
@@ -52,7 +53,8 @@ if($gn=='cfif'){
$webdx=json_encode(array('max'=>daddslashes($_POST['webdx'] ?? 0),'dq'=>0));
$sqldx=json_encode(array('max'=>daddslashes($_POST['sqldx'] ?? 0),'dq'=>0));
$ymbds=daddslashes($_POST['ymbds'] ?? 0);
- if($et_zj!='' || $et_zj!=false){
+ // 直接真值判断:已存在则报错
+ if($et_zj){
api_lifecycle_log('API开通主机','开通'.$user.'失败:主机已存在','开通失败');
api_json_exit(100, '错误!该主机已经存在,请重新开通!');
}
@@ -87,12 +89,11 @@ if($gn=='cfif'){
api_lifecycle_log('API开通主机','开通'.$user.'失败:账号重复','开通失败');
api_json_exit(100, '错误!该账号已存在!请更换账号!');
}
- $rowe=$DB->get_row_prepare("SELECT * FROM MN_zj WHERE 1 order by id desc limit 1");
- $id=$rowe['id']+1;
+ // max(id)+1 并发下可能重复导致插入失败,失败时重新取号重试几次
$hskr=mt_rand(4,10);
$rqsj=md5($date.$user);
$wjler=substr($rqsj, $hskr , 3);
- $btserw='mnbt.'.$id.mt_rand(1,999).$wjler;
+ $btserw='mnbt.'.mt_rand(1,999).$wjler;
$mrwww=$cert['btos']=='1' ? $conf['hxi'].'/'.$btserw : $conf['hxo'].'/'.$btserw;
$r_data = $api->webkt($user,$pass,$btserw,'主机','true','true',$phpVersion,$mrwww);
$cjqk=$r_data['siteStatus'] ?? false;
@@ -111,7 +112,17 @@ if($gn=='cfif'){
$aedfs = '0'; $sqlfs = '0';
foreach(($r_datn['data'] ?? []) as $val){ if($val['name']===$user){ $aedfs=$val['id']; break; } }
foreach(($r_datp['data'] ?? []) as $val){ if($val['name']===$user){ $sqlfs=$val['id']; break; } }
- if($DB->query_prepare("INSERT INTO `MN_zj` (`id`, `ssbt`, `user`, `pass`, `sqluser`, `sqlpass`, `data`, `datae`, `qk`, `btid`, `sqldz`, `ftpid`, `ymbds`, `hxa`, `hxb`, `hxc`, `hxd`, `llmax`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", [$id, $bh, $user, $pass, $user, $pass, $date, $datae, 'true', $zdide, $btserw, $aedfs, $ymbds, $webdx, $sqldx, '2', $sqlfs, $flowratemax])){
+ // 写入主机记录:max(id)+1 在并发下可能重复导致插入失败,失败时重新取号重试几次
+ $insert_ok = false;
+ for($tryi = 0; $tryi < 3; $tryi++){
+ $rowe=$DB->get_row_prepare("SELECT id FROM MN_zj WHERE 1 order by id desc limit 1");
+ $id=($rowe['id'] ?? 0)+1;
+ if($DB->query_prepare("INSERT INTO `MN_zj` (`id`, `ssbt`, `user`, `pass`, `sqluser`, `sqlpass`, `data`, `datae`, `qk`, `btid`, `sqldz`, `ftpid`, `ymbds`, `hxa`, `hxb`, `hxc`, `hxd`, `llmax`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", [$id, $bh, $user, $pass, $user, $pass, $date, $datae, 'true', $zdide, $btserw, $aedfs, $ymbds, $webdx, $sqldx, '2', $sqlfs, $flowratemax])){
+ $insert_ok = true;
+ break;
+ }
+ }
+ if($insert_ok){
api_lifecycle_log('API开通主机','开通'.$user.'成功,站点'.$btserw,'开通成功');
$host_row = $DB->get_row_prepare("SELECT * FROM MN_zj WHERE id=? limit 1", [$id]);
if (function_exists('mnbt_do_action')) {
@@ -128,6 +139,7 @@ if($gn=='cfif'){
api_json_exit(100, '错误!网站创建失败!宝塔返回信息:'.($r_data['msg'] ?? '未知错误'));
}
}elseif($gn=='zt'){
+ if(empty($et_zj)) api_json_exit(100, '不存在主机用户名');
$api = new bt_api($btipe,$btkeye);
$api->siteqt($et_zj['btid'],$et_zj['sqldz'],false);
$api->setftpzt($et_zj['ftpid'],$et_zj['user'],'0');
@@ -137,11 +149,13 @@ if($gn=='cfif'){
}
api_json_exit(200, '主机暂停成功!');
}elseif($gn=='xf'){
+ if(empty($et_zj)) api_json_exit(100, '不存在主机用户名');
$x_dq_date=($_POST['setdate'] ?? '0')=='0' ? '0000-00-00' : $_POST['setdate'];
$old_date=$et_zj['datae'] ?? '';
$api = new bt_api($btipe,$btkeye);
$r_data = $api->setdqsj($et_zj['btid'],$x_dq_date);
- if(strtotime($date)-strtotime($x_dq_date)<0 && $x_dq_date!='0000-00-00' && $et_zj['qk']){
+ // qk 为字符串 'true'/'false',需显式比较;未到期且已暂停时续费解停
+ if(strtotime($date)-strtotime($x_dq_date)<0 && $x_dq_date!='0000-00-00' && $et_zj['qk']=='true'){
$api->siteqt($et_zj['btid'],$et_zj['sqldz'],true);
$api->setftpzt($et_zj['ftpid'],$et_zj['user'],'1');
}
@@ -156,6 +170,7 @@ if($gn=='cfif'){
api_lifecycle_log('API续费主机','续费'.$user.'数据库写入失败','续费失败');
api_json_exit(100, '主机续费失败,数据库写入失败!');
}elseif($gn=='jc'){
+ if(empty($et_zj)) api_json_exit(100, '不存在主机用户名');
$api = new bt_api($btipe,$btkeye);
$api->siteqt($et_zj['btid'],$et_zj['sqldz'],true);
$api->setftpzt($et_zj['ftpid'],$et_zj['user'],'1');
@@ -167,6 +182,7 @@ if($gn=='cfif'){
}
else api_json_exit(100, '主机暂停解除成功!但是写入数据库时出现错误!请站长排查!');
}elseif($gn=='tz'){
+ if(empty($et_zj)) api_json_exit(100, '不存在主机用户名');
$api = new bt_api($btipe,$btkeye);
$r_data = $api->delsite($et_zj['btid'],$et_zj['sqldz']);
if($r_data['status']){
@@ -184,7 +200,9 @@ if($gn=='cfif'){
api_json_exit(100, '主机删除失败!因为'.($r_data['msg'] ?? '未知错误'));
}
}elseif($gn=='czmm'){
- $x_up_pass=$_POST['password'];
+ if(empty($et_zj)) api_json_exit(100, '不存在主机用户名');
+ if(!isset($_POST['password']) || $_POST['password']==='')api_json_exit(100, '错误!缺少新密码参数 password!');
+ $x_up_pass=daddslashes($_POST['password']);
$api = new bt_api($btipe,$btkeye);
$api->setftppass($et_zj['ftpid'],$user,$x_up_pass);
if($DB->query_prepare("update `MN_zj` set `pass` =? where `user`=?", [$x_up_pass, $user]))api_json_exit(200, '主机FTP及控制面板登陆密码重置成功!');
@@ -195,9 +213,10 @@ if($gn=='cfif'){
$hxa_array = json_decode($zjdata['hxa'],true);
$hxb_array = json_decode($zjdata['hxb'],true);
$llmax_array = json_decode($zjdata['llmax'],true);
- $hxa_array['max'] = $_POST['websize'];
- $hxb_array['max'] = $_POST['sqlsize'];
- $llmax_array['max'] = $_POST['ll'];
+ // 有传参才覆盖对应配额,三项相互独立,避免缺参导致配额被清零
+ if(isset($_POST['websize']) && $_POST['websize']!=='') $hxa_array['max'] = $_POST['websize'];
+ if(isset($_POST['sqlsize']) && $_POST['sqlsize']!=='') $hxb_array['max'] = $_POST['sqlsize'];
+ if(isset($_POST['ll']) && $_POST['ll']!=='') $llmax_array['max'] = $_POST['ll'];
if($DB->query_prepare("UPDATE `MN_zj` SET `hxa` = ?, `hxb` = ?, `llmax` = ? WHERE `user` = ?", [json_encode($hxa_array), json_encode($hxb_array), json_encode($llmax_array), $user])) api_json_exit(200, '主机修改成功');
api_json_exit(100, '主机修改失败我也不知道什么问题,请联系开发者');
}elseif($gn == 'start'){
diff --git a/api/docker.php b/api/docker.php
index 4ef1963..006dd00 100644
--- a/api/docker.php
+++ b/api/docker.php
@@ -185,7 +185,8 @@ if ($gn === 'xf') {
api_json_exit(100, '错误!该 Docker 账号不存在');
}
$old_datae = $urow['datae'];
- $updates = "datae='" . $new_datae . "'";
+ $updates = "datae=?";
+ $bind = [$new_datae];
// 若原 expired 且新到期时间未过 → 恢复 active
if ($urow['qk'] === 'expired') {
if ($new_datae === '0000-00-00' || strtotime($date) - strtotime($new_datae) < 0) {
@@ -200,7 +201,8 @@ if ($gn === 'xf') {
}
}
}
- $DB->query("UPDATE MN_docker_user SET {$updates} WHERE id=" . intval($urow['id']));
+ $bind[] = $urow['id'];
+ $DB->query_prepare("UPDATE MN_docker_user SET {$updates} WHERE id=?", $bind);
api_lifecycle_log('API续费Docker', '续费 ' . $user . ' ' . $old_datae . '=>' . $new_datae, '续费成功');
if (function_exists('mnbt_do_action')) {
mnbt_do_action('docker.user.renewed', array_merge($urow, ['datae' => $new_datae]), ['source' => 'api', 'old_date' => $old_datae, 'new_date' => $new_datae]);
diff --git a/mf_modules/servers/mnbtdocker/README.md b/mf_modules/servers/mnbtdocker/README.md
index bd35e49..ca1cba1 100644
--- a/mf_modules/servers/mnbtdocker/README.md
+++ b/mf_modules/servers/mnbtdocker/README.md
@@ -31,23 +31,19 @@
产品关联模块后只需填写可选配置(如 `plan_id`),其余从服务器字段自动解析。
-### 方式二:模块配置选项(精确控制)
+**注意**:`api_url` / `api_key` / `node_id` / `call_key` 不支持通过配置选项填写,必须使用上述服务器字段。
-产品配置选项中填写:
+产品可配置选项(configoptions)仅支持以下两个:
-| key | 说明 |
+| 配置选项 | 说明 |
|-----|------|
-| `api_url` | `https://mnbt.example.com/api/docker.php` |
-| `api_key` | 系统 API 密钥 |
-| `node_id` | 节点编号 |
-| `call_key` | `md5(节点ktmy . 节点qmk)` |
-| `plan_id` | 默认套餐 ID(可选) |
-| `console_url` | `https://mnbt.example.com/docker/login.php` |
+| `configoption1`(默认套餐 ID) | 开通时绑定的默认套餐(`MN_docker_plan.id`,可选) |
+| `configoption2`(控制台地址) | Docker 控制台入口 URL(可选,留空则按服务器地址自动拼接 `/docker/login.php`) |
## 产品关联
1. 后台「产品 → 添加产品」→ 类型:**服务器产品** → 模块:**梦奈宝塔Docker对接插件**
-2. 可配置选项添加「套餐 ID」字段,映射 key 为 `plan_id`(可选,用于升降级)
+2. 可配置选项添加「套餐 ID」字段(即 `configoption1`,可选,用于升降级;变更套餐时以此处的值为新套餐)
3. 上架后即可前台购买
## 模块方法说明
@@ -74,4 +70,5 @@
- **单容器模型**:每个账号仅一个容器;开通后用户需在 MNBT 控制台的应用商店自行创建容器
- **双登录体系**:魔方登录态 ≠ 梦奈宝塔Docker对接插件_token,用户需用 Docker 账号二次登录控制台
+- **用量查询(gn=sy)**:本插件未实现用量查询功能,前台不展示磁盘用量
- **P1 自动登录**:下期通过 `gn=dl` 一次性票据实现免登录跳转
diff --git a/mf_modules/servers/mnbtdocker/mnbtdocker.php b/mf_modules/servers/mnbtdocker/mnbtdocker.php
index 56afe90..2d5ca90 100644
--- a/mf_modules/servers/mnbtdocker/mnbtdocker.php
+++ b/mf_modules/servers/mnbtdocker/mnbtdocker.php
@@ -125,8 +125,11 @@ function _mnbtdocker_api_call($params, $gn, $extra = [], $timeout = 30)
'username' => $username,
], $extra);
- // DEBUG:打印请求参数(部署确认问题后可删除此段)
- $debug_info = "gn={$gn}, mn_bh=[{$post['mn_bh']}], mn_key=[len=" . strlen($post['mn_key']) . "], mn_keye=[len=" . strlen($post['mn_keye']) . "], mn_vs=[{$post['mn_vs']}], username=[{$post['username']}]";
+ // DEBUG:仅在 MNBT_DEBUG 为 true 时打印请求参数
+ $debug_info = '';
+ if (defined('MNBT_DEBUG') && MNBT_DEBUG) {
+ $debug_info = "gn={$gn}, mn_bh=[{$post['mn_bh']}], mn_key=[len=" . strlen($post['mn_key']) . "], mn_keye=[len=" . strlen($post['mn_keye']) . "], mn_vs=[{$post['mn_vs']}], username=[{$post['username']}]";
+ }
$url = $api_url . '?gn=' . urlencode($gn);
@@ -155,8 +158,10 @@ function _mnbtdocker_api_call($params, $gn, $extra = [], $timeout = 30)
return ['success' => false, 'code' => 0, 'msg' => '[mnbtdocker] 响应解析失败:' . substr($resp, 0, 200)];
}
- // 附加调试信息到响应中
- $decoded['_debug'] = $debug_info;
+ // 附加调试信息到响应中(仅 MNBT_DEBUG 开启时)
+ if ($debug_info !== '') {
+ $decoded['_debug'] = $debug_info;
+ }
return $decoded;
}
@@ -207,6 +212,25 @@ function mnbtdocker_TestLink($params)
// 生命周期方法
// ========================================================================
+/**
+ * 统一把 nextduedate 转为 'Y-m-d'(后端 dqtime/setdate 期望格式)
+ * 纯数字时间戳先转日期;空值/'0000-00-00' 返回 '0'(永久)
+ */
+function _mnbtdocker_format_dqtime($nextduedate)
+{
+ $dqtime = trim((string)($nextduedate ?? ''));
+ if ($dqtime === '' || $dqtime === '0000-00-00' || $dqtime === '0000-00-00 00:00:00') {
+ return '0';
+ }
+ if (ctype_digit($dqtime)) {
+ $dqtime = date('Y-m-d', (int)$dqtime);
+ } else {
+ $ts = strtotime($dqtime);
+ $dqtime = $ts ? date('Y-m-d', $ts) : '0';
+ }
+ return $dqtime ?: '0';
+}
+
/**
* 开通账户
* 只开通账号,不创建容器(容器由用户登录控制台后在应用商店创建)
@@ -223,10 +247,7 @@ function mnbtdocker_CreateAccount($params)
$password = substr(md5(uniqid(mt_rand(), true)), 0, 12);
}
- $dqtime = $params['nextduedate'] ?? '';
- if (empty($dqtime) || $dqtime == '0000-00-00') {
- $dqtime = '0';
- }
+ $dqtime = _mnbtdocker_format_dqtime($params['nextduedate'] ?? '');
$extra = [
'username' => $username,
@@ -241,9 +262,18 @@ function mnbtdocker_CreateAccount($params)
$r = _mnbtdocker_api_call($params, 'kt', $extra);
$result = _mnbtdocker_return($r);
- if ($result === 'success' && !empty($password)) {
- // 尝试回写密码到产品表
- // idcsmart 可能通过返回值中的 password 字段自动更新
+ // 开通成功后把真实密码回写到魔方产品表,确保用户可在产品详情中看到密码
+ if ($result === 'success' && !empty($password) && !empty($params['hostid'])) {
+ try {
+ if (class_exists('\think\facade\Db')) {
+ // 魔方财务(ThinkPHP 6)环境:更新 host 表 password 字段
+ \think\facade\Db::name('host')
+ ->where('id', (int)$params['hostid'])
+ ->update(['password' => $password]);
+ }
+ } catch (\Throwable $e) {
+ // 回写失败不影响开通结果(如非 TP 环境或字段不存在)
+ }
}
return $result;
@@ -273,10 +303,7 @@ function mnbtdocker_TerminateAccount($params)
/** 续费 */
function mnbtdocker_Renew($params)
{
- $dqtime = $params['nextduedate'] ?? '';
- if (empty($dqtime) || $dqtime == '0000-00-00') {
- $dqtime = '0';
- }
+ $dqtime = _mnbtdocker_format_dqtime($params['nextduedate'] ?? '');
$r = _mnbtdocker_api_call($params, 'xf', ['setdate' => $dqtime]);
return _mnbtdocker_return($r);
}
@@ -287,11 +314,13 @@ function mnbtdocker_ChangePackage($params)
// 从可配置选项中获取新的 plan_id
// 魔方升降级时 old_configoptions / configoptions 会有新旧值
$new_plan_id = $params['configoptions']['plan_id'] ?? $params['config_options']['plan_id'] ?? 0;
- if (empty($new_plan_id)) {
- // 兼容:某些魔方版本 key 在 configoptionX 直接覆盖
- list(, , , , $new_plan_id) = _mnbtdocker_resolve_params($params);
+ $new_plan_id = is_array($new_plan_id) ? '' : trim((string)$new_plan_id);
+ if ($new_plan_id === '') {
+ // 兼容:某些魔方版本 key 在 configoptionX 直接覆盖(此时 configoption1 即新套餐)
+ $new_plan_id = trim((string)($params['configoption1'] ?? ''));
}
- if (empty($new_plan_id)) {
+ if ($new_plan_id === '' || (int)$new_plan_id <= 0) {
+ // 解析不到新套餐 ID 时明确报错,避免静默重复提交原套餐
return '未找到新套餐 ID,请确认产品可配置选项中已设置 plan_id 字段';
}
$r = _mnbtdocker_api_call($params, 'bg', ['plan_id' => $new_plan_id]);
@@ -315,7 +344,7 @@ function mnbtdocker_CrackPassword($params, $new_pass = '')
*/
function mnbtdocker_Status($params)
{
- $r = _mnbtdocker_api_call($params, 'ztcx', [], 60);
+ $r = _mnbtdocker_api_call($params, 'ztcx', [], 25);
if (!($r['success'] ?? false) || ($r['code'] ?? 0) != 200) {
return [
'status' => 'error',
@@ -349,7 +378,7 @@ function mnbtdocker_Status($params)
}
// 附加容器信息
- if ($container && !empty($container['port'])) {
+ if ($container && !empty($container['port']) && is_array($container['port'])) {
$des .= ' | 端口:' . implode(', ', $container['port']);
}
diff --git a/mf_modules/servers/mnbtdocker/templates/console.html b/mf_modules/servers/mnbtdocker/templates/console.html
index 914205b..d3df65b 100644
--- a/mf_modules/servers/mnbtdocker/templates/console.html
+++ b/mf_modules/servers/mnbtdocker/templates/console.html
@@ -31,14 +31,14 @@
<div class="info-label">控制台地址</div>
<div class="info-value">
{if condition="!empty($console_url)"}
- <a href="{$console_url}" target="_blank">{$console_url}</a>
+ <a href="{$console_url|htmlspecialchars}" target="_blank" rel="noopener noreferrer">{$console_url|htmlspecialchars}</a>
{else /}
<span style="color:#999">未配置</span>
{/if}
</div>
</div>
{if condition="!empty($console_url)"}
- <button class="copy-btn" onclick="dockerCopy(this,'{$console_url}')" title="复制">📋</button>
+ <button class="copy-btn" data-text="{$console_url|htmlspecialchars}" title="复制">📋</button>
{/if}
</div>
@@ -46,10 +46,10 @@
<div class="info-icon user">👤</div>
<div class="info-body">
<div class="info-label">登录账号</div>
- <div class="info-value"><code>{$username|default='—'}</code></div>
+ <div class="info-value"><code>{$username|default='—'|htmlspecialchars}</code></div>
</div>
{if condition="!empty($username)"}
- <button class="copy-btn" onclick="dockerCopy(this,'{$username}')" title="复制">📋</button>
+ <button class="copy-btn" data-text="{$username|htmlspecialchars}" title="复制">📋</button>
{/if}
</div>
@@ -57,24 +57,24 @@
<div class="info-icon pwd">🔑</div>
<div class="info-body">
<div class="info-label">登录密码</div>
- <div class="info-value"><code>{$password|default='—'}</code></div>
+ <div class="info-value"><code>{$password|default='—'|htmlspecialchars}</code></div>
</div>
{if condition="!empty($password)"}
- <button class="copy-btn" onclick="dockerCopy(this,'{$password}')" title="复制">📋</button>
+ <button class="copy-btn" data-text="{$password|htmlspecialchars}" title="复制">📋</button>
{/if}
</div>
<div class="info-row">
- <div class="info-icon stat {$status_class}">📊</div>
+ <div class="info-icon stat {$status_class|htmlspecialchars}">📊</div>
<div class="info-body">
<div class="info-label">容器状态</div>
- <div class="status-text">{$status_text}</div>
+ <div class="status-text">{$status_text|htmlspecialchars}</div>
</div>
</div>
{if condition="!empty($console_url)"}
<div class="btn-area">
- <a href="{$console_url}" target="_blank" class="btn btn-primary btn-sm">前往控制台</a>
+ <a href="{$console_url|htmlspecialchars}" target="_blank" rel="noopener noreferrer" class="btn btn-primary btn-sm">前往控制台</a>
</div>
{/if}
@@ -82,6 +82,7 @@
</div>
<script>
+// 通过 data-* 属性安全传参,避免将变量直接拼入 JS 字符串
function dockerCopy(btn, text) {
var ta = document.createElement('textarea');
ta.value = text;
@@ -93,4 +94,9 @@ function dockerCopy(btn, text) {
btn.innerHTML = '✓';
setTimeout(function(){ btn.classList.remove('copied'); btn.innerHTML = '📋'; }, 1500);
}
+document.addEventListener('click', function(e) {
+ var btn = e.target.closest ? e.target.closest('.copy-btn') : null;
+ if (!btn) return;
+ dockerCopy(btn, btn.getAttribute('data-text') || '');
+});
</script>
diff --git a/mf_modules/servers/mnbthost/README.md b/mf_modules/servers/mnbthost/README.md
index 18f9f5c..f5f7fe0 100644
--- a/mf_modules/servers/mnbthost/README.md
+++ b/mf_modules/servers/mnbthost/README.md
@@ -52,7 +52,7 @@ MNBT 后台「宝塔列表」→ 找到对应节点 → **ktmy 列**点击👁
| 恢复 | `jc` | qk=true |
| 删除 | `tz` | 删站点+删行 |
| 续费 | `xf` | 更新到期时间 |
-| 升降级 | `zjmode` | 更新空间/数据库/流量配额(仅传变更项) |
+| 升降级 | `zjmode` | 全量传三项配额(空间/数据库/流量),后端有传参才覆盖对应字段 |
| 改密 | `czmm` | 重置 FTP+控制面板密码 |
| 开机/关机 | `start`/`stop` | 站点启停(P0 已实现) |
| 状态/同步 | `ztcx` | 状态+配额用量 |
diff --git a/mf_modules/servers/mnbthost/mnbthost.php b/mf_modules/servers/mnbthost/mnbthost.php
index db63af3..e2d1311 100644
--- a/mf_modules/servers/mnbthost/mnbthost.php
+++ b/mf_modules/servers/mnbthost/mnbthost.php
@@ -79,7 +79,7 @@ function mnbthost_ConfigOptions()
* server_password → 调用密钥 md5(ktmy.qmk),空则默认 md5('')
* accesshash → 系统 API 密钥($conf['api'])
*
- * @return array [api_url, api_key, node_id, call_key, plan_id, console_url]
+ * @return array [api_url, api_key, node_id, call_key, console_url]
*/
function _mnbthost_resolve_params($params)
{
@@ -125,6 +125,24 @@ function _mnbthost_resolve_params($params)
return [$api_url, $api_key, $node_id, $call_key, $console_url];
}
+/**
+ * 将魔方的到期时间统一转换为 'Y-m-d' 格式
+ * 魔方 nextduedate 可能是纯时间戳或 'Y-m-d H:i:s' 字符串;
+ * 空 / '0000-00-00' 返回 '0'(后端约定 0 = 永不到期)
+ */
+function _mnbthost_format_duedate($nextduedate)
+{
+ $nextduedate = trim((string)$nextduedate);
+ if ($nextduedate === '' || $nextduedate == '0000-00-00' || $nextduedate == '0000-00-00 00:00:00') {
+ return '0';
+ }
+ if (ctype_digit($nextduedate)) {
+ return date('Y-m-d', (int)$nextduedate);
+ }
+ $ts = strtotime($nextduedate);
+ return $ts ? date('Y-m-d', $ts) : '0';
+}
+
/**
* 调用 MNBT 虚拟主机 API
*
@@ -155,10 +173,17 @@ function _mnbthost_api_call($params, $gn, $extra = [], $timeout = 30)
'username' => $username,
], $extra);
- // DEBUG:打印请求参数摘要(定位鉴权问题用,确认后可删除)
- $dbg = "gn={$gn} | mn_bh=[{$post['mn_bh']}] | mn_key_len=" . strlen($post['mn_key'])
- . " | mn_keye=[" . substr($post['mn_keye'], 0, 6) . '***' . substr($post['mn_keye'], -4) . '](len=' . strlen($post['mn_keye']) . ')'
- . " | username=[{$post['username']}]";
+ // DEBUG:打印请求参数摘要(仅当定义了 MNBT_DEBUG 且为真时附加,避免生产环境泄露敏感信息)
+ $dbg = '';
+ if (defined('MNBT_DEBUG') && MNBT_DEBUG) {
+ $dbg = "gn={$gn} | mn_bh=[{$post['mn_bh']}] | mn_key_len=" . strlen($post['mn_key'])
+ . " | mn_keye=[" . substr($post['mn_keye'], 0, 6) . '***' . substr($post['mn_keye'], -4) . '](len=' . strlen($post['mn_keye']) . ')'
+ . " | username=[{$post['username']}]";
+ }
+
+ // SSL 证书校验开关:默认关闭以兼容自签名证书环境;
+ // 需要开启时在魔方入口文件定义 define('MNBT_SSL_VERIFY', true) 即可
+ $ssl_verify = defined('MNBT_SSL_VERIFY') ? MNBT_SSL_VERIFY : false;
$url = $api_url . '?gn=' . urlencode($gn);
@@ -169,8 +194,8 @@ function _mnbthost_api_call($params, $gn, $extra = [], $timeout = 30)
CURLOPT_POSTFIELDS => http_build_query($post),
CURLOPT_TIMEOUT => $timeout,
CURLOPT_RETURNTRANSFER => true,
- CURLOPT_SSL_VERIFYHOST => false,
- CURLOPT_SSL_VERIFYPEER => false,
+ CURLOPT_SSL_VERIFYPEER => (bool)$ssl_verify,
+ CURLOPT_SSL_VERIFYHOST => $ssl_verify ? 2 : 0,
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded; charset=UTF-8'],
]);
$resp = curl_exec($ch);
@@ -187,8 +212,10 @@ function _mnbthost_api_call($params, $gn, $extra = [], $timeout = 30)
return ['success' => false, 'code' => 0, 'msg' => '[mnbthost] 响应解析失败:' . substr($resp, 0, 200)];
}
- // 附加调试摘要到响应
- $decoded['_debug'] = $dbg;
+ // 附加调试摘要到响应(仅 MNBT_DEBUG 开启时)
+ if ($dbg !== '') {
+ $decoded['_debug'] = $dbg;
+ }
return $decoded;
}
@@ -246,10 +273,7 @@ function mnbthost_CreateAccount($params)
$password = substr(md5(uniqid(mt_rand(), true)), 0, 12);
}
- $dqtime = $params['nextduedate'] ?? '';
- if (empty($dqtime) || $dqtime == '0000-00-00') {
- $dqtime = '0';
- }
+ $dqtime = _mnbthost_format_duedate($params['nextduedate'] ?? '');
$co = $params['configoptions'] ?? [];
$extra = [
@@ -290,40 +314,33 @@ function mnbthost_TerminateAccount($params)
/** 续费 */
function mnbthost_Renew($params)
{
- $dqtime = $params['nextduedate'] ?? '';
- if (empty($dqtime) || $dqtime == '0000-00-00') {
- $dqtime = '0';
- }
+ $dqtime = _mnbthost_format_duedate($params['nextduedate'] ?? '');
$r = _mnbthost_api_call($params, 'xf', ['setdate' => $dqtime]);
return _mnbthost_return($r);
}
-/** 升降级(更新空间/数据库/流量配额,仅传变更项) */
+/** 升降级(更新空间/数据库/流量配额,全量传三项配额,用现有配置值兜底) */
function mnbthost_ChangePackage($params)
{
- $upgrade = $params['configoptions_upgrade'] ?? [];
$co = $params['configoptions'] ?? [];
- $extra = [];
- if (isset($upgrade['webdx'])) $extra['websize'] = $co['webdx'];
- if (isset($upgrade['sqldx'])) $extra['sqlsize'] = $co['sqldx'];
- if (isset($upgrade['sizemax'])) $extra['ll'] = $co['sizemax'];
-
- if (empty($extra)) {
- // 无配额变更时仍调用一次确保同步(zjmode 需传全量,用现有值兜底)
- $extra = [
- 'websize' => $co['webdx'] ?? 0,
- 'sqlsize' => $co['sqldx'] ?? 0,
- 'll' => $co['sizemax'] ?? 0,
- ];
- }
+ // 后端 zjmode 按传参覆盖对应配额,为保持三项配额一致,这里全量传三项
+ $extra = [
+ 'websize' => $co['webdx'] ?? 0,
+ 'sqlsize' => $co['sqldx'] ?? 0,
+ 'll' => $co['sizemax'] ?? 0,
+ ];
$r = _mnbthost_api_call($params, 'zjmode', $extra);
return _mnbthost_return($r);
}
-/** 重置密码(idcsmart 将新密码作为第二参数传入) */
+/** 重置密码(idcsmart 将新密码作为第二参数传入,部分版本仅放在 $params['password']) */
function mnbthost_CrackPassword($params, $new_pass = '')
{
+ // 优先取第二参数,为空则回退到 $params['password'](兼容魔方部分版本)
+ if (empty($new_pass)) {
+ $new_pass = $params['password'] ?? '';
+ }
if (empty($new_pass)) return '缺少新密码';
$r = _mnbthost_api_call($params, 'czmm', ['password' => $new_pass]);
return _mnbthost_return($r);
@@ -439,11 +456,13 @@ function mnbthost_ClientAreaOutput($params, $key)
return [
'template' => 'templates/console.html',
'vars' => [
- 'status_text' => $status_info['des'],
- 'status_class' => $status_class,
- 'console_url' => $console_url,
- 'username' => $params['domain'] ?? ($params['username'] ?? ''),
- 'password' => $params['password'] ?? '',
+ 'status_text' => htmlspecialchars($status_info['des'] ?? '', ENT_QUOTES),
+ 'status_class' => htmlspecialchars($status_class, ENT_QUOTES),
+ 'console_url' => htmlspecialchars($console_url ?? '', ENT_QUOTES),
+ 'username' => htmlspecialchars($params['domain'] ?? ($params['username'] ?? ''), ENT_QUOTES),
+ 'password' => htmlspecialchars($params['password'] ?? '', ENT_QUOTES),
+ // 密码默认展示掩码,点击"显示"后再展示明文
+ 'password_mask' => !empty($params['password']) ? str_repeat('•', 8) : '',
'quota' => $quota,
'quota_pct' => $quota_pct,
],
diff --git a/mf_modules/servers/mnbthost/templates/console.html b/mf_modules/servers/mnbthost/templates/console.html
index ee1b7b0..4fe7791 100644
--- a/mf_modules/servers/mnbthost/templates/console.html
+++ b/mf_modules/servers/mnbthost/templates/console.html
@@ -46,7 +46,7 @@
</div>
</div>
{if condition="!empty($console_url)"}
- <button class="copy-btn" onclick="hostCopy(this,'{$console_url}')" title="复制">📋</button>
+ <button class="copy-btn" data-copy="{$console_url}" onclick="hostCopy(this)" title="复制">📋</button>
{/if}
</div>
@@ -57,7 +57,7 @@
<div class="info-value"><code>{$username|default='—'}</code></div>
</div>
{if condition="!empty($username)"}
- <button class="copy-btn" onclick="hostCopy(this,'{$username}')" title="复制">📋</button>
+ <button class="copy-btn" data-copy="{$username}" onclick="hostCopy(this)" title="复制">📋</button>
{/if}
</div>
@@ -65,10 +65,15 @@
<div class="info-icon pwd">🔑</div>
<div class="info-body">
<div class="info-label">登录密码</div>
- <div class="info-value"><code>{$password|default='—'}</code></div>
+ <div class="info-value"><input type="hidden" id="hostPasswordPlain" value="{$password}">
+<code id="hostPassword" style="user-select:none">{$password_mask|default='—'}</code>
+ {if condition="!empty($password)"}
+ <a href="javascript:void(0)" onclick="hostTogglePwd(this)" style="margin-left:6px;font-size:12px;">显示</a>
+ {/if}
+ </div>
</div>
{if condition="!empty($password)"}
- <button class="copy-btn" onclick="hostCopy(this,'{$password}')" title="复制">📋</button>
+ <button class="copy-btn" data-copy="{$password}" onclick="hostCopy(this)" title="复制">📋</button>
{/if}
</div>
@@ -138,7 +143,9 @@
</div>
<script>
-function hostCopy(btn, text) {
+// 复制:从 data-copy 属性取值,避免将值拼进 JS 字符串导致注入
+function hostCopy(btn) {
+ var text = btn.getAttribute('data-copy') || '';
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed'; ta.style.left = '-9999px';
@@ -149,4 +156,19 @@ function hostCopy(btn, text) {
btn.innerHTML = '✓';
setTimeout(function(){ btn.classList.remove('copied'); btn.innerHTML = '📋'; }, 1500);
}
+
+// 密码显示/隐藏切换
+function hostTogglePwd(link) {
+ var el = document.getElementById('hostPassword');
+ if (!el) return;
+ if (el.dataset.show !== '1') {
+ el.dataset.plain = el.textContent = document.getElementById('hostPasswordPlain').value;
+ el.dataset.show = '1';
+ link.textContent = '隐藏';
+ } else {
+ el.textContent = '••••••••';
+ el.dataset.show = '0';
+ link.textContent = '显示';
+ }
+}
</script>