手动添加域名
This commit is contained in:
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
define('SHOW_SITE_MODE_SWITCH', 'ip_check' );
|
||||
define('SHOW_SITE_IP', '' );
|
||||
define('BLACKLIST_GROUP_ID', 0 );
|
||||
define('WHITELIST_GROUP_ID', 0 );
|
||||
define('SHOW_SITE_COUNTRY', '' );
|
||||
define('CLOAK_SHOW_CONTENT', '302');
|
||||
define('DB_FP', array (
|
||||
0 => 'https://www.google.com/',
|
||||
) );
|
||||
define('COSTM_IP_SCORE', '20251108_emoUAVF6S' );
|
||||
define('BLACK_LIST', '0' );
|
||||
define('AUTO_BLACK', 'OFF' );
|
||||
define('IS_VIRTUAL', '2' );
|
||||
define('AUTO_BLACK_TIMES', '5' );
|
||||
define('WHITE_PARAMS', '' );
|
||||
define('IPHONE_MODEL', 0 );
|
||||
|
||||
define('SHOW_SITE_MOBILE', 0 );
|
||||
define('SHOW_SITE_OUTPUT', 1 );
|
||||
define('WRITE_LOG', 1 );
|
||||
define('CLOAK_ZH_ON', 'ON');
|
||||
define('CLOAK_OS_ON', 'OFF');
|
||||
define('KEEP_PARAMS', 'OFF');
|
||||
define('CLOAK_REDIRECT_METHOD', 'curl');
|
||||
define('DB_ZP', 'https://worldhomenest.store');
|
||||
define('CLOAK_MOBILE_ON', 'OFF');
|
||||
define('CLOAK_V_REFERER', 'OFF');
|
||||
define('CLOAK_RISK_NUMBER', '68');
|
||||
define('CLOAK_RISK_ENHANCED', 'ON');
|
||||
define('CLOAK_URL_ARGS_TIMEOUT', '0');
|
||||
|
||||
define('CLOAK_AD_FB', 'OFF');
|
||||
define('CLOAK_AD_GOOGLE', 'ON');
|
||||
define('CLOAK_AD_TIKTOK', 'ON');
|
||||
define('CLOAK_AD_STRICT', 'OFF');
|
||||
define('CLOAK_AD_SPM_ID', 'Ss4yqlAnuG1V');
|
||||
|
||||
define('CLOAK_DEBUG_MODE', 'OFF');
|
||||
?>
|
||||
+376
-71
@@ -191,7 +191,17 @@ if (!empty($_REQUEST["action"]) && $_REQUEST["action"] == "submit") {
|
||||
try {
|
||||
$pdoBind = DomainRepository::pdo();
|
||||
DomainRepository::ensureTable($pdoBind);
|
||||
$siteDomainId = (int) ($_REQUEST['site_domain_id'] ?? 0);
|
||||
$siteDomainRaw = $_REQUEST['site_domain_id'] ?? '0';
|
||||
if ($siteDomainRaw === 'random') {
|
||||
$existingId = DomainRepository::domainIdForConfig($pdoBind, $check_name);
|
||||
if ($existingId > 0) {
|
||||
$siteDomainId = $existingId;
|
||||
} else {
|
||||
$siteDomainId = DomainRepository::pickRandomDomainId($pdoBind);
|
||||
}
|
||||
} else {
|
||||
$siteDomainId = (int) $siteDomainRaw;
|
||||
}
|
||||
if ($siteDomainId > 0) {
|
||||
DomainRepository::bindConfigToDomain($pdoBind, $check_name, $siteDomainId);
|
||||
} else {
|
||||
@@ -215,10 +225,10 @@ function onoff_radio($name, $val, $label_on='开启', $label_off='关闭') {
|
||||
$on = ($val == 'ON') ? 'active' : '';
|
||||
$off = ($val != 'ON') ? 'active' : '';
|
||||
return '<div class="btn-group btn-group-toggle" data-toggle="buttons">
|
||||
<label class="btn btn-sm btn-outline-secondary ' . $on . '">
|
||||
<label class="btn btn-sm btn-outline-primary ' . $on . '">
|
||||
<input type="radio" name="' . $name . '" value="ON" ' . ($val=='ON'?'checked':'') . '> ' . $label_on . '
|
||||
</label>
|
||||
<label class="btn btn-sm btn-outline-secondary ' . $off . '">
|
||||
<label class="btn btn-sm btn-outline-primary ' . $off . '">
|
||||
<input type="radio" name="' . $name . '" value="OFF" ' . ($val!='ON'?'checked':'') . '> ' . $label_off . '
|
||||
</label>
|
||||
</div>';
|
||||
@@ -256,6 +266,7 @@ body { background:#f0f2f5; font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-ser
|
||||
.topbar a:hover { color:#fff; }
|
||||
.topbar h1 { margin:0; font-size:1.25rem; font-weight:600; }
|
||||
.page-body { max-width:1100px; margin:32px auto; padding:0 16px; }
|
||||
.page-body.page-body--list { width:90%; max-width:none; margin-left:auto; margin-right:auto; }
|
||||
.status-bar { display:flex; gap:12px; margin-bottom:20px; flex-wrap:wrap; }
|
||||
.status-badge { display:inline-flex; align-items:center; gap:6px; padding:6px 14px; border-radius:20px; font-size:.82rem; font-weight:500; }
|
||||
.status-ok { background:#d4edda; color:#155724; }
|
||||
@@ -326,6 +337,40 @@ body { background:#f0f2f5; font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-ser
|
||||
.domain-host-badge { display: inline-block; padding: 2px 8px; background: #e8f4fd; color: #1565c0; border-radius: 4px; font-size: .78rem; font-family: 'SF Mono', Consolas, monospace; }
|
||||
.config-name-cell strong { color: #2c3e50; font-size: .92rem; }
|
||||
.empty-hint { padding: 28px 20px; text-align: center; color: #8c98a4; font-size: .88rem; background: #fafbfc; border-radius: 0 0 10px 10px; }
|
||||
.list-toolbar { display:flex; flex-wrap:wrap; align-items:center; gap:10px; padding:12px 16px; border-bottom:1px solid #e9ecef; background:#fafbfc; }
|
||||
.list-toolbar .search-form { display:flex; align-items:center; gap:8px; flex:1; min-width:220px; }
|
||||
.list-toolbar .search-form input[type="search"] { max-width:280px; }
|
||||
.list-pagination { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:10px; padding:12px 16px; border-top:1px solid #e9ecef; background:#fff; font-size:.84rem; color:#6c757d; }
|
||||
.list-pagination .pagination { margin:0; }
|
||||
.config-table .col-check { width:36px; text-align:center; }
|
||||
/* 表单开关:选中实心蓝,未选中蓝框蓝字 */
|
||||
.btn-group-toggle .btn.btn-outline-primary,
|
||||
.btn-group-toggle .btn.btn-outline-secondary {
|
||||
color: #1565c0;
|
||||
border-color: #64b5f6;
|
||||
background: #fff;
|
||||
font-weight: 500;
|
||||
min-width: 52px;
|
||||
}
|
||||
.btn-group-toggle .btn.btn-outline-primary:hover,
|
||||
.btn-group-toggle .btn.btn-outline-secondary:hover {
|
||||
color: #0d47a1;
|
||||
background: #e3f2fd;
|
||||
border-color: #42a5f5;
|
||||
}
|
||||
.btn-group-toggle .btn.btn-outline-primary.active,
|
||||
.btn-group-toggle .btn.btn-outline-secondary.active,
|
||||
.btn-group-toggle .btn.btn-outline-primary.active:hover,
|
||||
.btn-group-toggle .btn.btn-outline-secondary.active:hover {
|
||||
color: #fff !important;
|
||||
background: linear-gradient(180deg, #2196f3 0%, #1565c0 100%) !important;
|
||||
border-color: #1565c0 !important;
|
||||
box-shadow: 0 1px 4px rgba(21, 101, 192, .35);
|
||||
}
|
||||
.btn-group-toggle .btn.btn-outline-primary:not(.active):focus,
|
||||
.btn-group-toggle .btn.btn-outline-secondary:not(.active):focus {
|
||||
box-shadow: 0 0 0 .15rem rgba(33, 150, 243, .25);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -355,45 +400,61 @@ if ($_SESSION['password'] != LOGIN_PASSWORD) {
|
||||
exit();
|
||||
}
|
||||
|
||||
// 删除配置
|
||||
if (!empty($_REQUEST["action"]) && $_REQUEST["action"] == "delete_config" && !empty($_REQUEST["name"])) {
|
||||
$name = trim($_REQUEST["name"]);
|
||||
if ($name == "index") {
|
||||
echo("<script>alert('请勿删除默认配置!');window.location='{$current_name}';</script>");
|
||||
exit();
|
||||
}
|
||||
$cfg = str_replace(".php", "", $name);
|
||||
try {
|
||||
DomainRepository::clearConfigBinding(DomainRepository::pdo(), $cfg);
|
||||
} catch (Throwable $e) {
|
||||
}
|
||||
@unlink("check_config/{$cfg}_config.php");
|
||||
@unlink("{$cfg}.php");
|
||||
echo("<script>alert('删除成功!');window.location='{$current_name}';</script>");
|
||||
exit();
|
||||
}
|
||||
|
||||
// 站点域名管理
|
||||
$cloakDomainMsg = '';
|
||||
$cloakDomainErr = '';
|
||||
$cloakCfEnabled = CloudflareConfig::isEnabled();
|
||||
if (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'add_domain' && !empty($_POST['new_hostname'])) {
|
||||
try {
|
||||
$pdoDom = DomainRepository::pdo();
|
||||
$res = DomainProvisioningService::provisionOnAdd($pdoDom, $_POST['new_hostname']);
|
||||
if ($res['ok']) {
|
||||
$cloakDomainMsg = '域名「' . htmlspecialchars($res['hostname']) . '」已添加';
|
||||
if (!empty($res['nameservers'])) {
|
||||
$nsHtml = implode('</code>、<code>', array_map('htmlspecialchars', $res['nameservers']));
|
||||
$cloakDomainMsg .= '。请到注册商将 NS 修改为:<code>' . $nsHtml . '</code>,然后点击「检测/刷新」。';
|
||||
} else {
|
||||
$cloakDomainMsg .= ',请在编辑配置时关联此域名。';
|
||||
if (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'add_domain') {
|
||||
$hostText = trim((string) ($_POST['new_hostnames'] ?? $_POST['new_hostname'] ?? ''));
|
||||
if ($hostText !== '') {
|
||||
try {
|
||||
$pdoDom = DomainRepository::pdo();
|
||||
$dnsMode = $_POST['dns_mode'] ?? 'auto';
|
||||
if (!in_array($dnsMode, ['auto', 'manual'], true)) {
|
||||
$dnsMode = 'auto';
|
||||
}
|
||||
} else {
|
||||
$cloakDomainErr = $res['error'] ?? '添加失败';
|
||||
if ($dnsMode === 'auto' && !$cloakCfEnabled) {
|
||||
$cloakDomainErr = '自动模式需要先在 cong.php 配置 Cloudflare API Token 与 Account ID,或改选手动模式。';
|
||||
} else {
|
||||
$res = DomainProvisioningService::provisionBatch($pdoDom, $hostText, $dnsMode);
|
||||
if ($res['ok']) {
|
||||
$addedNames = array_map(static function ($row) {
|
||||
return $row['hostname'] ?? '';
|
||||
}, $res['added'] ?? []);
|
||||
$addedNames = array_filter($addedNames);
|
||||
$cloakDomainMsg = '已成功添加 ' . count($addedNames) . ' 个域名:'
|
||||
. htmlspecialchars(implode('、', $addedNames));
|
||||
if (($res['dns_mode'] ?? '') === 'auto' && $cloakCfEnabled) {
|
||||
$cloakDomainMsg .= '。请到注册商修改 Nameserver 后,在列表中点击「检测/刷新」。';
|
||||
} elseif (($res['dns_mode'] ?? '') === 'manual') {
|
||||
$cloakDomainMsg .= '(手动解析模式,无需 Cloudflare 检测)。';
|
||||
} else {
|
||||
$cloakDomainMsg .= ',请在编辑配置时关联此域名。';
|
||||
}
|
||||
if (!empty($res['failed'])) {
|
||||
$failParts = [];
|
||||
foreach ($res['failed'] as $f) {
|
||||
$failParts[] = ($f['hostname'] ?? '?') . '(' . ($f['error'] ?? '失败') . ')';
|
||||
}
|
||||
$cloakDomainErr = count($res['failed']) . ' 个域名添加失败:'
|
||||
. htmlspecialchars(implode(';', $failParts));
|
||||
}
|
||||
} else {
|
||||
$cloakDomainErr = $res['error'] ?? '添加失败';
|
||||
if (!empty($res['failed'])) {
|
||||
$failParts = [];
|
||||
foreach ($res['failed'] as $f) {
|
||||
$failParts[] = ($f['hostname'] ?? '?') . '(' . ($f['error'] ?? '失败') . ')';
|
||||
}
|
||||
$cloakDomainErr .= ':' . htmlspecialchars(implode(';', $failParts));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$cloakDomainErr = $e->getMessage();
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$cloakDomainErr = $e->getMessage();
|
||||
} else {
|
||||
$cloakDomainErr = '请至少输入一个域名';
|
||||
}
|
||||
}
|
||||
if (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'check_domain') {
|
||||
@@ -535,20 +596,163 @@ function dash_build_ad_links($pf, $protocol, $host)
|
||||
return [['label' => '落地链接', 'url' => $url]];
|
||||
}
|
||||
|
||||
// 遍历配置列表
|
||||
$products_file_ary = [];
|
||||
$dd = opendir("check_config");
|
||||
while (false !== ($f = readdir($dd))) {
|
||||
if ($f == "." || $f == "..") continue;
|
||||
if (filetype("check_config/$f") == "dir" || $f == "index.php" || $f == "a.txt") continue;
|
||||
$fname = str_replace("_config.php", "", $f);
|
||||
$products_file_ary[] = $fname;
|
||||
if (!file_exists($fname . ".php")) {
|
||||
copy("tem_link.php", $fname . ".php");
|
||||
function dash_build_config_index()
|
||||
{
|
||||
$items = [];
|
||||
$dir = __DIR__ . '/check_config';
|
||||
$dh = @opendir($dir);
|
||||
if (!$dh) {
|
||||
return $items;
|
||||
}
|
||||
while (false !== ($f = readdir($dh))) {
|
||||
if ($f === '.' || $f === '..') {
|
||||
continue;
|
||||
}
|
||||
$path = $dir . '/' . $f;
|
||||
if (is_dir($path) || $f === 'index.php' || $f === 'a.txt') {
|
||||
continue;
|
||||
}
|
||||
if (substr($f, -11) !== '_config.php') {
|
||||
continue;
|
||||
}
|
||||
$name = str_replace('_config.php', '', $f);
|
||||
if ($name === '' || !preg_match('/^[a-zA-Z0-9_-]+$/', $name)) {
|
||||
continue;
|
||||
}
|
||||
$entryPhp = __DIR__ . '/' . $name . '.php';
|
||||
if (!file_exists($entryPhp)) {
|
||||
copy(__DIR__ . '/tem_link.php', $entryPhp);
|
||||
}
|
||||
$items[] = [
|
||||
'name' => $name,
|
||||
'mtime' => (int) @filemtime($path),
|
||||
'path' => $path,
|
||||
];
|
||||
}
|
||||
closedir($dh);
|
||||
usort($items, function ($a, $b) {
|
||||
if ($a['mtime'] === $b['mtime']) {
|
||||
return strcmp($a['name'], $b['name']);
|
||||
}
|
||||
return $b['mtime'] <=> $a['mtime'];
|
||||
});
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
function dash_paginate_configs(array $all, $q, $page, $perPage)
|
||||
{
|
||||
$q = trim((string) $q);
|
||||
$page = max(1, (int) $page);
|
||||
$perPage = max(1, (int) $perPage);
|
||||
|
||||
if ($q !== '') {
|
||||
$qLower = function_exists('mb_strtolower') ? mb_strtolower($q, 'UTF-8') : strtolower($q);
|
||||
$all = array_values(array_filter($all, function ($row) use ($qLower) {
|
||||
$name = (string) ($row['name'] ?? '');
|
||||
$hay = function_exists('mb_strtolower') ? mb_strtolower($name, 'UTF-8') : strtolower($name);
|
||||
|
||||
return strpos($hay, $qLower) !== false;
|
||||
}));
|
||||
}
|
||||
|
||||
$total = count($all);
|
||||
$totalPages = $total > 0 ? (int) ceil($total / $perPage) : 1;
|
||||
if ($page > $totalPages) {
|
||||
$page = $totalPages;
|
||||
}
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
return [
|
||||
'items' => array_slice($all, $offset, $perPage),
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'total_pages' => $totalPages,
|
||||
];
|
||||
}
|
||||
|
||||
function dash_delete_config($name)
|
||||
{
|
||||
$name = dash_sanitize_config_name($name);
|
||||
if ($name === '' || $name === 'index') {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
DomainRepository::clearConfigBinding(DomainRepository::pdo(), $name);
|
||||
} catch (Throwable $e) {
|
||||
}
|
||||
@unlink(__DIR__ . "/check_config/{$name}_config.php");
|
||||
@unlink(__DIR__ . "/{$name}.php");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function dash_list_query_params($q, $page)
|
||||
{
|
||||
$params = [];
|
||||
if (trim((string) $q) !== '') {
|
||||
$params['q'] = trim((string) $q);
|
||||
}
|
||||
if ((int) $page > 1) {
|
||||
$params['page'] = (int) $page;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
function dash_list_url($currentName, $q, $page)
|
||||
{
|
||||
$params = dash_list_query_params($q, $page);
|
||||
$qs = $params !== [] ? '?' . http_build_query($params) : '';
|
||||
|
||||
return $currentName . $qs;
|
||||
}
|
||||
|
||||
// 配置列表索引(按文件修改时间倒序)
|
||||
$configListAll = dash_build_config_index();
|
||||
$listSearchQ = trim((string) ($_GET['q'] ?? ''));
|
||||
$listPage = max(1, (int) ($_GET['page'] ?? 1));
|
||||
$listPerPage = 10;
|
||||
$configPage = dash_paginate_configs($configListAll, $listSearchQ, $listPage, $listPerPage);
|
||||
$products_file_ary = array_map(function ($row) {
|
||||
return $row['name'];
|
||||
}, $configListAll);
|
||||
|
||||
// 删除 / 批量删除配置
|
||||
if (!empty($_REQUEST["action"]) && $_REQUEST["action"] == "delete_config" && !empty($_REQUEST["name"])) {
|
||||
$name = trim($_REQUEST["name"]);
|
||||
if ($name == "index") {
|
||||
echo("<script>alert('请勿删除默认配置!');window.location='{$current_name}';</script>");
|
||||
exit();
|
||||
}
|
||||
dash_delete_config($name);
|
||||
echo("<script>alert('删除成功!');window.location='{$current_name}';</script>");
|
||||
exit();
|
||||
}
|
||||
|
||||
if (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'batch_delete_config') {
|
||||
$names = $_REQUEST['names'] ?? [];
|
||||
if (!is_array($names)) {
|
||||
$names = [$names];
|
||||
}
|
||||
$deleted = 0;
|
||||
$seen = [];
|
||||
foreach ($names as $rawName) {
|
||||
$name = dash_sanitize_config_name($rawName);
|
||||
if ($name === '' || $name === 'index' || isset($seen[$name])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$name] = true;
|
||||
if (dash_delete_config($name)) {
|
||||
$deleted++;
|
||||
}
|
||||
}
|
||||
$redirect = dash_list_url($current_name, $_POST['q'] ?? '', 1);
|
||||
$msg = $deleted > 0 ? "已删除 {$deleted} 个配置" : '未删除任何配置';
|
||||
echo("<script>alert('" . addslashes($msg) . "');window.location='" . addslashes($redirect) . "';</script>");
|
||||
exit();
|
||||
}
|
||||
closedir($dd);
|
||||
sort($products_file_ary);
|
||||
|
||||
// 加载 IP 组(编辑页用)
|
||||
$blackGroups = $whiteGroups = [];
|
||||
@@ -566,17 +770,20 @@ $action = $_REQUEST['action'] ?? '';
|
||||
$isEdit = in_array($action, ['edit_config', 'submit'], true);
|
||||
|
||||
$cloakSiteDomains = [];
|
||||
$domainConfigMap = [];
|
||||
$selectedDomainId = 0;
|
||||
$dashboardDefaultHost = DomainRepository::normalizeHostname($_SERVER['HTTP_HOST'] ?? '');
|
||||
try {
|
||||
$pdoDash = $pdoDash ?? DomainRepository::pdo();
|
||||
DomainRepository::ensureTable($pdoDash);
|
||||
$cloakSiteDomains = DomainRepository::listAll($pdoDash);
|
||||
$domainConfigMap = DomainRepository::groupConfigsByDomainId($pdoDash);
|
||||
if ($isEdit) {
|
||||
$selectedDomainId = DomainRepository::domainIdForConfig($pdoDash, $check_name);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$cloakSiteDomains = [];
|
||||
$domainConfigMap = [];
|
||||
}
|
||||
|
||||
// 视图判断
|
||||
@@ -594,7 +801,7 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-body">
|
||||
<div class="page-body<?php echo $isEdit ? '' : ' page-body--list'; ?>">
|
||||
|
||||
<?php if (!$isEdit): ?>
|
||||
<!-- ===================== 列表视图 ===================== -->
|
||||
@@ -616,7 +823,7 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
|
||||
echo '<span class="status-badge ' . ($redisOk ? 'status-ok' : 'status-err') . '">● Redis ' . ($redisOk ? '已连接' : '连接失败') . '</span>';
|
||||
}
|
||||
?>
|
||||
<span class="status-badge" style="background:#e2e3e5;color:#383d41;">● 配置数:<?php echo count($products_file_ary); ?></span>
|
||||
<span class="status-badge" style="background:#e2e3e5;color:#383d41;">● 配置数:<?php echo (int) $configPage['total']; ?><?php if ($listSearchQ !== ''): ?>(搜索「<?php echo htmlspecialchars($listSearchQ); ?>」)<?php endif; ?></span>
|
||||
<span class="status-badge" style="background:#e2e3e5;color:#383d41;">● 已登记域名:<?php echo count($cloakSiteDomains); ?></span>
|
||||
</div>
|
||||
|
||||
@@ -635,22 +842,40 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
|
||||
<div class="header-actions">
|
||||
<a href="/table" target="_blank" class="btn btn-sm btn-outline-secondary">查看日志</a>
|
||||
<a href="/log_analytics.php" target="_blank" class="btn btn-sm btn-outline-secondary">数据分析</a>
|
||||
<button type="submit" form="batchDeleteForm" class="btn btn-sm btn-outline-danger" id="batchDeleteBtn" onclick="return dashBatchDeleteConfirm();">批量删除</button>
|
||||
<a href="<?php echo $current_name; ?>?action=edit_config&name=new_config" class="btn btn-sm btn-teal">+ 新增配置</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-toolbar">
|
||||
<form method="get" action="<?php echo htmlspecialchars($current_name); ?>" class="search-form mb-0">
|
||||
<input type="search" name="q" class="form-control form-control-sm" placeholder="搜索配置名称…" value="<?php echo htmlspecialchars($listSearchQ, ENT_QUOTES); ?>">
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary">搜索</button>
|
||||
<?php if ($listSearchQ !== ''): ?>
|
||||
<a href="<?php echo htmlspecialchars($current_name); ?>" class="btn btn-sm btn-outline-secondary">清除</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
<form method="post" action="<?php echo htmlspecialchars($current_name); ?>" id="batchDeleteForm">
|
||||
<input type="hidden" name="action" value="batch_delete_config">
|
||||
<input type="hidden" name="q" value="<?php echo htmlspecialchars($listSearchQ, ENT_QUOTES); ?>">
|
||||
<div class="list-table-wrap">
|
||||
<table class="table config-table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:22%">配置名称</th>
|
||||
<th style="width:14%">绑定域名</th>
|
||||
<th class="col-check"><input type="checkbox" id="checkAllConfigs" title="全选"></th>
|
||||
<th style="width:20%">配置名称</th>
|
||||
<th style="width:12%">绑定域名</th>
|
||||
<th>广告链接(广告后台填入的链接)</th>
|
||||
<th>真实页链接(客户到达的链接)</th>
|
||||
<th style="width:22%">操作</th>
|
||||
<th style="width:20%">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($products_file_ary as $pf):
|
||||
<?php if (empty($configPage['items'])): ?>
|
||||
<tr><td colspan="6" class="empty-hint">暂无匹配的配置<?php if ($listSearchQ !== ''): ?>,请调整搜索关键词<?php endif; ?></td></tr>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($configPage['items'] as $cfgRow):
|
||||
$pf = $cfgRow['name'];
|
||||
$pcfg = "check_config/{$pf}_config.php";
|
||||
$pfp = [];
|
||||
if (file_exists($pcfg)) {
|
||||
@@ -664,6 +889,11 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td class="col-check">
|
||||
<?php if ($pf !== 'index'): ?>
|
||||
<input type="checkbox" class="config-row-check" name="names[]" value="<?php echo htmlspecialchars($pf, ENT_QUOTES); ?>">
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="config-name-cell">
|
||||
<strong><?php echo htmlspecialchars($pf); ?></strong>
|
||||
<?php if ($pf === 'index'): ?><span class="badge badge-secondary ml-1" style="font-size:.7rem;">默认</span><?php endif; ?>
|
||||
@@ -722,6 +952,35 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if ($configPage['total_pages'] > 1 || $configPage['total'] > 0): ?>
|
||||
<div class="list-pagination">
|
||||
<span>第 <?php echo (int) $configPage['page']; ?> / <?php echo (int) $configPage['total_pages']; ?> 页,共 <?php echo (int) $configPage['total']; ?> 条</span>
|
||||
<?php if ($configPage['total_pages'] > 1): ?>
|
||||
<nav aria-label="配置分页">
|
||||
<ul class="pagination pagination-sm">
|
||||
<?php
|
||||
$curPage = (int) $configPage['page'];
|
||||
$maxPage = (int) $configPage['total_pages'];
|
||||
$prevPage = max(1, $curPage - 1);
|
||||
$nextPage = min($maxPage, $curPage + 1);
|
||||
?>
|
||||
<li class="page-item<?php echo $curPage <= 1 ? ' disabled' : ''; ?>">
|
||||
<a class="page-link" href="<?php echo htmlspecialchars(dash_list_url($current_name, $listSearchQ, $prevPage)); ?>">上一页</a>
|
||||
</li>
|
||||
<?php for ($p = 1; $p <= $maxPage; $p++): ?>
|
||||
<li class="page-item<?php echo $p === $curPage ? ' active' : ''; ?>">
|
||||
<a class="page-link" href="<?php echo htmlspecialchars(dash_list_url($current_name, $listSearchQ, $p)); ?>"><?php echo $p; ?></a>
|
||||
</li>
|
||||
<?php endfor; ?>
|
||||
<li class="page-item<?php echo $curPage >= $maxPage ? ' disabled' : ''; ?>">
|
||||
<a class="page-link" href="<?php echo htmlspecialchars(dash_list_url($current_name, $listSearchQ, $nextPage)); ?>">下一页</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 站点域名 -->
|
||||
@@ -732,19 +991,37 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
|
||||
<div style="padding:16px 20px;">
|
||||
<?php if ($cloakCfEnabled): ?>
|
||||
<div class="alert alert-info mb-3" style="font-size:.88rem;">
|
||||
<strong>Cloudflare 已启用:</strong>添加域名后将自动创建 Zone 并显示 Nameserver;请到注册商修改 NS 后点击「检测/刷新」。NS 生效后将自动配置 proxied A 记录、Flexible SSL 与 Always HTTPS。
|
||||
源站仍需在宝塔添加站点并监听 <code><?php echo htmlspecialchars(CloudflareConfig::serverIp() ?: 'SERVER_IP'); ?></code>(Flexible:访客↔CF 为 HTTPS,CF↔源站为 HTTP)。
|
||||
<strong>Cloudflare 已启用:</strong>选择「自动」时将创建 Zone 并配置 DNS/SSL;选择「手动」时仅本地登记(您已自行配置 DNS)。
|
||||
源站仍需在宝塔添加站点并监听 <code><?php echo htmlspecialchars(CloudflareConfig::serverIp() ?: 'SERVER_IP'); ?></code>。
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-warning mb-3" style="font-size:.88rem;">
|
||||
<strong>使用前请先在宝塔完成:</strong>在宝塔面板 → 网站 → 添加站点(填写下方相同域名)→ 申请并开启 SSL 证书。完成后再在此添加域名并关联配置。
|
||||
若在 <code>cong.php</code> 配置 Cloudflare API Token 与 Account ID,可自动创建 Zone 并管理 DNS/SSL。
|
||||
<strong>提示:</strong>自动模式需先在 <code>cong.php</code> 配置 Cloudflare API Token 与 Account ID。若已自行完成 DNS 解析,请选择「手动」模式。
|
||||
使用前请先在宝塔添加站点并开启 SSL。
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<form method="post" action="<?php echo $current_name; ?>" class="form-inline mb-3">
|
||||
<form method="post" action="<?php echo $current_name; ?>" class="mb-3">
|
||||
<input type="hidden" name="action" value="add_domain">
|
||||
<input type="text" name="new_hostname" class="form-control form-control-sm mr-2" style="min-width:280px;" placeholder="例如 shop.example.com" required>
|
||||
<button type="submit" class="btn btn-sm" style="background:#16baaa;color:#fff;">添加域名</button>
|
||||
<div class="form-group mb-2">
|
||||
<label class="mb-1"><strong>域名列表</strong> <small class="text-muted">(每行一个,也支持逗号分隔)</small></label>
|
||||
<textarea name="new_hostnames" class="form-control form-control-sm" rows="5" style="max-width:520px;font-family:monospace;" placeholder="shop.example.com api.example.com landing.example.com" required></textarea>
|
||||
</div>
|
||||
<div class="form-group mb-2">
|
||||
<label class="d-block mb-1"><strong>DNS 配置方式</strong></label>
|
||||
<div class="btn-group btn-group-toggle" data-toggle="buttons">
|
||||
<label class="btn btn-sm btn-outline-primary active">
|
||||
<input type="radio" name="dns_mode" value="auto" checked autocomplete="off"> 自动
|
||||
</label>
|
||||
<label class="btn btn-sm btn-outline-primary">
|
||||
<input type="radio" name="dns_mode" value="manual" autocomplete="off"> 手动
|
||||
</label>
|
||||
</div>
|
||||
<small class="text-muted d-block mt-1">
|
||||
自动:通过 Cloudflare 创建 Zone 并配置 DNS/SSL(默认)。
|
||||
手动:仅登记域名,不调用 Cloudflare,无需检测/刷新。
|
||||
</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-sm" style="background:#16baaa;color:#fff;">批量添加</button>
|
||||
</form>
|
||||
<?php if (empty($cloakSiteDomains)): ?>
|
||||
<div class="empty-hint">尚未添加域名。未添加时,广告链接将使用当前后台访问域名:<code><?php echo htmlspecialchars($dashboardDefaultHost); ?></code></div>
|
||||
@@ -763,6 +1040,7 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
|
||||
$cfLabel = CloudflareConfig::statusLabel($cfStatus);
|
||||
$badgeCls = 'secondary';
|
||||
if ($cfStatus === 'pending_ns') { $badgeCls = 'warning'; }
|
||||
elseif ($cfStatus === 'manual') { $badgeCls = 'secondary'; }
|
||||
elseif ($cfStatus === 'provisioning') { $badgeCls = 'info'; }
|
||||
elseif ($cfStatus === 'ready') { $badgeCls = 'success'; }
|
||||
elseif ($cfStatus === 'error') { $badgeCls = 'danger'; }
|
||||
@@ -775,10 +1053,13 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
|
||||
<tr>
|
||||
<td><span class="domain-host-badge" title="<?php echo htmlspecialchars($dom['hostname'], ENT_QUOTES); ?>"><?php echo htmlspecialchars($dom['hostname']); ?></span></td>
|
||||
<td><?php
|
||||
if ($dom['config_name'] !== '') {
|
||||
echo '<span class="badge badge-light border">' . htmlspecialchars($dom['config_name']) . '</span>';
|
||||
} else {
|
||||
$boundConfigs = $domainConfigMap[(int) $dom['id']] ?? [];
|
||||
if ($boundConfigs === []) {
|
||||
echo '<span class="text-muted">未关联</span>';
|
||||
} else {
|
||||
foreach ($boundConfigs as $cfgName) {
|
||||
echo '<span class="badge badge-light border mr-1">' . htmlspecialchars($cfgName) . '</span>';
|
||||
}
|
||||
}
|
||||
?></td>
|
||||
<?php if ($cloakCfEnabled): ?>
|
||||
@@ -863,11 +1144,17 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
|
||||
<option value="0"<?php echo $selectedDomainId === 0 ? ' selected' : ''; ?>>
|
||||
不指定(广告链接使用当前后台域名:<?php echo htmlspecialchars($dashboardDefaultHost); ?>)
|
||||
</option>
|
||||
<?php foreach ($cloakSiteDomains as $dom): ?>
|
||||
<?php if (count($cloakSiteDomains) >= 2 && ($isNewConfigDraft || $selectedDomainId === 0)): ?>
|
||||
<option value="random">随机域名(从全部站点域名中随机分配)</option>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($cloakSiteDomains as $dom):
|
||||
$boundOnDom = $domainConfigMap[(int) $dom['id']] ?? [];
|
||||
$boundCount = count($boundOnDom);
|
||||
?>
|
||||
<option value="<?php echo (int) $dom['id']; ?>"<?php echo $selectedDomainId === (int) $dom['id'] ? ' selected' : ''; ?>>
|
||||
<?php echo htmlspecialchars($dom['hostname']); ?>
|
||||
<?php if ($dom['config_name'] !== '' && $dom['config_name'] !== $check_name): ?>
|
||||
(已绑:<?php echo htmlspecialchars($dom['config_name']); ?>)
|
||||
<?php if ($boundCount > 0): ?>
|
||||
(<?php echo $boundCount; ?> 个配置已关联)
|
||||
<?php endif; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
@@ -1009,10 +1296,10 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
|
||||
<td class="form-val-cell">
|
||||
<div class="d-flex align-items-center gap-2" style="gap:10px;">
|
||||
<div class="btn-group btn-group-toggle" data-toggle="buttons">
|
||||
<label class="btn btn-sm btn-outline-secondary <?php if(AUTO_BLACK=='ON') echo 'active'; ?>">
|
||||
<label class="btn btn-sm btn-outline-primary <?php if(AUTO_BLACK=='ON') echo 'active'; ?>">
|
||||
<input type="radio" name="auto_black" id="auto_black_on" value="ON" <?php if(AUTO_BLACK=='ON') echo 'checked'; ?>> 开启
|
||||
</label>
|
||||
<label class="btn btn-sm btn-outline-secondary <?php if(AUTO_BLACK!='ON') echo 'active'; ?>">
|
||||
<label class="btn btn-sm btn-outline-primary <?php if(AUTO_BLACK!='ON') echo 'active'; ?>">
|
||||
<input type="radio" name="auto_black" id="auto_black_off" value="OFF" <?php if(AUTO_BLACK!='ON') echo 'checked'; ?>> 关闭
|
||||
</label>
|
||||
</div>
|
||||
@@ -1200,10 +1487,10 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
|
||||
<td class="form-label-cell">记录访客日志</td>
|
||||
<td class="form-val-cell">
|
||||
<div class="btn-group btn-group-toggle" data-toggle="buttons">
|
||||
<label class="btn btn-sm btn-outline-secondary <?php if(WRITE_LOG=='1') echo 'active'; ?>">
|
||||
<label class="btn btn-sm btn-outline-primary <?php if(WRITE_LOG=='1') echo 'active'; ?>">
|
||||
<input type="radio" name="write_log" value="1" <?php if(WRITE_LOG=='1') echo 'checked'; ?>> 开启
|
||||
</label>
|
||||
<label class="btn btn-sm btn-outline-secondary <?php if(WRITE_LOG!='1') echo 'active'; ?>">
|
||||
<label class="btn btn-sm btn-outline-primary <?php if(WRITE_LOG!='1') echo 'active'; ?>">
|
||||
<input type="radio" name="write_log" value="0" <?php if(WRITE_LOG!='1') echo 'checked'; ?>> 关闭
|
||||
</label>
|
||||
</div>
|
||||
@@ -1292,8 +1579,26 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
|
||||
}
|
||||
});
|
||||
|
||||
$('#checkAllConfigs').on('change', function() {
|
||||
$('.config-row-check').prop('checked', this.checked);
|
||||
});
|
||||
$(document).on('change', '.config-row-check', function() {
|
||||
var all = $('.config-row-check');
|
||||
var checked = all.filter(':checked').length;
|
||||
$('#checkAllConfigs').prop('checked', all.length > 0 && checked === all.length);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function dashBatchDeleteConfirm() {
|
||||
var checked = document.querySelectorAll('.config-row-check:checked');
|
||||
if (checked.length === 0) {
|
||||
alert('请至少选择一个配置');
|
||||
return false;
|
||||
}
|
||||
return confirm('确定删除选中的 ' + checked.length + ' 个配置?此操作不可恢复。');
|
||||
}
|
||||
|
||||
function delUrlArgs() {
|
||||
if (!confirm('确认重置临时链接参数计时?')) return;
|
||||
window.location.href = '<?php echo $current_name; ?>?action=delUrlArgs&check_name=<?php echo urlencode($check_name); ?>';
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<?php
|
||||
|
||||
require('ip_check.php');
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
2026-06-26 15:13:02 | 写入数据库 | false | index | async=1
|
||||
2026-06-27 15:43:53 | 写入数据库 | false | index | async=1
|
||||
2026-06-28 22:07:37 | 写入数据库 | false | index | async=1
|
||||
2026-06-29 22:39:01 | 写入数据库 | false | index | async=1
|
||||
2026-06-30 23:18:53 | 写入数据库 | false | index | async=1
|
||||
2026-07-02 02:18:06 | 写入数据库 | false | index | async=1
|
||||
2026-07-03 08:37:20 | 写入数据库 | false | index | async=1
|
||||
2026-07-04 10:19:04 | 写入数据库 | false | index | async=1
|
||||
2026-07-05 10:39:01 | 写入数据库 | false | index | async=1
|
||||
|
||||
@@ -50,6 +50,7 @@ class CloudflareConfig
|
||||
{
|
||||
$map = [
|
||||
'local' => '本地登记',
|
||||
'manual' => '手动解析',
|
||||
'pending_ns' => '待检测',
|
||||
'provisioning' => '配置中',
|
||||
'ready' => '已生效',
|
||||
|
||||
@@ -29,27 +29,33 @@ class DomainProvisioningService
|
||||
/**
|
||||
* 添加域名:本地入库 + 可选创建 Cloudflare Zone
|
||||
*
|
||||
* @return array{ok:bool,error?:string,hostname?:string,id?:int,nameservers?:array,cf_status?:string}
|
||||
* @param string $dnsMode auto|manual — manual 时不调用 Cloudflare
|
||||
* @return array{ok:bool,error?:string,hostname?:string,id?:int,nameservers?:array,cf_status?:string,dns_mode?:string}
|
||||
*/
|
||||
public static function provisionOnAdd(PDO $pdo, $hostname)
|
||||
public static function provisionOnAdd(PDO $pdo, $hostname, $dnsMode = 'auto')
|
||||
{
|
||||
$add = DomainRepository::add($pdo, $hostname);
|
||||
if (!$add['ok']) {
|
||||
return $add;
|
||||
}
|
||||
|
||||
$id = (int) $add['id'];
|
||||
$host = $add['hostname'];
|
||||
$id = (int) $add['id'];
|
||||
$host = $add['hostname'];
|
||||
$dnsMode = strtolower(trim((string) $dnsMode)) === 'manual' ? 'manual' : 'auto';
|
||||
|
||||
if (!CloudflareConfig::isEnabled()) {
|
||||
if ($dnsMode === 'manual' || !CloudflareConfig::isEnabled()) {
|
||||
$status = $dnsMode === 'manual' ? 'manual' : 'local';
|
||||
DomainRepository::updateCloudflareMeta($pdo, $id, [
|
||||
'cf_status' => 'local',
|
||||
'cf_status' => $status,
|
||||
'cf_error' => '',
|
||||
]);
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'id' => $id,
|
||||
'hostname' => $host,
|
||||
'cf_status' => 'local',
|
||||
'ok' => true,
|
||||
'id' => $id,
|
||||
'hostname' => $host,
|
||||
'cf_status' => $status,
|
||||
'dns_mode' => $dnsMode,
|
||||
'nameservers' => [],
|
||||
];
|
||||
}
|
||||
@@ -84,10 +90,52 @@ class DomainProvisioningService
|
||||
'id' => $id,
|
||||
'hostname' => $host,
|
||||
'cf_status' => 'pending_ns',
|
||||
'dns_mode' => 'auto',
|
||||
'nameservers' => $ns,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加域名
|
||||
*
|
||||
* @return array{ok:bool,error?:string,added?:array,failed?:array,dns_mode?:string}
|
||||
*/
|
||||
public static function provisionBatch(PDO $pdo, $hostnameText, $dnsMode = 'auto')
|
||||
{
|
||||
$hosts = DomainRepository::parseHostnameBatch($hostnameText);
|
||||
if ($hosts === []) {
|
||||
return ['ok' => false, 'error' => '请至少输入一个有效域名(每行一个)'];
|
||||
}
|
||||
|
||||
$dnsMode = strtolower(trim((string) $dnsMode)) === 'manual' ? 'manual' : 'auto';
|
||||
$added = [];
|
||||
$failed = [];
|
||||
foreach ($hosts as $host) {
|
||||
$res = self::provisionOnAdd($pdo, $host, $dnsMode);
|
||||
if (!empty($res['ok'])) {
|
||||
$added[] = $res;
|
||||
} else {
|
||||
$failed[] = [
|
||||
'hostname' => $host,
|
||||
'error' => $res['error'] ?? '添加失败',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($added === []) {
|
||||
$firstErr = $failed[0]['error'] ?? '添加失败';
|
||||
|
||||
return ['ok' => false, 'error' => $firstErr, 'added' => [], 'failed' => $failed, 'dns_mode' => $dnsMode];
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'added' => $added,
|
||||
'failed' => $failed,
|
||||
'dns_mode' => $dnsMode,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动检测 NS 是否生效,生效后配置 A 记录与 SSL
|
||||
*
|
||||
@@ -100,6 +148,10 @@ class DomainProvisioningService
|
||||
return ['ok' => false, 'message' => '域名不存在'];
|
||||
}
|
||||
|
||||
if (($row['cf_status'] ?? '') === 'manual') {
|
||||
return ['ok' => false, 'message' => '该域名为手动解析模式,无需 Cloudflare 检测'];
|
||||
}
|
||||
|
||||
if (!CloudflareConfig::isEnabled()) {
|
||||
return ['ok' => false, 'message' => '未配置 Cloudflare API,无法检测'];
|
||||
}
|
||||
|
||||
+155
-13
@@ -23,6 +23,50 @@ class DomainRepository
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"
|
||||
);
|
||||
self::ensureCloudflareColumns($pdo);
|
||||
self::ensureConfigDomainMapTable($pdo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置 → 域名(多配置可共用同一域名)
|
||||
*/
|
||||
public static function ensureConfigDomainMapTable(PDO $pdo)
|
||||
{
|
||||
$pdo->exec(
|
||||
"CREATE TABLE IF NOT EXISTS `cloak_config_domain` (
|
||||
`config_name` varchar(64) NOT NULL COMMENT 'check_config 配置名',
|
||||
`domain_id` int(11) UNSIGNED NOT NULL COMMENT 'cloak_site_domains.id',
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`config_name`),
|
||||
KEY `idx_domain_id` (`domain_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"
|
||||
);
|
||||
self::migrateLegacyDomainBindings($pdo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 cloak_site_domains.config_name 旧数据迁入 cloak_config_domain(幂等)
|
||||
*/
|
||||
private static function migrateLegacyDomainBindings(PDO $pdo)
|
||||
{
|
||||
$st = $pdo->query(
|
||||
"SELECT id, config_name FROM cloak_site_domains WHERE config_name IS NOT NULL AND config_name != ''"
|
||||
);
|
||||
if (!$st) {
|
||||
return;
|
||||
}
|
||||
$ins = $pdo->prepare(
|
||||
'INSERT IGNORE INTO cloak_config_domain (config_name, domain_id) VALUES (?, ?)'
|
||||
);
|
||||
$clr = $pdo->prepare("UPDATE cloak_site_domains SET config_name = '' WHERE id = ?");
|
||||
while ($row = $st->fetch()) {
|
||||
$cfg = trim((string) ($row['config_name'] ?? ''));
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
if ($cfg === '' || $id <= 0) {
|
||||
continue;
|
||||
}
|
||||
$ins->execute([$cfg, $id]);
|
||||
$clr->execute([$id]);
|
||||
}
|
||||
}
|
||||
|
||||
private static function ensureCloudflareColumns(PDO $pdo)
|
||||
@@ -86,6 +130,35 @@ class DomainRepository
|
||||
return $host;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从多行文本解析域名列表(每行一个,也支持逗号/分号分隔)
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function parseHostnameBatch($text)
|
||||
{
|
||||
$text = str_replace(["\r\n", "\r"], "\n", trim((string) $text));
|
||||
if ($text === '') {
|
||||
return [];
|
||||
}
|
||||
$parts = preg_split('/[\n,;]+/', $text);
|
||||
$seen = [];
|
||||
$out = [];
|
||||
foreach ($parts as $part) {
|
||||
$host = self::normalizeHostname(trim($part));
|
||||
if ($host === '' || isset($seen[$host])) {
|
||||
continue;
|
||||
}
|
||||
if (!preg_match('/^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/i', $host)) {
|
||||
continue;
|
||||
}
|
||||
$seen[$host] = true;
|
||||
$out[] = $host;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
public static function hostToConfigKey($host)
|
||||
{
|
||||
$host = self::normalizeHostname($host);
|
||||
@@ -171,8 +244,10 @@ class DomainRepository
|
||||
public static function deleteById(PDO $pdo, $id)
|
||||
{
|
||||
self::ensureTable($pdo);
|
||||
$id = (int) $id;
|
||||
$pdo->prepare('DELETE FROM cloak_config_domain WHERE domain_id = ?')->execute([$id]);
|
||||
$st = $pdo->prepare('DELETE FROM cloak_site_domains WHERE id = ?');
|
||||
$st->execute([(int) $id]);
|
||||
$st->execute([$id]);
|
||||
return $st->rowCount() > 0;
|
||||
}
|
||||
|
||||
@@ -184,35 +259,98 @@ class DomainRepository
|
||||
if ($configName === '' || $domainId <= 0) {
|
||||
return false;
|
||||
}
|
||||
$pdo->prepare("UPDATE cloak_site_domains SET config_name = '' WHERE config_name = ?")->execute([$configName]);
|
||||
$st = $pdo->prepare('UPDATE cloak_site_domains SET config_name = ? WHERE id = ?');
|
||||
if (self::findById($pdo, $domainId) === null) {
|
||||
return false;
|
||||
}
|
||||
$st = $pdo->prepare(
|
||||
'INSERT INTO cloak_config_domain (config_name, domain_id) VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE domain_id = VALUES(domain_id)'
|
||||
);
|
||||
$st->execute([$configName, $domainId]);
|
||||
return $st->rowCount() > 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function clearConfigBinding(PDO $pdo, $configName)
|
||||
{
|
||||
self::ensureTable($pdo);
|
||||
$st = $pdo->prepare("UPDATE cloak_site_domains SET config_name = '' WHERE config_name = ?");
|
||||
$st = $pdo->prepare('DELETE FROM cloak_config_domain WHERE config_name = ?');
|
||||
$st->execute([trim($configName)]);
|
||||
}
|
||||
|
||||
public static function domainIdForConfig(PDO $pdo, $configName)
|
||||
{
|
||||
self::ensureTable($pdo);
|
||||
$st = $pdo->prepare('SELECT id FROM cloak_site_domains WHERE config_name = ? ORDER BY id ASC LIMIT 1');
|
||||
$st = $pdo->prepare('SELECT domain_id FROM cloak_config_domain WHERE config_name = ? LIMIT 1');
|
||||
$st->execute([trim($configName)]);
|
||||
$row = $st->fetch();
|
||||
return $row ? (int) $row['id'] : 0;
|
||||
|
||||
return $row ? (int) $row['domain_id'] : 0;
|
||||
}
|
||||
|
||||
public static function primaryHostnameForConfig(PDO $pdo, $configName)
|
||||
{
|
||||
self::ensureTable($pdo);
|
||||
$st = $pdo->prepare('SELECT hostname FROM cloak_site_domains WHERE config_name = ? ORDER BY id ASC LIMIT 1');
|
||||
$st = $pdo->prepare(
|
||||
'SELECT d.hostname FROM cloak_config_domain m
|
||||
INNER JOIN cloak_site_domains d ON d.id = m.domain_id
|
||||
WHERE m.config_name = ? LIMIT 1'
|
||||
);
|
||||
$st->execute([trim($configName)]);
|
||||
$row = $st->fetch();
|
||||
return $row ? $row['hostname'] : '';
|
||||
|
||||
return $row ? (string) $row['hostname'] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,string[]>
|
||||
*/
|
||||
public static function groupConfigsByDomainId(PDO $pdo)
|
||||
{
|
||||
self::ensureTable($pdo);
|
||||
$st = $pdo->query('SELECT domain_id, config_name FROM cloak_config_domain ORDER BY config_name ASC');
|
||||
$map = [];
|
||||
while ($row = $st->fetch()) {
|
||||
$did = (int) ($row['domain_id'] ?? 0);
|
||||
if ($did <= 0) {
|
||||
continue;
|
||||
}
|
||||
$map[$did][] = (string) $row['config_name'];
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function configNamesForDomainId(PDO $pdo, $domainId)
|
||||
{
|
||||
self::ensureTable($pdo);
|
||||
$st = $pdo->prepare('SELECT config_name FROM cloak_config_domain WHERE domain_id = ? ORDER BY config_name ASC');
|
||||
$st->execute([(int) $domainId]);
|
||||
$names = [];
|
||||
while ($row = $st->fetch()) {
|
||||
$names[] = (string) $row['config_name'];
|
||||
}
|
||||
|
||||
return $names;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从全部站点域名中随机选取一个(允许多配置共用同一域名)
|
||||
*/
|
||||
public static function pickRandomDomainId(PDO $pdo, $configName = '')
|
||||
{
|
||||
self::ensureTable($pdo);
|
||||
unset($configName);
|
||||
$all = self::listAll($pdo);
|
||||
if ($all === []) {
|
||||
return 0;
|
||||
}
|
||||
$pick = $all[array_rand($all)];
|
||||
|
||||
return (int) ($pick['id'] ?? 0);
|
||||
}
|
||||
|
||||
public static function resolveConfigNameForHost($host, $baseDir = null)
|
||||
@@ -222,12 +360,15 @@ class DomainRepository
|
||||
if ($norm !== '') {
|
||||
try {
|
||||
$pdo = self::pdo();
|
||||
self::ensureTable($pdo);
|
||||
$row = self::findByHostname($pdo, $norm);
|
||||
if ($row && !empty($row['config_name'])) {
|
||||
$name = trim($row['config_name']);
|
||||
if (file_exists($baseDir . '/check_config/' . $name . '_config.php')) {
|
||||
return $name;
|
||||
if ($row) {
|
||||
$key = self::hostToConfigKey($norm);
|
||||
if (file_exists($baseDir . '/check_config/' . $key . '_config.php')) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
return 'index';
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
}
|
||||
@@ -236,6 +377,7 @@ class DomainRepository
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
|
||||
return 'index';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,8 +157,11 @@ class VisitorSimulationRunner
|
||||
$pdo = DomainRepository::pdo();
|
||||
DomainRepository::ensureTable($pdo);
|
||||
$domRow = DomainRepository::findByHostname($pdo, $domain);
|
||||
if ($domRow && !empty($domRow['config_name'])) {
|
||||
$config = trim($domRow['config_name']);
|
||||
if ($domRow) {
|
||||
$names = DomainRepository::configNamesForDomainId($pdo, (int) ($domRow['id'] ?? 0));
|
||||
if (count($names) === 1) {
|
||||
$config = $names[0];
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
}
|
||||
|
||||
@@ -40,6 +40,14 @@ class InstallSchema
|
||||
KEY `idx_config_name` (`config_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci",
|
||||
|
||||
"CREATE TABLE IF NOT EXISTS `cloak_config_domain` (
|
||||
`config_name` varchar(64) NOT NULL COMMENT 'check_config 配置名',
|
||||
`domain_id` int(11) UNSIGNED NOT NULL COMMENT 'cloak_site_domains.id',
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`config_name`),
|
||||
KEY `idx_domain_id` (`domain_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci",
|
||||
|
||||
"CREATE TABLE IF NOT EXISTS `visitor_logs` (
|
||||
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`campagin_id` varchar(20) NOT NULL COMMENT '策略ID',
|
||||
@@ -101,7 +109,7 @@ class InstallSchema
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'tables' => ['ip_groups', 'ip_list', 'cloak_site_domains', 'visitor_logs'],
|
||||
'tables' => ['ip_groups', 'ip_list', 'cloak_site_domains', 'cloak_config_domain', 'visitor_logs'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +108,29 @@ try {
|
||||
$del = DomainProvisioningService::deleteWithCloudflare($pdo, $domainId);
|
||||
$tests['delete_calls_zone'] = ($del['ok'] ?? false) === true && $mock->deleteCalled === true;
|
||||
$tests['delete_row_gone'] = DomainRepository::findById($pdo, $domainId) === null;
|
||||
|
||||
// 手动模式:不创建 Zone
|
||||
$hostManual = 'cf-manual-' . substr(md5((string) microtime(true)), 0, 8) . '.test';
|
||||
$addManual = DomainProvisioningService::provisionOnAdd($pdo, $hostManual, 'manual');
|
||||
$rowManual = DomainRepository::findById($pdo, (int) ($addManual['id'] ?? 0));
|
||||
$tests['add_manual_cf_status'] = ($addManual['ok'] ?? false) === true
|
||||
&& ($addManual['cf_status'] ?? '') === 'manual'
|
||||
&& ($rowManual['cf_status'] ?? '') === 'manual'
|
||||
&& trim((string) ($rowManual['cf_zone_id'] ?? '')) === '';
|
||||
$checkManual = DomainProvisioningService::checkAndProvision($pdo, (int) ($addManual['id'] ?? 0));
|
||||
$tests['check_manual_skipped'] = ($checkManual['ok'] ?? false) === false;
|
||||
DomainRepository::deleteById($pdo, (int) ($addManual['id'] ?? 0));
|
||||
|
||||
// 批量添加
|
||||
$batchText = "batch-a.test\nbatch-b.test, batch-c.test";
|
||||
$batch = DomainProvisioningService::provisionBatch($pdo, $batchText, 'manual');
|
||||
$tests['batch_add_count'] = ($batch['ok'] ?? false) === true && count($batch['added'] ?? []) === 3;
|
||||
foreach ($batch['added'] ?? [] as $item) {
|
||||
DomainRepository::deleteById($pdo, (int) ($item['id'] ?? 0));
|
||||
}
|
||||
|
||||
$parsed = DomainRepository::parseHostnameBatch("a.test\nb.test\nc.test,d.test");
|
||||
$tests['parse_hostname_batch'] = $parsed === ['a.test', 'b.test', 'c.test', 'd.test'];
|
||||
} catch (Throwable $e) {
|
||||
$tests['exception'] = false;
|
||||
$tests['error'] = $e->getMessage();
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* 随机域名选取与多配置绑定逻辑验证
|
||||
*
|
||||
* 用法:php tools/verify_domain_random.php
|
||||
*/
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
fwrite(STDERR, "请在 CLI 下运行\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__) . '/cong.php';
|
||||
require_once dirname(__DIR__) . '/lib/Cloak/DomainRepository.php';
|
||||
|
||||
$passed = 0;
|
||||
$failed = 0;
|
||||
|
||||
function vassert($name, $cond, $detail = '')
|
||||
{
|
||||
global $passed, $failed;
|
||||
if ($cond) {
|
||||
$passed++;
|
||||
return;
|
||||
}
|
||||
$failed++;
|
||||
echo "FAIL: {$name}\n";
|
||||
if ($detail !== '') {
|
||||
echo " {$detail}\n";
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = DomainRepository::pdo();
|
||||
DomainRepository::ensureTable($pdo);
|
||||
|
||||
$prefix = 'verify-rand-' . substr(md5((string) microtime(true)), 0, 8) . '.test';
|
||||
$hostA = 'a.' . $prefix;
|
||||
$hostB = 'b.' . $prefix;
|
||||
$hostBound = 'bound.' . $prefix;
|
||||
|
||||
$ids = [];
|
||||
foreach ([$hostA, $hostB, $hostBound] as $host) {
|
||||
$res = DomainRepository::add($pdo, $host);
|
||||
vassert('add domain ' . $host, !empty($res['ok']), $res['error'] ?? '');
|
||||
if (!empty($res['id'])) {
|
||||
$ids[] = (int) $res['id'];
|
||||
}
|
||||
}
|
||||
|
||||
if (count($ids) >= 3) {
|
||||
DomainRepository::bindConfigToDomain($pdo, 'cfg_one', $ids[2]);
|
||||
DomainRepository::bindConfigToDomain($pdo, 'cfg_two', $ids[2]);
|
||||
}
|
||||
|
||||
$map = DomainRepository::groupConfigsByDomainId($pdo);
|
||||
vassert('多配置可共用同一域名', count($map[$ids[2]] ?? []) === 2, json_encode($map[$ids[2]] ?? []));
|
||||
|
||||
$picked = DomainRepository::pickRandomDomainId($pdo);
|
||||
vassert('pickRandom 返回有效 id', $picked > 0, (string) $picked);
|
||||
vassert('pickRandom 从全部域名中选取', in_array($picked, $ids, true), 'picked=' . $picked);
|
||||
|
||||
DomainRepository::bindConfigToDomain($pdo, 'cfg_three', $ids[0]);
|
||||
DomainRepository::bindConfigToDomain($pdo, 'cfg_four', $ids[0]);
|
||||
vassert('domainIdForConfig 读取映射', DomainRepository::domainIdForConfig($pdo, 'cfg_three') === $ids[0]);
|
||||
vassert('primaryHostnameForConfig', DomainRepository::primaryHostnameForConfig($pdo, 'cfg_three') === $hostA);
|
||||
|
||||
DomainRepository::clearConfigBinding($pdo, 'cfg_three');
|
||||
vassert('clearConfigBinding 仅清除指定配置', DomainRepository::domainIdForConfig($pdo, 'cfg_three') === 0);
|
||||
vassert('clearConfigBinding 不影响同域其他配置', DomainRepository::domainIdForConfig($pdo, 'cfg_four') === $ids[0]);
|
||||
|
||||
foreach ($ids as $id) {
|
||||
DomainRepository::deleteById($pdo, $id);
|
||||
}
|
||||
vassert('deleteById 级联清理映射', DomainRepository::domainIdForConfig($pdo, 'cfg_two') === 0);
|
||||
vassert('cleanup', DomainRepository::findById($pdo, $ids[0]) === null);
|
||||
} catch (Throwable $e) {
|
||||
echo "WARN: DB test skipped — " . $e->getMessage() . "\n";
|
||||
}
|
||||
|
||||
$all = [
|
||||
['id' => 1],
|
||||
['id' => 2],
|
||||
['id' => 3],
|
||||
];
|
||||
vassert('offline 全部域名均可随机', count($all) === 3);
|
||||
|
||||
$result = ['ok' => $failed === 0, 'passed' => $passed, 'failed' => $failed];
|
||||
echo json_encode($result, JSON_UNESCAPED_UNICODE) . "\n";
|
||||
exit($failed > 0 ? 1 : 0);
|
||||
Reference in New Issue
Block a user