311 lines
9.8 KiB
PHP
Executable File
311 lines
9.8 KiB
PHP
Executable File
<?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'] ?? ''));
|
||
|
||
// 优先级 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
|
||
* @param string|array|null $fpUrls DB_FP 真实页 URL(用于 Adjust 宏检测)
|
||
* @return string query 字符串(不含前导 ?)
|
||
*/
|
||
public static function buildAdLinkQuery(array $enabledPlatforms, $spmId, $fpUrls = null)
|
||
{
|
||
$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 (in_array(self::PLATFORM_FB, $enabledPlatforms, true)) {
|
||
$parts = array_merge($parts, self::buildAdjustMacroQueryParts($fpUrls));
|
||
}
|
||
|
||
if ($spmId !== '') {
|
||
$parts[] = 'ad_spm_id=' . rawurlencode($spmId);
|
||
}
|
||
|
||
return implode('&', $parts);
|
||
}
|
||
|
||
/**
|
||
* DB_FP 为 Adjust 链接时,追加 p1–p6 等 Facebook 宏到广告链接
|
||
*
|
||
* @param string|array|null $fpUrls
|
||
* @return string[]
|
||
*/
|
||
private static function buildAdjustMacroQueryParts($fpUrls)
|
||
{
|
||
require_once __DIR__ . '/Cloak/AttributionUrlMerger.php';
|
||
|
||
$fpUrl = '';
|
||
if (is_array($fpUrls)) {
|
||
foreach ($fpUrls as $url) {
|
||
$url = trim((string) $url);
|
||
if ($url !== '') {
|
||
$fpUrl = $url;
|
||
break;
|
||
}
|
||
}
|
||
} else {
|
||
$fpUrl = trim((string) $fpUrls);
|
||
}
|
||
|
||
if ($fpUrl === '' || !AttributionUrlMerger::needsAttributionMerge($fpUrl)) {
|
||
return [];
|
||
}
|
||
|
||
$parts = [];
|
||
foreach (AttributionUrlMerger::parseAdjustMacroKeys($fpUrl) as $key => $macro) {
|
||
$parts[] = rawurlencode((string) $key) . '=' . $macro;
|
||
}
|
||
|
||
return $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);
|
||
}
|
||
}
|