$title,
'status' => $status,
'detail' => $detail,
'file' => self::IP_CHECK_FILE,
'line' => (int)$line,
'extra' => $extra,
'at' => microtime(true),
];
}
/**
* 记录 check_result 写入(请从 ip_check.php 使用 cloak_dbg_record(__LINE__, ...) 调用)
*/
public static function record($result, $reason, array $extra = [])
{
$loc = self::callerLoc();
self::recordAt($loc['line'], $result, $reason, $extra);
}
/**
* 记录 check_result 写入,指定 ip_check.php 行号
*/
public static function recordAt($line, $result, $reason, array $extra = [])
{
$stage = isset($extra['stage']) ? (string)$extra['stage'] : 'unknown';
$entry = [
'result' => $result,
'reason' => $reason,
'file' => self::IP_CHECK_FILE,
'line' => (int)$line,
'stage' => $stage,
'extra' => $extra,
'at' => microtime(true),
];
self::$trail[] = $entry;
$decideDetail = $reason !== '' ? $reason : '(理由为空)';
if ($result === 'true' || $result === 'false') {
$decideDetail .= ' → check_result=' . $result;
self::$flags['final'] = $entry;
} elseif ($result === null) {
$decideDetail = ($reason !== '' ? $reason : '(理由为空)') . '(未写入 check_result,流程继续)';
}
self::$flow[] = [
'title' => self::stageTitle($stage),
'status' => ($result === 'true' || $result === 'false') ? 'decide' : 'warn',
'detail' => $decideDetail,
'file' => self::IP_CHECK_FILE,
'line' => (int)$line,
'extra' => array_merge($extra, ['check_result' => $result]),
'at' => microtime(true),
];
}
public static function setApiResponse(array $response)
{
self::$apiResponse = $response;
}
public static function setFlag($key, $value)
{
self::$flags[$key] = $value;
}
public static function setExitPoint($line, $label, array $extra = [])
{
self::$exitPoint = [
'file' => self::IP_CHECK_FILE,
'line' => (int)$line,
'label' => $label,
'extra' => $extra,
];
}
public static function renderAndExit(array $ctx)
{
$result = (string)($ctx['check_result'] ?? 'false');
$reason = (string)($ctx['reason'] ?? '');
$final = self::$flags['final'] ?? null;
$isRealPage = ($result === 'true');
$elapsed = isset($ctx['started_at'])
? round((microtime(true) - $ctx['started_at']) * 1000)
: round((microtime(true) - self::$startedAt) * 1000);
$routeInfo = self::buildRouteInfo($ctx, $result);
$diagnosis = self::buildReasonDiagnosis($reason, $result, $ctx);
$flowHtml = self::buildFlowHtml();
$trailHtml = self::buildTrailHtml();
$sessionHtml = self::buildSessionHtml($ctx['session'] ?? []);
$verdictLabel = $isRealPage ? '真实页 (DB_FP)' : '安全页 (DB_ZP)';
$verdictClass = $isRealPage ? 'verdict-pass' : 'verdict-block';
$verdictIcon = $isRealPage ? '✓' : '✕';
$finalLoc = $final ? self::formatLoc($final['file'], $final['line']) : '—';
$finalStage = $final ? htmlspecialchars(self::stageTitle($final['stage'])) : '—';
$finalReason = $final ? htmlspecialchars($final['reason'] !== '' ? $final['reason'] : '(空)') : '—';
$exit = self::$exitPoint;
$exitLoc = $exit ? self::formatLoc($exit['file'], $exit['line']) : self::IP_CHECK_FILE . ':?';
$exitLabel = $exit ? htmlspecialchars($exit['label']) : 'CloakDebugPage::renderAndExit()';
header('Content-Type: text/html; charset=utf-8');
header('X-Robots-Tag: noindex, nofollow');
echo '
';
echo '';
echo 'Cloak DEBUG — ' . htmlspecialchars($ctx['config_name'] ?? '') . '';
echo self::styles();
echo '';
echo '
';
// 流程终止位置(醒目)
echo '
';
echo '流程终止位置
';
echo '' . htmlspecialchars($exitLoc) . '
';
echo 'DEBUG 在此截停执行(' . $exitLabel . '),未继续跳转真实页/安全页。
';
echo '';
echo '- 未开启 DEBUG 时下一分支
- ' . htmlspecialchars($routeInfo['next_branch']) . '
';
echo '- 预计跳转目标
- ' . htmlspecialchars($routeInfo['dest'] !== '' ? $routeInfo['dest'] : '—') . '
';
echo '- 路由条件
' . htmlspecialchars($routeInfo['condition']) . ' ';
echo '
';
echo '
';
echo '' . $verdictIcon . '
';
echo '';
echo '
' . htmlspecialchars($verdictLabel) . '
';
echo '
check_result = ' . htmlspecialchars($result) . '
';
echo '
';
// 判断理由 + 空理由诊断
echo '
判定结论
';
echo '- 当前 $reason
- ';
if ($reason !== '') {
echo htmlspecialchars($reason);
} else {
echo '为空';
}
echo '
';
echo '- 最终写入 check_result
' . htmlspecialchars($finalLoc) . '(' . $finalStage . ') ';
echo '- 写入时 $reason
- ' . $finalReason . '
';
echo '
';
if ($diagnosis !== '') {
echo '' . $diagnosis . '
';
}
echo '';
// 完整判断流程
echo '
完整判断流程
';
echo '按 ip_check.php 实际执行顺序展示; 表示写入 check_result, 拦截, 跳过。
';
echo $flowHtml !== '' ? $flowHtml : '暂无流程记录(请确认 CLOAK_DEBUG_MODE 已保存为 ON)。
';
echo '';
if ($trailHtml !== '') {
echo '
check_result 写入轨迹
';
echo '每次对 $_SESSION[\'check_result\'] 的赋值;最后一项为最终判定依据。
';
echo $trailHtml . '';
}
if (self::$apiResponse) {
echo '
API 判定接口 (cloak_check_curl)
';
echo self::buildApiHtml(self::$apiResponse);
echo '';
}
echo '
';
echo '
Session 状态
';
echo $sessionHtml . '';
echo '
路由预览
';
echo '- SHOW_SITE_MODE
' . htmlspecialchars($ctx['mode'] ?? '') . ' ';
echo '- visit_to_2
' . htmlspecialchars($ctx['session']['visit_to_2'] ?? '—') . ' =2 时跳过 API 块 ';
echo '- visit_to_3
' . htmlspecialchars($ctx['session']['visit_to_3'] ?? '—') . ' ';
echo '
';
echo '
';
echo '
访客依据信息
';
echo '' . self::buildVisitorRows($ctx) . '
';
echo '
请求参数
';
echo '' . self::buildQueryRows() . '
';
echo '
配置快照
';
echo '' . self::buildConfigRows() . '
';
echo '';
echo '
';
exit;
}
private static function callerLoc()
{
$bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 20);
foreach ($bt as $frame) {
if (empty($frame['file'])) {
continue;
}
if (basename($frame['file']) === self::IP_CHECK_FILE) {
return [
'file' => self::IP_CHECK_FILE,
'line' => (int)($frame['line'] ?? 0),
];
}
}
return [
'file' => self::IP_CHECK_FILE,
'line' => 0,
];
}
private static function formatLoc($file, $line)
{
$file = ($file !== '' && $file !== '—') ? $file : self::IP_CHECK_FILE;
if (strpos($file, 'ip_check') !== false) {
$file = self::IP_CHECK_FILE;
}
return $file . ':' . (int)$line;
}
private static function stageTitle($stage)
{
$map = [
'session_fast_path' => 'Session 快速路径',
'url_args_timeout' => '临时链接参数',
'blacklist' => '黑名单',
'whitelist_ip' => 'IP 白名单',
'whitelist_params' => '链接参数白名单',
'fingerprint_cookie' => 'JS 指纹 Cookie',
'api_cloak' => 'API 远程判定',
'no_referer_or_gcuid' => '无 Referer/gcuid',
'session_partial' => 'Session 已有结果',
'main_block_skipped' => '主判定块跳过',
];
return isset($map[$stage]) ? $map[$stage] : $stage;
}
private static function buildRouteInfo(array $ctx, $result)
{
$mode = $ctx['mode'] ?? '';
$isReal = ($mode !== 'zp') && ((($mode === 'ip_check' && $result !== 'false') || $mode === 'fp'));
$forceRisk = !empty($ctx['force_risk']);
$visit3 = $ctx['session']['visit_to_3'] ?? '';
if (!$isReal) {
return [
'next_branch' => '安全页分支 (ip_check.php ~859)',
'dest' => (string)($ctx['zp_url'] ?? ''),
'condition' => "mode={$mode}, check_result={$result} → false 或 mode=zp",
];
}
if ($forceRisk && $visit3 !== '3') {
return [
'next_branch' => '真实页 + 二次风控 (page_666.php / cloakjs)',
'dest' => self::firstUrl($ctx['fp_urls'] ?? []),
'condition' => 'check_result!=false 且 CLOAK_RISK_NUMBER>0',
];
}
return [
'next_branch' => '真实页 (page_6.php)',
'dest' => self::firstUrl($ctx['fp_urls'] ?? []),
'condition' => "mode={$mode}, check_result={$result}",
];
}
private static function buildReasonDiagnosis($reason, $result, array $ctx)
{
if ($reason !== '') {
return '';
}
$items = [];
$final = self::$flags['final'] ?? null;
if (!$final) {
$items[] = '整个流程未记录到任何 check_result 写入点;最终 result 可能来自默认值或未赋值。';
} elseif ($final['reason'] === '') {
$items[] = '最终写入点 ' . htmlspecialchars(self::formatLoc($final['file'], $final['line'])) . '(' . htmlspecialchars(self::stageTitle($final['stage'])) . ')写入时 $reason 即为空。';
}
if (self::$apiResponse) {
$apiReason = self::$apiResponse['reason'] ?? '';
$apiCalled = !empty(self::$apiResponse['called']);
if ($apiCalled && $apiReason === '') {
$items[] = 'API 接口 cloak_check_curl 已调用,但返回 JSON 中 reason 字段为空(见下方 API 详情)。';
} elseif (!$apiCalled) {
$items[] = 'API 判定未执行(可能缺少 ip/Browser/Accept-Language,或主判定块被跳过)。';
}
}
$visit2 = $ctx['session']['visit_to_2'] ?? '';
if ($visit2 === '2' && empty(self::$apiResponse['called'])) {
$items[] = '$_SESSION[\'visit_to_2\']=2 导致主判定块(API 调用)被跳过;若同时 check_result 未重建,$reason 会保持初始空字符串。DEBUG 模式已重置 visit_to_*,若仍出现请检查是否在判定前被重新写入。';
}
if ($result === 'true') {
$items[] = '生产环境中,进入真实页分支后 ip_check.php:818 会将 $logs[\'reason\'] 清空(注释:访问仿品时没用跳转理由),日志里可能显示空理由,但 DEBUG 截停前应仍能看到写入时的 reason。';
}
if (empty($items)) {
$items[] = '$reason 在流程开始时初始化为空字符串,且后续没有任何分支对其赋值。';
}
$html = '⚠ $reason 为空 — 溯源分析
';
foreach ($items as $item) {
$html .= '- ' . $item . '
';
}
$html .= '
';
return $html;
}
private static function buildFlowHtml()
{
if (empty(self::$flow)) {
return '';
}
$html = '';
$final = self::$flags['final'] ?? null;
foreach (self::$flow as $i => $s) {
$isFinal = $final
&& isset($s['extra']['check_result'])
&& ($s['extra']['check_result'] === 'true' || $s['extra']['check_result'] === 'false')
&& $final['line'] === $s['line']
&& $final['stage'] === ($s['extra']['stage'] ?? '');
$cls = 'flow-step status-' . htmlspecialchars($s['status']);
if ($isFinal) {
$cls .= ' flow-final';
}
$html .= '
';
$html .= '
' . ($i + 1) . '
';
$html .= '
';
$html .= '
';
$html .= '' . htmlspecialchars($s['title']) . '';
$html .= '' . htmlspecialchars(self::statusLabel($s['status'])) . '';
$html .= '' . htmlspecialchars(self::formatLoc($s['file'], $s['line'])) . '';
$html .= '
';
if ($s['detail'] !== '') {
$html .= '
' . htmlspecialchars($s['detail']) . '
';
}
if (!empty($s['extra'])) {
$show = $s['extra'];
unset($show['stage'], $show['check_result']);
if (!empty($show)) {
$html .= '
' . htmlspecialchars(json_encode($show, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) . '
';
}
}
$html .= '
';
}
$html .= '
';
return $html;
}
private static function statusLabel($status)
{
$map = [
'pass' => '通过', 'fail' => '拦截', 'skip' => '跳过',
'warn' => '可疑', 'info' => '信息', 'decide' => '写入结果',
];
return isset($map[$status]) ? $map[$status] : $status;
}
private static function buildTrailHtml()
{
if (empty(self::$trail)) {
return '';
}
$html = '';
$final = self::$flags['final'] ?? null;
foreach (self::$trail as $e) {
$isFinal = $final && $final['line'] === $e['line'] && $final['stage'] === $e['stage'];
$cls = $isFinal ? ' trail-item final' : ' trail-item';
$res = $e['result'] !== null ? $e['result'] : '—';
$html .= '
';
$html .= '
';
$html .= '' . htmlspecialchars(self::formatLoc($e['file'], $e['line'])) . '';
$html .= '' . htmlspecialchars(self::stageTitle($e['stage'])) . '';
if ($e['result'] !== null) {
$html .= '' . htmlspecialchars((string)$res) . '';
}
$html .= '
';
$html .= '
' . htmlspecialchars($e['reason'] !== '' ? $e['reason'] : '(理由为空)') . '
';
$html .= '
';
}
$html .= '
';
return $html;
}
private static function buildApiHtml(array $api)
{
$html = '';
$html .= '- 是否调用
- ' . (!empty($api['called']) ? '是' : '否') . '
';
if (!empty($api['skip_reason'])) {
$html .= '- 未调用原因
- ' . htmlspecialchars($api['skip_reason']) . '
';
}
$html .= '- API result
' . htmlspecialchars((string)($api['result_raw'] ?? $api['check_result'] ?? '—')) . ' ';
$html .= '- API reason
- ' . htmlspecialchars((string)($api['reason'] ?? '—')) . '
';
$html .= '- API country
- ' . htmlspecialchars((string)($api['country'] ?? '—')) . '
';
if (!empty($api['curl_error'])) {
$html .= '- Curl 错误
- ' . htmlspecialchars($api['curl_error']) . '
';
}
if (!empty($api['raw'])) {
$html .= '- 原始响应
' . htmlspecialchars(is_string($api['raw']) ? $api['raw'] : json_encode($api['raw'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) . '
';
}
$html .= '
';
return $html;
}
private static function buildSessionHtml(array $session)
{
if (empty($session)) {
return '无 Session 快照
';
}
$html = '';
foreach ($session as $k => $v) {
$html .= '| ' . htmlspecialchars((string)$k) . ' | ' . htmlspecialchars(is_scalar($v) ? (string)$v : json_encode($v, JSON_UNESCAPED_UNICODE)) . ' |
';
}
$html .= '
';
return $html;
}
private static function buildVisitorRows(array $ctx)
{
$v = $ctx['visitor'] ?? null;
$rows = [
'IP 地址' => $v ? $v->v_ip : ($ctx['logs']['customers_ip'] ?? ''),
'国家/地区' => $v ? $v->v_country : '',
'浏览器' => $v ? $v->v_Browser : ($ctx['logs']['v_Browser'] ?? ''),
'客户端类型' => $v ? $v->v_Client : ($ctx['logs']['Client'] ?? ''),
'Referer' => $v ? $v->v_referer : ($ctx['logs']['v_referer'] ?? ''),
'当前 URL' => $v ? $v->v_curPageURL : ($ctx['logs']['v_PageURL'] ?? ''),
'Accept-Language' => $v ? $v->accept_language : ($ctx['logs']['accept_language'] ?? ''),
'User-Agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
'visit_md5' => $v ? $v->visit_md5_code : ($ctx['logs']['visit_md5_code'] ?? ''),
'设备型号' => $ctx['device'] ?? '',
'gcuid' => $_SESSION['gcuid'] ?? '',
];
return self::rowsHtml($rows);
}
private static function buildQueryRows()
{
$keys = ['fbclid', 'gclid', 'wbraid', 'gbraid', 'ttclid', 'ad_spm_id'];
$rows = [];
foreach ($keys as $k) {
$rows[$k] = isset($_GET[$k]) ? $_GET[$k] : '(未携带)';
}
return self::rowsHtml($rows);
}
private static function buildConfigRows()
{
$map = [
'SHOW_SITE_MODE_SWITCH' => defined('SHOW_SITE_MODE_SWITCH') ? SHOW_SITE_MODE_SWITCH : '',
'SHOW_SITE_COUNTRY' => defined('SHOW_SITE_COUNTRY') ? SHOW_SITE_COUNTRY : '',
'CLOAK_DEBUG_MODE' => defined('CLOAK_DEBUG_MODE') ? CLOAK_DEBUG_MODE : '',
'CLOAK_RISK_NUMBER' => defined('CLOAK_RISK_NUMBER') ? CLOAK_RISK_NUMBER : '',
'CLOAK_RISK_ENHANCED' => defined('CLOAK_RISK_ENHANCED') ? CLOAK_RISK_ENHANCED : '',
'CLOAK_URL_ARGS_TIMEOUT' => defined('CLOAK_URL_ARGS_TIMEOUT') ? CLOAK_URL_ARGS_TIMEOUT : '',
];
return self::rowsHtml($map);
}
private static function rowsHtml(array $rows)
{
$html = '';
foreach ($rows as $label => $val) {
$html .= '| ' . htmlspecialchars((string)$label) . ' | ' . htmlspecialchars((string)$val) . ' |
';
}
return $html;
}
private static function firstUrl($urls)
{
if (is_array($urls) && !empty($urls)) {
return (string)$urls[0];
}
return is_string($urls) ? $urls : '';
}
private static function styles()
{
return <<<'CSS'
CSS;
}
}