*/ private const array UTM_KEYS = [ 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', ]; /** * pt-BR labels for the UTM keys, in the order they should be * displayed when composing the briefing e-mail row. * * @var array */ private const array LABELS = [ 'utm_source' => 'origem', 'utm_medium' => 'mídia', 'utm_campaign' => 'campanha', 'utm_term' => 'termo', 'utm_content' => 'conteúdo', ]; /** * Reads UTM parameters from the query string, falling back to the * HTTP referrer when none are present. Returns an empty array when * neither is available — capturing nothing is a valid outcome * ("capturada quando disponível"). * * A referrer pointing back at this same host is not a marketing * origin — it is the visitor clicking from one internal page to * another (common once the original session has expired and a * fresh one starts mid-visit) — so it is discarded rather than * stored as noise (SPEC.md §12.5, "coletar apenas dados * necessários"). * * @param array $queryParams * @return array */ public static function capture(array $queryParams, ?string $referrer, ?string $requestHost): array { $utm = []; foreach (self::UTM_KEYS as $key) { $value = $queryParams[$key] ?? null; if (is_string($value) && trim($value) !== '') { $utm[$key] = self::sanitize($value); } } if ($utm !== []) { return $utm; } if (is_string($referrer) && trim($referrer) !== '' && ! self::isSameHost($referrer, $requestHost)) { $origin = self::originOf($referrer); if ($origin !== null) { return ['referrer' => self::sanitize($origin)]; } } return []; } /** * Reduces a referrer URL to its scheme+host identity, dropping the * path, query string, fragment, and any userinfo. The referrer is * only ever used as a marketing-origin *site* label (SPEC.md WEB-05) * — the query string can carry data that identifies an individual * (e.g. a personalized campaign link's `?email=...`), which would * exceed "coletar apenas dados necessários" (SPEC.md §12.5) once it * lands in the session and the internal briefing e-mail. */ private static function originOf(string $referrer): ?string { $host = parse_url($referrer, PHP_URL_HOST); if (! is_string($host) || $host === '') { return null; } $scheme = parse_url($referrer, PHP_URL_SCHEME); $prefix = is_string($scheme) && $scheme !== '' ? "{$scheme}://" : ''; return "{$prefix}{$host}"; } private static function isSameHost(string $referrer, ?string $requestHost): bool { if ($requestHost === null || $requestHost === '') { return false; } $referrerHost = parse_url($referrer, PHP_URL_HOST); return is_string($referrerHost) && strcasecmp($referrerHost, $requestHost) === 0; } /** * Composes the single-row, pt-BR display value for the internal * briefing e-mail. Returns null when nothing was captured, letting * the caller fall back to the same "—" convention already used for * other optional briefing fields. * * Accepts loosely-typed input on purpose: this reads back whatever * was put in the session, so it is treated as untrusted rather than * assumed to still match the shape `capture()` produced. * * @param array $origin */ public static function describe(array $origin): ?string { if (isset($origin['referrer']) && is_string($origin['referrer']) && $origin['referrer'] !== '') { return $origin['referrer']; } $parts = []; foreach (self::LABELS as $key => $label) { if (isset($origin[$key]) && is_string($origin[$key]) && $origin[$key] !== '') { $parts[] = "{$label}: {$origin[$key]}"; } } return $parts === [] ? null : implode(' | ', $parts); } /** * Untrusted input (query string, HTTP referrer) that ends up in an * HTML e-mail: strip any markup, drop control characters (also * closes off header-injection-style newline tricks), trim, and cap * the length before it ever reaches storage. */ private static function sanitize(string $value): string { $withoutTags = strip_tags($value); // No /u flag: this is a byte-wise scrub of ASCII control bytes, so // it cannot land mid-sequence in valid UTF-8 (continuation bytes // are all >= 0x80) — unlike the Unicode-mode regex, it never // blanks the whole string just because one byte is malformed. $withoutControlChars = preg_replace('/[\x00-\x1F\x7F]/', '', $withoutTags) ?? ''; return mb_substr(trim($withoutControlChars), 0, self::MAX_LENGTH); } }