#!/usr/bin/env php = ' . CloakPhpCompat::MIN_PHP_VERSION, $compat['ok'], implode('; ', $compat['errors'])); foreach ($compat['warnings'] as $w) { if ($verbose) { echo " ! WARN: {$w}\n"; } } // ── 2. 语法检查 ─────────────────────────────────────────────── echo "[2] 全项目 php -l\n"; $lint = CloakPhpCompat::lintProject($root); reg_assert('所有 PHP 文件语法正确', $lint['ok'], implode("\n ", array_slice($lint['failures'], 0, 5))); // ── 3. Bootstrap 链 ─────────────────────────────────────────── echo "[3] 引导与配置\n"; require_once $root . '/cong.php'; require_once $root . '/config/ConfigLoader.php'; require_once $root . '/lib/bootstrap.php'; ConfigLoader::loadByName('index', $root); reg_assert('ConfigLoader 可加载 index 配置', defined('SHOW_SITE_MODE_SWITCH')); reg_assert('bootstrap 已加载 CheckPipeline', class_exists('CheckPipeline', false)); // ── 4. ConfigWriter 输出 ────────────────────────────────────── echo "[4] 后台 ConfigWriter\n"; require_once $root . '/admin/ConfigWriter.php'; $sample = ConfigWriter::buildDefineSource('index.php', [ 'methods' => 'ip_check', 'blacklist_group_id' => 0, 'whitelist_group_id' => 0, 'country' => 'US', 'show_content' => '302', 'DB_fp' => ['https://x.com/'], 'costm_ip_score' => 'test', 'auto_black' => 'OFF', 'is_virtual' => '0', 'auto_black_times' => '5', 'white_params' => '', 'iphone_model' => 0, 'write_log' => 0, 'zh_on' => 'OFF', 'os_on' => 'OFF', 'keep_params' => 'OFF', 'redirect_method' => '302', 'DB_zp' => 'https://z.com', 'mobile_on' => 'OFF', 'v_referer' => 'OFF', 'risk_number' => '0', 'risk_enhanced' => 'OFF', 'url_args_timeout' => '0', 'debug_mode' => 'OFF', 'ad_fb' => 'OFF', 'ad_google' => 'OFF', 'ad_tiktok' => 'OFF', 'ad_strict' => 'OFF', 'ad_spm_id' => '', ]); $tmpCfg = sys_get_temp_dir() . '/cloak_reg_cfg_' . getmypid() . '.php'; file_put_contents($tmpCfg, $sample); exec(escapeshellarg(PHP_BINARY) . ' -l ' . escapeshellarg($tmpCfg) . ' 2>&1', $lintOut, $lintCode); @unlink($tmpCfg); reg_assert('ConfigWriter 生成合法 PHP', $lintCode === 0, implode(' ', $lintOut)); reg_assert('ConfigWriter 含 deprecated SHOW_SITE_MOBILE', strpos($sample, 'SHOW_SITE_MOBILE') !== false); reg_assert('ConfigWriter 含 CLOAK_RISK_ENHANCED', strpos($sample, 'CLOAK_RISK_ENHANCED') !== false); reg_assert('ConfigWriter 含 CLOAK_AD_FB', strpos($sample, 'CLOAK_AD_FB') !== false); reg_assert('ConfigWriter 含 CLOAK_AD_SPM_ID', strpos($sample, 'CLOAK_AD_SPM_ID') !== false); $adGuardOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_ad_source_guard.php') . ' 2>/dev/null'); $adGuardJson = json_decode((string) $adGuardOut, true); reg_assert('广告来源 Guard 验证', ($adGuardJson['ok'] ?? false) === true, (string) $adGuardOut); $timerOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_pipeline_timer.php') . ' 2>/dev/null'); $timerJson = json_decode((string) $timerOut, true); reg_assert('PipelineTimer 验证', ($timerJson['ok'] ?? false) === true, (string) $timerOut); $realVisitorOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_session_real_visitor.php') . ' 2>/dev/null'); $realVisitorJson = json_decode((string) $realVisitorOut, true); reg_assert('真实访客 Session 缓存', ($realVisitorJson['ok'] ?? false) === true, (string) $realVisitorOut); $reqUrlOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_request_url.php') . ' 2>/dev/null'); $reqUrlJson = json_decode((string) $reqUrlOut, true); reg_assert('请求 URL 辅助函数', ($reqUrlJson['ok'] ?? false) === true, (string) $reqUrlOut); // ── 5. CheckPipeline 三种模式 ───────────────────────────────── echo "[5] CheckPipeline 模式\n"; function run_pipeline_subprocess($root, $fixtureName) { $fixture = $root . '/tools/fixtures/' . $fixtureName; $cmd = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/run_pipeline_fixture.php') . ' ' . escapeshellarg($fixture) . ' 2>&1'; exec($cmd, $out, $code); $json = json_decode(implode("\n", $out), true); return is_array($json) ? $json : ['error' => implode("\n", $out), 'code' => $code]; } $ipResult = run_pipeline_subprocess($root, 'regression_ip_check.php'); reg_assert('ip_check + fbclid 产生 check_result', ($ipResult['result'] ?? null) !== null, json_encode($ipResult, JSON_UNESCAPED_UNICODE)); reg_assert('ip_check 无意外 HTML 输出', (int) ($ipResult['output_len'] ?? -1) === 0); $fpResult = run_pipeline_subprocess($root, 'regression_fp.php'); reg_assert('fp 模式配置生效', ($fpResult['mode'] ?? '') === 'fp', json_encode($fpResult, JSON_UNESCAPED_UNICODE)); $zpResult = run_pipeline_subprocess($root, 'regression_zp.php'); reg_assert('zp 模式配置生效', ($zpResult['mode'] ?? '') === 'zp', json_encode($zpResult, JSON_UNESCAPED_UNICODE)); // ── 7. 指纹跳转模板 ─────────────────────────────────────────── echo "[7] 指纹跳转模板\n"; require_once $root . '/lib/Cloak/Page/FingerprintRedirectRenderer.php'; $tpl = $root . '/resources/fingerprint_redirect.html.php'; reg_assert('指纹模板文件存在', is_readable($tpl)); $tplBody = file_get_contents($tpl); reg_assert('模板含 CONFIG_NAME 占位符', strpos($tplBody, '{{CONFIG_NAME}}') !== false); reg_assert('模板 time 参数存在时停止重定向', strpos($tplBody, "urlParams.has('time')") !== false && strpos($tplBody, 'return;') !== false); reg_assert('模板在加载 dist 前注册回调', strpos($tplBody, '__cloakFingerprintDone') !== false && strpos($tplBody, '') !== false); require_once $root . '/lib/CloakHttpHelpers.php'; $_GET = ['time' => '1234567890']; reg_assert('cloak_fingerprint_time_param_present', cloak_fingerprint_time_param_present()); $_GET = []; $_COOKIE = ['ctime' => '1', 'cyyek' => 'x', 'cl' => 'oklot']; reg_assert('cloak_fingerprint_cookies_ready', cloak_fingerprint_cookies_ready()); $_SERVER['HTTP_HOST'] = 'shop.example.com'; reg_assert('cloak_fingerprint_cookie_domain', cloak_fingerprint_cookie_domain() === '.example.com'); $_GET = ['c' => 'index']; ob_start(); include $root . '/dist/index.php'; $distJs = ob_get_clean(); reg_assert('dist/index.php 输出 Cookies.set', strpos($distJs, "fpCookies.set('ctime'") !== false); reg_assert('dist/index.php 输出指纹完成回调', strpos($distJs, '__cloakFingerprintDone') !== false); reg_assert('dist/index.php 无 PHP Fatal', stripos($distJs, 'Fatal error') === false); $virtualDetectOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_virtual_detect.php') . ' 2>/dev/null'); $virtualDetectJson = json_decode((string) $virtualDetectOut, true); reg_assert('虚拟设备检测规则引擎', ($virtualDetectJson['ok'] ?? false) === true, (string) $virtualDetectOut); $_COOKIE['mmr'] = '18'; reg_assert('mmr 18 映射', cloak_fingerprint_mmr_reason() === '移动端无有效加速度计/陀螺仪'); unset($_COOKIE['mmr']); // ── 8. f_check JSON 契约(405) ────────────────────────────── echo "[8] f_check API 边界\n"; $fcheckCmd = escapeshellarg(PHP_BINARY) . ' -r ' . escapeshellarg( '$_SERVER["REQUEST_METHOD"]="GET"; include ' . var_export($root . '/f_check.php', true) . ';' ) . ' 2>/dev/null'; $fcheckOut = shell_exec($fcheckCmd); $fcJson = json_decode((string) $fcheckOut, true); reg_assert('f_check 非 POST 返回 JSON status=error', is_array($fcJson) && ($fcJson['status'] ?? '') === 'error', (string) $fcheckOut); $fcheckBadCmd = escapeshellarg(PHP_BINARY) . ' -r ' . escapeshellarg( '$_SERVER["REQUEST_METHOD"]="POST"; $_POST=[]; include ' . var_export($root . '/f_check.php', true) . ';' ) . ' 2>/dev/null'; $fcheckBadOut = shell_exec($fcheckBadCmd); $fcBadJson = json_decode((string) $fcheckBadOut, true); reg_assert('f_check 无效 hdata 返回 JSON status=error', is_array($fcBadJson) && ($fcBadJson['status'] ?? '') === 'error', (string) $fcheckBadOut); // ── 11. 二次风控 cloakjs / RouteResolver ──────────────────────── echo "[11] 二次风控 (cloakjs / RouteResolver)\n"; $riskCmd = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/run_risk_route_fixture.php') . ' 2>&1'; exec($riskCmd, $riskOut, $riskCode); $riskResult = json_decode(implode("\n", $riskOut), true); reg_assert( 'RouteResolver 风险系数>0 输出二次风控页', ($riskResult['risk_page'] ?? false) === true && ($riskResult['has_fatal'] ?? true) === false, json_encode($riskResult, JSON_UNESCAPED_UNICODE) ); reg_assert('二次风控页嵌入访客 IP', ($riskResult['has_ip'] ?? false) === true, json_encode($riskResult, JSON_UNESCAPED_UNICODE)); reg_assert('二次风控页嵌入 log_id', ($riskResult['has_log_id'] ?? false) === true, json_encode($riskResult, JSON_UNESCAPED_UNICODE)); reg_assert('二次风控页含 f_check.php AJAX', ($riskResult['has_f_check'] ?? false) === true, json_encode($riskResult, JSON_UNESCAPED_UNICODE)); reg_assert('visit_to_3 缺省时自动初始化', ($riskResult['visit_to_3_was_unset'] ?? false) && ($riskResult['visit_to_3_init'] ?? '') === '1', json_encode($riskResult, JSON_UNESCAPED_UNICODE)); reg_assert('二次风控首次写库为 wait 非 true', ($riskResult['log_result'] ?? '') === 'wait', json_encode($riskResult, JSON_UNESCAPED_UNICODE)); reg_assert('cloakjs WebGL extension 空值防护', ($riskResult['webgl_null_guard'] ?? false) === true, json_encode($riskResult, JSON_UNESCAPED_UNICODE)); $cloakJsModeOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_cloakjs_mode.php') . ' 2>/dev/null'); $cloakJsModeJson = json_decode((string) $cloakJsModeOut, true); reg_assert('cloakjs 基础/加强模式输出', ($cloakJsModeJson['ok'] ?? false) === true, (string) $cloakJsModeOut); $v_infoStub = new stdClass(); $v_infoStub->v_ip = '198.51.100.7'; $v_infoStub->v_country = 'CA'; $log_idStub = 778899; ob_start(); extract(['v_info' => $v_infoStub, 'log_id' => $log_idStub], EXTR_SKIP); include $root . '/cloakjs.php'; $cloakJsOut = ob_get_clean(); reg_assert('cloakjs 独立渲染含 IP', strpos($cloakJsOut, '198.51.100.7') !== false); reg_assert('cloakjs 独立渲染含 log_id', strpos($cloakJsOut, '778899') !== false); reg_assert('cloakjs 默认模式不含 WebGPU 采集', strpos($cloakJsOut, 'getWebGPUFingerprint') === false); reg_assert('cloakjs 独立渲染无 PHP Fatal', stripos($cloakJsOut, 'Fatal error') === false); $ntVerifyCmd = escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_fcheck_nt.php') . ' 2>/dev/null'; $ntVerifyOut = shell_exec($ntVerifyCmd); $ntVerifyJson = json_decode((string) $ntVerifyOut, true); reg_assert('f_check nt.txt 畸形行跳过且 fp 索引合法', ($ntVerifyJson['ok'] ?? false) === true, (string) $ntVerifyOut); // ── 12. ConfigLoader / 宝塔部署文件 ─────────────────────────── echo "[12] ConfigLoader 与宝塔部署\n"; require_once $root . '/config/ConfigLoader.php'; reg_assert('ConfigLoader::sanitizeConfigName', ConfigLoader::sanitizeConfigName('ab.php') === 'ab'); reg_assert('ConfigLoader::sanitizeConfigName 非法回退 index', ConfigLoader::sanitizeConfigName('../x') === 'index'); $cfgLoaderOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_config_loader.php') . ' 2>/dev/null'); $cfgLoaderJson = json_decode((string) $cfgLoaderOut, true); reg_assert('DomainRepository 规范化', ($cfgLoaderJson['tests']['normalize_strips_www'] ?? false) === true, (string) $cfgLoaderOut); $btNginxOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_baota_nginx.php') . ' 2>/dev/null'); $btNginxJson = json_decode((string) $btNginxOut, true); reg_assert('宝塔 deploy 模板文件齐全', ($btNginxJson['ok'] ?? false) === true, (string) $btNginxOut); $cfDomainOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_cloudflare_domain.php') . ' 2>/dev/null'); $cfDomainJson = json_decode((string) $cfDomainOut, true); reg_assert('Cloudflare 域名自动化 Mock 流转', ($cfDomainJson['ok'] ?? false) === true, (string) $cfDomainOut); $geoOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_geoip.php') . ' 2>/dev/null'); $geoJson = json_decode((string) $geoOut, true); if (($geoJson['tests']['db_file_exists'] ?? false) === true) { reg_assert('GeoIP 本地国家库', ($geoJson['ok'] ?? false) === true, (string) $geoOut); } else { reg_skip('GeoIP 本地国家库', 'mmdb 未安装,运行 php tools/setup_geoip_db.php'); } // ── 9. 数据库连通(可选) ───────────────────────────────────── echo "[9] 数据库\n"; if (defined('DB_NAME')) { $conn = @new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME); if ($conn->connect_error) { reg_skip('MySQL 连接', $conn->connect_error); } else { reg_assert('MySQL 连接', true); require_once $root . '/lib/Cloak/VisitorLogSchema.php'; VisitorLogSchema::ensureColumns($conn); $fpHdataCol = $conn->query("SHOW COLUMNS FROM `visitor_logs` LIKE 'fp_hdata'"); reg_assert('visitor_logs.fp_hdata 列存在', $fpHdataCol && $fpHdataCol->num_rows > 0); require_once $root . '/lib/Cloak/VisitorLogWorker.php'; $prev = error_reporting(E_ALL); $GLOBALS['__cloak_dbg_run'] = 'regression-db-test'; $asyncOn = defined('VISITOR_LOG_ASYNC') && strtoupper(VISITOR_LOG_ASYNC) === 'ON'; $sampleLogs = [ 'campagin_id' => 'reg_test', 'visit_md5_code' => 'reg_async_' . substr(md5((string) microtime(true)), 0, 16), 'site_name' => 'reg.test', 'customers_ip' => '127.0.0.1', 'country' => null, 'result' => 'false', 'reason' => 'regression', 'v_referer' => null, 'Client' => 'PC', 'v_Browser' => null, 'v_PageURL' => '/', 'accept_language' => null, 'fp_url' => null, 'device' => null, ]; $logId = @write_log_db($sampleLogs); if ($asyncOn) { VisitorLogWorker::processBatch(20); if ($logId === null) { $md5Esc = $conn->real_escape_string($sampleLogs['visit_md5_code']); $res = $conn->query("SELECT `id` FROM `visitor_logs` WHERE `visit_md5_code`='{$md5Esc}' ORDER BY `id` DESC LIMIT 1"); $row = $res ? $res->fetch_assoc() : null; $logId = $row ? (int) $row['id'] : null; } reg_assert('write_log_db 异步模式 worker 落库', $logId !== null && (int) $logId > 0); } else { reg_assert('write_log_db 含 null 字段可正常执行', $logId !== null && (int) $logId > 0); } $waitId = @write_log_db(array_merge($sampleLogs, [ 'visit_md5_code' => 'reg_wait_' . substr(md5((string) microtime(true)), 0, 16), 'result' => 'wait', 'reason' => '二次风控检测中', 'fp_url' => '', ])); reg_assert('write_log_db wait 路径返回 log_id', $waitId !== null && (int) $waitId > 0); if ($asyncOn) { VisitorLogWorker::processBatch(20); } error_reporting($prev); if ($logId) { $conn->query('DELETE FROM `visitor_logs` WHERE `id` = ' . (int) $logId); } if ($waitId) { $conn->query('DELETE FROM `visitor_logs` WHERE `id` = ' . (int) $waitId); } $conn->close(); } } else { reg_skip('MySQL 连接', 'cong.php 未定义 DB_NAME'); } // ── 10. DEBUG 组件 ──────────────────────────────────────────── echo "[10] DEBUG 组件\n"; reg_assert('CloakDebugPage 可初始化', class_exists('CloakDebugPage', false)); CloakDebugPage::init(); CloakDebugPage::stepAt(__LINE__, 'regression', 'info', 'ok'); reg_assert('CloakDebugPage::stepAt 可调用', true); // ── 汇总 ────────────────────────────────────────────────────── echo "\n=== 汇总 ===\n"; echo "通过: {$passed} 失败: {$failed} 跳过: {$skipped}\n"; if ($failed > 0) { echo "\n回归未通过,请修复上述失败项。\n"; exit(1); } echo "\n全部回归通过。\n"; exit(0);