'utm_source=facebook&site_source={{site_source_name}}&placement={{placement}}', self::PLATFORM_GOOGLE => 'utm_source=google&utm_term={keyword}&network={network}&placement={placement}', self::PLATFORM_TIKTOK => 'utm_source=tiktok&placement=__PLACEMENT__', ]; private static $multiPlatformParams = [ self::PLATFORM_FB => 'campaign_id={{campaign.id}}', self::PLATFORM_GOOGLE => 'utm_campaign={campaignid}', self::PLATFORM_TIKTOK => 'adgroup_id=__CID__', ]; /** * @param array $config ad_fb, ad_google, ad_tiktok, ad_strict, ad_spm_id (bool/string) * @param array $context referer, query, user_agent * @return array { passed: bool, matched_by: string, reason: string } */ public static function evaluate(array $config, array $context) { $enabled = self::anyPlatformEnabled($config); if (!$enabled) { return ['passed' => true, 'matched_by' => 'guard_disabled', 'reason' => '']; } $query = $context['query'] ?? []; $referer = strtolower((string) ($context['referer'] ?? '')); $userAgent = strtolower((string) ($context['user_agent'] ?? '')); $spmId = trim((string) ($config['ad_spm_id'] ?? '')); // 优先级 1:ad_spm_id 强验证 if ($spmId !== '' && isset($query['ad_spm_id']) && (string) $query['ad_spm_id'] === $spmId) { return ['passed' => true, 'matched_by' => 'ad_spm_id', 'reason' => '']; } // 优先级 2:clid 或 TikTok 动态宏 foreach (self::$clidParams as $param) { if (!empty($query[$param])) { return ['passed' => true, 'matched_by' => 'clid:' . $param, 'reason' => '']; } } $queryString = self::buildQueryString($query); foreach (self::$tiktokMacros as $macro) { if (strpos($queryString, $macro) !== false) { return ['passed' => true, 'matched_by' => 'tiktok_macro:' . $macro, 'reason' => '']; } } // 优先级 3:Referer foreach (self::$refererDomains as $domain) { if ($referer !== '' && strpos($referer, $domain) !== false) { return ['passed' => true, 'matched_by' => 'referer:' . $domain, 'reason' => '']; } } // 优先级 4:User-Agent foreach (self::$uaPatterns as $pattern) { if ($userAgent !== '' && strpos($userAgent, $pattern) !== false) { return ['passed' => true, 'matched_by' => 'ua:' . $pattern, 'reason' => '']; } } $strict = !empty($config['ad_strict']); if ($strict) { return [ 'passed' => false, 'matched_by' => 'none', 'reason' => '来源限制:未命中广告来源验证', ]; } return [ 'passed' => false, 'matched_by' => 'none', 'reason' => '未确定流量来源', ]; } /** * @param array $enabledPlatforms PLATFORM_* 常量列表 * @param string $spmId * @return string query 字符串(不含前导 ?) */ public static function buildAdLinkQuery(array $enabledPlatforms, $spmId) { $spmId = trim((string) $spmId); if (empty($enabledPlatforms)) { return $spmId !== '' ? 'ad_spm_id=' . rawurlencode($spmId) : ''; } $parts = []; if (count($enabledPlatforms) === 1) { $platform = $enabledPlatforms[0]; if (isset(self::$fullTemplates[$platform])) { $parts[] = self::$fullTemplates[$platform]; } } else { foreach ($enabledPlatforms as $platform) { if (isset(self::$multiPlatformParams[$platform])) { $parts[] = self::$multiPlatformParams[$platform]; } } } if ($spmId !== '') { $parts[] = 'ad_spm_id=' . rawurlencode($spmId); } return implode('&', $parts); } /** * 生成 10–12 位随机字母数字 ad_spm_id */ public static function generateSpmId() { $len = random_int(10, 12); $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; $max = strlen($chars) - 1; $id = ''; for ($i = 0; $i < $len; $i++) { $id .= $chars[random_int(0, $max)]; } return $id; } /** * @param string $spmId */ public static function isValidSpmId($spmId) { return (bool) preg_match('/^[A-Za-z0-9]{10,12}$/', (string) $spmId); } /** * @param array $config */ public static function anyPlatformEnabled(array $config) { return !empty($config['ad_fb']) || !empty($config['ad_google']) || !empty($config['ad_tiktok']); } /** * 从当前站点配置读取广告来源限制参数 */ public static function configFromDefines() { return [ 'ad_fb' => defined('CLOAK_AD_FB') && strtoupper(CLOAK_AD_FB) === 'ON', 'ad_google' => defined('CLOAK_AD_GOOGLE') && strtoupper(CLOAK_AD_GOOGLE) === 'ON', 'ad_tiktok' => defined('CLOAK_AD_TIKTOK') && strtoupper(CLOAK_AD_TIKTOK) === 'ON', 'ad_strict' => defined('CLOAK_AD_STRICT') && strtoupper(CLOAK_AD_STRICT) === 'ON', 'ad_spm_id' => defined('CLOAK_AD_SPM_ID') ? (string) CLOAK_AD_SPM_ID : '', ]; } /** * 合并 Referer query 与 $_GET(与流水线 Guard 一致) */ public static function buildMergedRequestQuery(array $get = null) { if ($get === null) { $get = $_GET; } $refRaw = isset($_SERVER['HTTP_REFERER']) ? (string) $_SERVER['HTTP_REFERER'] : ''; $refParts = parse_url($refRaw); $refQ = []; if (!empty($refParts['query'])) { parse_str($refParts['query'], $refQ); } return array_merge($refQ, $get); } /** * 构建 evaluate 用的请求上下文 */ public static function buildRequestContext(array $get = null) { return [ 'referer' => isset($_SERVER['HTTP_REFERER']) ? (string) $_SERVER['HTTP_REFERER'] : '', 'query' => self::buildMergedRequestQuery($get), 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? (string) $_SERVER['HTTP_USER_AGENT'] : '', ]; } /** * 临时链接参数:是否满足开始/续期计时的来源条件 * 已开启广告来源限制 → 走 4 级优先级规则;未开启 → 兼容旧 clid/ad_spm_id 触发 */ public static function shouldStartUrlArgsTimer(array $config, array $context) { if (!self::anyPlatformEnabled($config)) { $query = $context['query'] ?? []; foreach (array_merge(self::$clidParams, ['ad_spm_id']) as $param) { if (!empty($query[$param])) { return true; } } return false; } $result = self::evaluate($config, $context); return !empty($result['passed']); } /** * @param array $query */ private static function buildQueryString(array $query) { if (empty($query)) { return ''; } return http_build_query($query); } }