array( 'label' => 'OpenAI – ChatGPT', 'domains' => array( 'chatgpt.com', 'chat.openai.com' ), 'utm_values' => array( 'chatgpt.com', 'chatgpt', 'openai' ), 'app_ids' => array( 'com.openai.chatgpt' ), ), 'perplexity' => array( 'label' => 'Perplexity', 'domains' => array( 'perplexity.ai' ), 'utm_values' => array( 'perplexity', 'perplexity.ai' ), 'app_ids' => array( 'ai.perplexity.app' ), ), 'claude' => array( 'label' => 'Anthropic – Claude', 'domains' => array( 'claude.ai' ), 'utm_values' => array( 'claude', 'claude.ai' ), 'app_ids' => array( 'com.anthropic.claude' ), ), 'gemini' => array( 'label' => 'Google – Gemini', 'domains' => array( 'gemini.google.com', 'aistudio.google.com' ), 'utm_values' => array(), 'app_ids' => array(), ), 'notebooklm' => array( 'label' => 'Google – NotebookLM', 'domains' => array( 'notebooklm.google.com' ), 'utm_values' => array(), 'app_ids' => array(), ), 'copilot' => array( 'label' => 'Microsoft – Copilot', 'domains' => array( 'copilot.microsoft.com' ), 'utm_values' => array( 'copilot' ), 'app_ids' => array( 'com.microsoft.copilot' ), ), 'deepseek' => array( 'label' => 'DeepSeek', 'domains' => array( 'chat.deepseek.com', 'deepseek.com' ), 'utm_values' => array( 'deepseek' ), 'app_ids' => array(), ), 'grok' => array( 'label' => 'xAI – Grok', 'domains' => array( 'grok.com' ), 'utm_values' => array( 'grok' ), 'app_ids' => array(), ), 'mistral' => array( 'label' => 'Mistral – Le Chat', 'domains' => array( 'chat.mistral.ai' ), 'utm_values' => array(), 'app_ids' => array(), ), 'metaai' => array( 'label' => 'Meta AI', 'domains' => array( 'meta.ai' ), 'utm_values' => array(), 'app_ids' => array(), ), 'youcom' => array( 'label' => 'You.com', 'domains' => array( 'you.com' ), 'utm_values' => array(), 'app_ids' => array(), ), 'poe' => array( 'label' => 'Quora – Poe', 'domains' => array( 'poe.com' ), 'utm_values' => array(), 'app_ids' => array(), ), 'phind' => array( 'label' => 'Phind', 'domains' => array( 'phind.com' ), 'utm_values' => array(), 'app_ids' => array(), ), 'brave' => array( 'label' => 'Brave Search / Leo', 'domains' => array( 'search.brave.com' ), 'utm_values' => array(), 'app_ids' => array(), ), ); $filtered = apply_filters( 'ai_o_optimiser_referral_sources', $sources ); return is_array( $filtered ) ? $filtered : $sources; } /** * Resolve a referrer URL to a known AI referral source. * * @param string $referrer Raw HTTP referrer. * @return array|null [ 'slug' => ..., 'label' => ... ] or null. */ public static function match( $referrer ) { if ( empty( $referrer ) ) { return null; } $host = wp_parse_url( $referrer, PHP_URL_HOST ); if ( empty( $host ) ) { return null; } $host = strtolower( preg_replace( '/^www\./', '', $host ) ); foreach ( self::defaults() as $slug => $source ) { foreach ( self::listed( $source, 'domains' ) as $domain ) { $domain = strtolower( preg_replace( '/^www\./', '', $domain ) ); if ( self::host_matches_domain( $host, $domain ) ) { return self::result( $slug, $source, 'referrer' ); } } } return null; } /** * Resolve a native-app referrer to a known AI referral source. * * Assistants opening a link from their own Android app send a referrer such * as android-app://com.openai.chatgpt. That scheme is not in WordPress's * allowed-protocol list, so esc_url_raw() reduces it to an empty string -- * the value has to be read before any URL sanitising is applied to it. * * @param string $raw_referrer Unsanitised Referer header value. * @return array|null */ public static function match_app_referrer( $raw_referrer ) { $raw_referrer = trim( (string) $raw_referrer ); if ( '' === $raw_referrer || 0 !== stripos( $raw_referrer, 'android-app://' ) ) { return null; } $package = strtolower( substr( $raw_referrer, strlen( 'android-app://' ) ) ); $package = trim( strtok( $package, '/?#' ) ); if ( '' === $package || ! preg_match( '/^[a-z0-9._-]+$/', $package ) ) { return null; } foreach ( self::defaults() as $slug => $source ) { foreach ( self::listed( $source, 'app_ids' ) as $app_id ) { if ( strtolower( (string) $app_id ) === $package ) { return self::result( $slug, $source, 'app' ); } } } return null; } /** * Resolve a campaign-style source value to a known AI referral source. * * ChatGPT appends utm_source=chatgpt.com to the links it hands out, and * several other assistants do something equivalent. Matching is exact rather * than substring: "openai" identifies a source, but a site's own campaign * named "openai-webinar-invite" is the site's traffic, not an assistant's. * * @param string $value Raw parameter value. * @return array|null */ public static function match_query_source( $value ) { $value = strtolower( trim( (string) $value ) ); if ( '' === $value ) { return null; } foreach ( self::defaults() as $slug => $source ) { foreach ( self::listed( $source, 'utm_values' ) as $candidate ) { if ( strtolower( (string) $candidate ) === $value ) { return self::result( $slug, $source, 'query' ); } } } return null; } /** * Resolve a request URI's query string to a known AI referral source. * * @param string $request_uri Raw request URI, query string included. * @return array|null */ public static function match_request_uri( $request_uri ) { $query = wp_parse_url( (string) $request_uri, PHP_URL_QUERY ); if ( ! is_string( $query ) || '' === $query ) { return null; } $args = array(); parse_str( $query, $args ); foreach ( self::QUERY_KEYS as $key ) { if ( ! isset( $args[ $key ] ) || ! is_scalar( $args[ $key ] ) ) { continue; } $match = self::match_query_source( (string) $args[ $key ] ); if ( null !== $match ) { $match['signal_value'] = $key . '=' . strtolower( trim( (string) $args[ $key ] ) ); return $match; } } return null; } /** * Read one source's list of a given kind, tolerating a custom source that * omits it. * * @param array $source Source definition. * @param string $key domains|utm_values|app_ids. * @return array */ private static function listed( $source, $key ) { return isset( $source[ $key ] ) && is_array( $source[ $key ] ) ? $source[ $key ] : array(); } /** * @param string $slug Source slug. * @param array $source Source definition. * @param string $signal referrer|app|query. * @return array */ private static function result( $slug, $source, $signal ) { return array( 'slug' => $slug, 'label' => isset( $source['label'] ) ? $source['label'] : $slug, 'signal' => $signal, ); } /** * Match exact domain or subdomain without requiring PHP 8 str_ends_with(). * * @param string $host Hostname from referrer. * @param string $domain Known domain. * @return bool */ private static function host_matches_domain( $host, $domain ) { if ( $host === $domain ) { return true; } $suffix = '.' . $domain; return substr( $host, -strlen( $suffix ) ) === $suffix; } }