Files
CLOAK/lib/CloakAdSourceGuard.php
T

271 lines
8.5 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 广告来源限制 — 4 级优先级检测 + 广告链接 query 拼装
*/
class CloakAdSourceGuard
{
const PLATFORM_FB = 'fb';
const PLATFORM_GOOGLE = 'google';
const PLATFORM_TIKTOK = 'tiktok';
private static $refererDomains = [
'fb.com',
'facebook.com',
'instagram.com',
'l.facebook.com',
'lm.facebook.com',
'm.facebook.com',
'google.com',
'googleadservices.com',
'doubleclick.net',
'youtube.com',
'googlesyndication.com',
'tiktok.com',
'tiktokv.com',
'bytedance.com',
'snssdk.com',
'pangleglobal.com',
'ads.tiktok.com',
];
private static $uaPatterns = [
'fban',
'fbav',
'instagram',
'googleads',
'adsbot-google',
'gsa/',
'tiktok',
'musical_ly',
'bytedancewebview',
'bytelocale',
'trill_',
];
private static $clidParams = ['fbclid', 'gclid', 'wbraid', 'gbraid', 'ttclid'];
private static $tiktokMacros = [
'__CAMPAIGN_ID__',
'__AID__',
'__CID__',
'__PLACEMENT__',
];
private static $fullTemplates = [
self::PLATFORM_FB => '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'] ?? ''));
// 优先级 1ad_spm_id 强验证
if ($spmId !== '' && isset($query['ad_spm_id']) && (string) $query['ad_spm_id'] === $spmId) {
return ['passed' => true, 'matched_by' => 'ad_spm_id', 'reason' => ''];
}
// 优先级 2clid 或 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' => ''];
}
}
// 优先级 3Referer
foreach (self::$refererDomains as $domain) {
if ($referer !== '' && strpos($referer, $domain) !== false) {
return ['passed' => true, 'matched_by' => 'referer:' . $domain, 'reason' => ''];
}
}
// 优先级 4User-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);
}
/**
* 生成 1012 位随机字母数字 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);
}
}