627 lines
29 KiB
PHP
627 lines
29 KiB
PHP
|
|
<?php
|
|||
|
|
/**
|
|||
|
|
* CloakDebugPage — DEBUG 模式调试结果页
|
|||
|
|
*/
|
|||
|
|
class CloakDebugPage
|
|||
|
|
{
|
|||
|
|
/** 判定逻辑所在文件(入口 index.php require 本文件,backtrace 无法可靠取行号) */
|
|||
|
|
const IP_CHECK_FILE = 'ip_check.php';
|
|||
|
|
|
|||
|
|
/** @var array 完整判断流程步骤 */
|
|||
|
|
private static $flow = [];
|
|||
|
|
|
|||
|
|
/** @var array check_result 写入轨迹 */
|
|||
|
|
private static $trail = [];
|
|||
|
|
|
|||
|
|
/** @var array|null */
|
|||
|
|
private static $apiResponse = null;
|
|||
|
|
|
|||
|
|
/** @var array */
|
|||
|
|
private static $flags = [];
|
|||
|
|
|
|||
|
|
/** @var array|null */
|
|||
|
|
private static $exitPoint = null;
|
|||
|
|
|
|||
|
|
/** @var float */
|
|||
|
|
private static $startedAt = 0;
|
|||
|
|
|
|||
|
|
public static function init()
|
|||
|
|
{
|
|||
|
|
self::$flow = [];
|
|||
|
|
self::$trail = [];
|
|||
|
|
self::$apiResponse = null;
|
|||
|
|
self::$flags = [];
|
|||
|
|
self::$exitPoint = null;
|
|||
|
|
self::$startedAt = microtime(true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 记录流程步骤(请从 ip_check.php 使用 cloak_dbg_step(__LINE__, ...) 调用)
|
|||
|
|
*/
|
|||
|
|
public static function step($title, $status, $detail = '', array $extra = [])
|
|||
|
|
{
|
|||
|
|
$loc = self::callerLoc();
|
|||
|
|
self::stepAt($loc['line'], $title, $status, $detail, $extra);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 记录流程步骤,指定 ip_check.php 行号
|
|||
|
|
*/
|
|||
|
|
public static function stepAt($line, $title, $status, $detail = '', array $extra = [])
|
|||
|
|
{
|
|||
|
|
self::$flow[] = [
|
|||
|
|
'title' => $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 '<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8">';
|
|||
|
|
echo '<meta name="viewport" content="width=device-width,initial-scale=1">';
|
|||
|
|
echo '<title>Cloak DEBUG — ' . htmlspecialchars($ctx['config_name'] ?? '') . '</title>';
|
|||
|
|
echo self::styles();
|
|||
|
|
echo '</head><body><div class="wrap">';
|
|||
|
|
|
|||
|
|
echo '<header class="hero">';
|
|||
|
|
echo '<div class="hero-badge">DEBUG MODE</div>';
|
|||
|
|
echo '<h1>流量判定调试报告</h1>';
|
|||
|
|
echo '<p class="hero-sub">配置 <code>' . htmlspecialchars($ctx['config_name'] ?? '') . '</code>';
|
|||
|
|
echo ' · 耗时 ' . (int)$elapsed . ' ms · ' . date('Y-m-d H:i:s') . '</p>';
|
|||
|
|
echo '</header>';
|
|||
|
|
|
|||
|
|
// 流程终止位置(醒目)
|
|||
|
|
echo '<section class="card exit-card">';
|
|||
|
|
echo '<h2>流程终止位置</h2>';
|
|||
|
|
echo '<div class="exit-main"><code>' . htmlspecialchars($exitLoc) . '</code></div>';
|
|||
|
|
echo '<p class="exit-desc">DEBUG 在此截停执行(<strong>' . $exitLabel . '</strong>),未继续跳转真实页/安全页。</p>';
|
|||
|
|
echo '<dl class="kv kv-tight">';
|
|||
|
|
echo '<dt>未开启 DEBUG 时下一分支</dt><dd>' . htmlspecialchars($routeInfo['next_branch']) . '</dd>';
|
|||
|
|
echo '<dt>预计跳转目标</dt><dd class="url-break">' . htmlspecialchars($routeInfo['dest'] !== '' ? $routeInfo['dest'] : '—') . '</dd>';
|
|||
|
|
echo '<dt>路由条件</dt><dd><code>' . htmlspecialchars($routeInfo['condition']) . '</code></dd>';
|
|||
|
|
echo '</dl></section>';
|
|||
|
|
|
|||
|
|
echo '<section class="card verdict-card ' . $verdictClass . '">';
|
|||
|
|
echo '<div class="verdict-icon">' . $verdictIcon . '</div>';
|
|||
|
|
echo '<div class="verdict-body">';
|
|||
|
|
echo '<div class="verdict-title">' . htmlspecialchars($verdictLabel) . '</div>';
|
|||
|
|
echo '<div class="verdict-meta">check_result = <code>' . htmlspecialchars($result) . '</code></div>';
|
|||
|
|
echo '</div></section>';
|
|||
|
|
|
|||
|
|
// 判断理由 + 空理由诊断
|
|||
|
|
echo '<section class="card"><h2>判定结论</h2><dl class="kv">';
|
|||
|
|
echo '<dt>当前 $reason</dt><dd>';
|
|||
|
|
if ($reason !== '') {
|
|||
|
|
echo htmlspecialchars($reason);
|
|||
|
|
} else {
|
|||
|
|
echo '<span class="tag tag-warn">为空</span>';
|
|||
|
|
}
|
|||
|
|
echo '</dd>';
|
|||
|
|
echo '<dt>最终写入 check_result</dt><dd><code>' . htmlspecialchars($finalLoc) . '</code>(' . $finalStage . ')</dd>';
|
|||
|
|
echo '<dt>写入时 $reason</dt><dd>' . $finalReason . '</dd>';
|
|||
|
|
echo '</dl>';
|
|||
|
|
if ($diagnosis !== '') {
|
|||
|
|
echo '<div class="diagnosis-box">' . $diagnosis . '</div>';
|
|||
|
|
}
|
|||
|
|
echo '</section>';
|
|||
|
|
|
|||
|
|
// 完整判断流程
|
|||
|
|
echo '<section class="card"><h2>完整判断流程</h2>';
|
|||
|
|
echo '<p class="hint">按 ip_check.php 实际执行顺序展示;<span class="legend-dot decide"></span> 表示写入 check_result,<span class="legend-dot fail"></span> 拦截,<span class="legend-dot skip"></span> 跳过。</p>';
|
|||
|
|
echo $flowHtml !== '' ? $flowHtml : '<p class="hint">暂无流程记录(请确认 CLOAK_DEBUG_MODE 已保存为 ON)。</p>';
|
|||
|
|
echo '</section>';
|
|||
|
|
|
|||
|
|
if ($trailHtml !== '') {
|
|||
|
|
echo '<section class="card"><h2>check_result 写入轨迹</h2>';
|
|||
|
|
echo '<p class="hint">每次对 $_SESSION[\'check_result\'] 的赋值;最后一项为最终判定依据。</p>';
|
|||
|
|
echo $trailHtml . '</section>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (self::$apiResponse) {
|
|||
|
|
echo '<section class="card"><h2>API 判定接口 (cloak_check_curl)</h2>';
|
|||
|
|
echo self::buildApiHtml(self::$apiResponse);
|
|||
|
|
echo '</section>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
echo '<div class="grid-2">';
|
|||
|
|
echo '<section class="card"><h2>Session 状态</h2>';
|
|||
|
|
echo $sessionHtml . '</section>';
|
|||
|
|
echo '<section class="card"><h2>路由预览</h2><dl class="kv kv-tight">';
|
|||
|
|
echo '<dt>SHOW_SITE_MODE</dt><dd><code>' . htmlspecialchars($ctx['mode'] ?? '') . '</code></dd>';
|
|||
|
|
echo '<dt>visit_to_2</dt><dd><code>' . htmlspecialchars($ctx['session']['visit_to_2'] ?? '—') . '</code> <span class="hint-inline">=2 时跳过 API 块</span></dd>';
|
|||
|
|
echo '<dt>visit_to_3</dt><dd><code>' . htmlspecialchars($ctx['session']['visit_to_3'] ?? '—') . '</code></dd>';
|
|||
|
|
echo '</dl></section>';
|
|||
|
|
echo '</div>';
|
|||
|
|
|
|||
|
|
echo '<section class="card"><h2>访客依据信息</h2>';
|
|||
|
|
echo '<table class="data-table"><tbody>' . self::buildVisitorRows($ctx) . '</tbody></table></section>';
|
|||
|
|
|
|||
|
|
echo '<section class="card"><h2>请求参数</h2>';
|
|||
|
|
echo '<table class="data-table"><tbody>' . self::buildQueryRows() . '</tbody></table></section>';
|
|||
|
|
|
|||
|
|
echo '<section class="card"><h2>配置快照</h2>';
|
|||
|
|
echo '<table class="data-table compact"><tbody>' . self::buildConfigRows() . '</tbody></table></section>';
|
|||
|
|
|
|||
|
|
echo '<footer class="foot">DEBUG 模式 — 生产环境请关闭 CLOAK_DEBUG_MODE</footer>';
|
|||
|
|
echo '</div></body></html>';
|
|||
|
|
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[] = '整个流程<strong>未记录</strong>到任何 <code>check_result</code> 写入点;最终 result 可能来自默认值或未赋值。';
|
|||
|
|
} elseif ($final['reason'] === '') {
|
|||
|
|
$items[] = '最终写入点 <code>' . htmlspecialchars(self::formatLoc($final['file'], $final['line'])) . '</code>(' . htmlspecialchars(self::stageTitle($final['stage'])) . ')写入时 <code>$reason</code> 即为空。';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (self::$apiResponse) {
|
|||
|
|
$apiReason = self::$apiResponse['reason'] ?? '';
|
|||
|
|
$apiCalled = !empty(self::$apiResponse['called']);
|
|||
|
|
if ($apiCalled && $apiReason === '') {
|
|||
|
|
$items[] = 'API 接口 <code>cloak_check_curl</code> 已调用,但返回 JSON 中 <code>reason</code> 字段为空(见下方 API 详情)。';
|
|||
|
|
} elseif (!$apiCalled) {
|
|||
|
|
$items[] = 'API 判定<strong>未执行</strong>(可能缺少 ip/Browser/Accept-Language,或主判定块被跳过)。';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$visit2 = $ctx['session']['visit_to_2'] ?? '';
|
|||
|
|
if ($visit2 === '2' && empty(self::$apiResponse['called'])) {
|
|||
|
|
$items[] = '<code>$_SESSION[\'visit_to_2\']=2</code> 导致主判定块(API 调用)被跳过;若同时 <code>check_result</code> 未重建,<code>$reason</code> 会保持初始空字符串。DEBUG 模式已重置 visit_to_*,若仍出现请检查是否在判定前被重新写入。';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($result === 'true') {
|
|||
|
|
$items[] = '生产环境中,进入真实页分支后 <code>ip_check.php:818</code> 会将 <code>$logs[\'reason\']</code> 清空(注释:访问仿品时没用跳转理由),日志里可能显示空理由,但 DEBUG 截停前应仍能看到写入时的 reason。';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (empty($items)) {
|
|||
|
|
$items[] = '$reason 在流程开始时初始化为空字符串,且后续没有任何分支对其赋值。';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html = '<h3 class="diag-title">⚠ $reason 为空 — 溯源分析</h3><ul class="diag-list">';
|
|||
|
|
foreach ($items as $item) {
|
|||
|
|
$html .= '<li>' . $item . '</li>';
|
|||
|
|
}
|
|||
|
|
$html .= '</ul>';
|
|||
|
|
return $html;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static function buildFlowHtml()
|
|||
|
|
{
|
|||
|
|
if (empty(self::$flow)) {
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
$html = '<div class="flow-timeline">';
|
|||
|
|
$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 .= '<div class="' . $cls . '">';
|
|||
|
|
$html .= '<div class="flow-marker">' . ($i + 1) . '</div>';
|
|||
|
|
$html .= '<div class="flow-body">';
|
|||
|
|
$html .= '<div class="flow-head">';
|
|||
|
|
$html .= '<span class="flow-title">' . htmlspecialchars($s['title']) . '</span>';
|
|||
|
|
$html .= '<span class="flow-badge badge-' . htmlspecialchars($s['status']) . '">' . htmlspecialchars(self::statusLabel($s['status'])) . '</span>';
|
|||
|
|
$html .= '<code class="flow-loc">' . htmlspecialchars(self::formatLoc($s['file'], $s['line'])) . '</code>';
|
|||
|
|
$html .= '</div>';
|
|||
|
|
if ($s['detail'] !== '') {
|
|||
|
|
$html .= '<div class="flow-detail">' . htmlspecialchars($s['detail']) . '</div>';
|
|||
|
|
}
|
|||
|
|
if (!empty($s['extra'])) {
|
|||
|
|
$show = $s['extra'];
|
|||
|
|
unset($show['stage'], $show['check_result']);
|
|||
|
|
if (!empty($show)) {
|
|||
|
|
$html .= '<pre class="code-block sm">' . htmlspecialchars(json_encode($show, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) . '</pre>';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
$html .= '</div></div>';
|
|||
|
|
}
|
|||
|
|
$html .= '</div>';
|
|||
|
|
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 = '<div class="trail">';
|
|||
|
|
$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 .= '<div class="' . trim($cls) . '">';
|
|||
|
|
$html .= '<div class="trail-head">';
|
|||
|
|
$html .= '<span class="trail-loc"><code>' . htmlspecialchars(self::formatLoc($e['file'], $e['line'])) . '</code></span>';
|
|||
|
|
$html .= '<span class="trail-stage">' . htmlspecialchars(self::stageTitle($e['stage'])) . '</span>';
|
|||
|
|
if ($e['result'] !== null) {
|
|||
|
|
$html .= '<span class="trail-res">' . htmlspecialchars((string)$res) . '</span>';
|
|||
|
|
}
|
|||
|
|
$html .= '</div>';
|
|||
|
|
$html .= '<div class="trail-reason">' . htmlspecialchars($e['reason'] !== '' ? $e['reason'] : '(理由为空)') . '</div>';
|
|||
|
|
$html .= '</div>';
|
|||
|
|
}
|
|||
|
|
$html .= '</div>';
|
|||
|
|
return $html;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static function buildApiHtml(array $api)
|
|||
|
|
{
|
|||
|
|
$html = '<dl class="kv">';
|
|||
|
|
$html .= '<dt>是否调用</dt><dd>' . (!empty($api['called']) ? '是' : '否') . '</dd>';
|
|||
|
|
if (!empty($api['skip_reason'])) {
|
|||
|
|
$html .= '<dt>未调用原因</dt><dd>' . htmlspecialchars($api['skip_reason']) . '</dd>';
|
|||
|
|
}
|
|||
|
|
$html .= '<dt>API result</dt><dd><code>' . htmlspecialchars((string)($api['result_raw'] ?? $api['check_result'] ?? '—')) . '</code></dd>';
|
|||
|
|
$html .= '<dt>API reason</dt><dd>' . htmlspecialchars((string)($api['reason'] ?? '—')) . '</dd>';
|
|||
|
|
$html .= '<dt>API country</dt><dd>' . htmlspecialchars((string)($api['country'] ?? '—')) . '</dd>';
|
|||
|
|
if (!empty($api['curl_error'])) {
|
|||
|
|
$html .= '<dt>Curl 错误</dt><dd class="tag tag-warn">' . htmlspecialchars($api['curl_error']) . '</dd>';
|
|||
|
|
}
|
|||
|
|
if (!empty($api['raw'])) {
|
|||
|
|
$html .= '<dt>原始响应</dt><dd><pre class="code-block">' . htmlspecialchars(is_string($api['raw']) ? $api['raw'] : json_encode($api['raw'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) . '</pre></dd>';
|
|||
|
|
}
|
|||
|
|
$html .= '</dl>';
|
|||
|
|
return $html;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static function buildSessionHtml(array $session)
|
|||
|
|
{
|
|||
|
|
if (empty($session)) {
|
|||
|
|
return '<p class="hint">无 Session 快照</p>';
|
|||
|
|
}
|
|||
|
|
$html = '<table class="data-table compact"><tbody>';
|
|||
|
|
foreach ($session as $k => $v) {
|
|||
|
|
$html .= '<tr><th>' . htmlspecialchars((string)$k) . '</th><td><code>' . htmlspecialchars(is_scalar($v) ? (string)$v : json_encode($v, JSON_UNESCAPED_UNICODE)) . '</code></td></tr>';
|
|||
|
|
}
|
|||
|
|
$html .= '</tbody></table>';
|
|||
|
|
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 .= '<tr><th>' . htmlspecialchars((string)$label) . '</th><td>' . htmlspecialchars((string)$val) . '</td></tr>';
|
|||
|
|
}
|
|||
|
|
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'
|
|||
|
|
<style>
|
|||
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|||
|
|
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:linear-gradient(145deg,#0f172a,#1e293b 50%,#0f172a);color:#e2e8f0;min-height:100vh;line-height:1.55}
|
|||
|
|
.wrap{max-width:980px;margin:0 auto;padding:24px 16px 48px}
|
|||
|
|
.hero{text-align:center;margin-bottom:24px}
|
|||
|
|
.hero-badge{display:inline-block;background:#f59e0b;color:#1e293b;font-size:.72rem;font-weight:700;letter-spacing:.08em;padding:4px 10px;border-radius:999px;margin-bottom:10px}
|
|||
|
|
.hero h1{font-size:1.5rem;margin-bottom:6px}
|
|||
|
|
.hero-sub{font-size:.85rem;color:#94a3b8}
|
|||
|
|
.hero-sub code{background:#334155;padding:2px 6px;border-radius:4px}
|
|||
|
|
.card{background:#1e293b;border:1px solid #334155;border-radius:12px;padding:20px;margin-bottom:16px}
|
|||
|
|
.card h2{font-size:.92rem;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:.04em;margin-bottom:14px}
|
|||
|
|
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
|||
|
|
@media(max-width:720px){.grid-2{grid-template-columns:1fr}}
|
|||
|
|
.exit-card{border-color:#f59e0b;background:linear-gradient(135deg,#422006 0%,#1e293b 40%)}
|
|||
|
|
.exit-main{font-size:1.4rem;font-weight:700;margin-bottom:8px}
|
|||
|
|
.exit-main code{color:#fcd34d}
|
|||
|
|
.exit-desc{font-size:.88rem;color:#cbd5e1;margin-bottom:12px}
|
|||
|
|
.verdict-card{display:flex;align-items:center;gap:20px;padding:22px}
|
|||
|
|
.verdict-pass{border-color:#059669;background:linear-gradient(135deg,#064e3b,#1e293b)}
|
|||
|
|
.verdict-block{border-color:#dc2626;background:linear-gradient(135deg,#7f1d1d,#1e293b)}
|
|||
|
|
.verdict-icon{width:52px;height:52px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:1.5rem;font-weight:700;flex-shrink:0}
|
|||
|
|
.verdict-pass .verdict-icon{background:#059669;color:#fff}
|
|||
|
|
.verdict-block .verdict-icon{background:#dc2626;color:#fff}
|
|||
|
|
.verdict-title{font-size:1.25rem;font-weight:700}
|
|||
|
|
.verdict-meta{font-size:.85rem;color:#94a3b8;margin-top:4px}
|
|||
|
|
.kv dt{font-size:.76rem;color:#64748b;margin-top:10px}
|
|||
|
|
.kv dt:first-child{margin-top:0}
|
|||
|
|
.kv dd{font-size:.9rem;margin-top:2px;word-break:break-all}
|
|||
|
|
.kv-tight dt{margin-top:6px}
|
|||
|
|
.diagnosis-box{margin-top:14px;padding:14px;background:#422006;border:1px solid #b45309;border-radius:8px;font-size:.85rem}
|
|||
|
|
.diag-title{font-size:.88rem;color:#fcd34d;margin-bottom:8px}
|
|||
|
|
.diag-list{padding-left:18px;color:#fde68a}
|
|||
|
|
.diag-list li{margin-bottom:6px}
|
|||
|
|
.flow-timeline{display:flex;flex-direction:column;gap:0;position:relative;padding-left:8px}
|
|||
|
|
.flow-step{display:flex;gap:14px;padding:12px 0;border-bottom:1px solid #334155;position:relative}
|
|||
|
|
.flow-step:last-child{border-bottom:none}
|
|||
|
|
.flow-step.flow-final .flow-body{border-left:3px solid #f59e0b;padding-left:12px;margin-left:-12px}
|
|||
|
|
.flow-marker{width:28px;height:28px;border-radius:50%;background:#334155;color:#94a3b8;font-size:.75rem;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:2px}
|
|||
|
|
.flow-step.status-decide .flow-marker{background:#b45309;color:#fff}
|
|||
|
|
.flow-step.status-fail .flow-marker{background:#dc2626;color:#fff}
|
|||
|
|
.flow-step.status-pass .flow-marker{background:#059669;color:#fff}
|
|||
|
|
.flow-step.status-skip .flow-marker{background:#475569;color:#cbd5e1}
|
|||
|
|
.flow-body{flex:1;min-width:0}
|
|||
|
|
.flow-head{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:4px}
|
|||
|
|
.flow-title{font-weight:600;font-size:.92rem}
|
|||
|
|
.flow-badge{font-size:.68rem;padding:2px 7px;border-radius:4px;font-weight:600}
|
|||
|
|
.badge-pass{background:#064e3b;color:#6ee7b7}
|
|||
|
|
.badge-fail{background:#7f1d1d;color:#fca5a5}
|
|||
|
|
.badge-skip{background:#334155;color:#94a3b8}
|
|||
|
|
.badge-warn{background:#78350f;color:#fcd34d}
|
|||
|
|
.badge-info{background:#1e3a5f;color:#7dd3fc}
|
|||
|
|
.badge-decide{background:#92400e;color:#fde68a}
|
|||
|
|
.flow-loc{font-size:.72rem;color:#38bdf8}
|
|||
|
|
.flow-detail{font-size:.85rem;color:#cbd5e1}
|
|||
|
|
.legend-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin:0 2px}
|
|||
|
|
.legend-dot.decide{background:#f59e0b}
|
|||
|
|
.legend-dot.fail{background:#dc2626}
|
|||
|
|
.legend-dot.skip{background:#64748b}
|
|||
|
|
.hint{font-size:.8rem;color:#64748b;margin-bottom:12px}
|
|||
|
|
.hint-inline{font-size:.72rem;color:#64748b}
|
|||
|
|
.tag{display:inline-block;font-size:.72rem;padding:2px 8px;border-radius:4px}
|
|||
|
|
.tag-warn{background:#78350f;color:#fcd34d}
|
|||
|
|
.tag-info{background:#1e3a5f;color:#7dd3fc}
|
|||
|
|
.url-break{word-break:break-all;color:#38bdf8;font-size:.85rem}
|
|||
|
|
.data-table{width:100%;border-collapse:collapse;font-size:.84rem}
|
|||
|
|
.data-table th{text-align:left;color:#64748b;font-weight:500;width:130px;padding:7px 10px 7px 0;vertical-align:top;border-bottom:1px solid #334155}
|
|||
|
|
.data-table td{padding:7px 0;word-break:break-all;border-bottom:1px solid #334155}
|
|||
|
|
.data-table.compact th{width:180px}
|
|||
|
|
.data-table tr:last-child th,.data-table tr:last-child td{border-bottom:none}
|
|||
|
|
.code-block{background:#0f172a;border:1px solid #334155;border-radius:6px;padding:10px;font-size:.76rem;overflow-x:auto;margin-top:6px;white-space:pre-wrap}
|
|||
|
|
.code-block.sm{font-size:.7rem;padding:6px 8px}
|
|||
|
|
.trail{display:flex;flex-direction:column;gap:8px}
|
|||
|
|
.trail-item{background:#0f172a;border:1px solid #334155;border-radius:8px;padding:10px 12px}
|
|||
|
|
.trail-item.final{border-color:#f59e0b}
|
|||
|
|
.trail-head{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:4px}
|
|||
|
|
.trail-loc code{font-size:.76rem;color:#38bdf8}
|
|||
|
|
.trail-stage{font-size:.72rem;color:#64748b}
|
|||
|
|
.trail-res{font-size:.72rem;background:#334155;padding:2px 7px;border-radius:4px;font-family:monospace}
|
|||
|
|
.trail-reason{font-size:.85rem;color:#cbd5e1}
|
|||
|
|
.foot{text-align:center;font-size:.76rem;color:#64748b;margin-top:20px}
|
|||
|
|
</style>
|
|||
|
|
CSS;
|
|||
|
|
}
|
|||
|
|
}
|