'meta', 'instagram' => 'meta', 'messenger' => 'meta', 'threads' => 'meta', 'an' => 'meta', ); /** * Other spellings of known sources, as they appear in utm_source or as website addresses. */ const ALIASES = array( 'google.com' => 'google', 'adwords' => 'google', 'googleads' => 'google', 'google_ads' => 'google', 'google-ads' => 'google', 'google ads' => 'google', 'gads' => 'google', 'googleadservices.com' => 'google', 'googlesyndication.com' => 'google', 'doubleclick.net' => 'google', 'syndicatedsearch.goog' => 'google', 'microsoft' => 'bing', 'microsoftads' => 'bing', 'microsoft_ads' => 'bing', 'microsoft-ads' => 'bing', 'msads' => 'bing', 'bingads' => 'bing', 'bing_ads' => 'bing', 'search.yahoo.com' => 'yahoo', 'ya.ru' => 'yandex', 'go.mail.ru' => 'mail.ru', 'so.com' => '360', 'sm.cn' => 'shenma', 'search.brave.com' => 'brave', 'fb' => 'facebook', 'fb.com' => 'facebook', 'fb.me' => 'facebook', 'facebook_ads' => 'facebook', 'facebookads' => 'facebook', 'fbads' => 'facebook', 'ig' => 'instagram', 'insta' => 'instagram', 'msg' => 'messenger', 'twitter' => 'x', 'twitter.com' => 'x', 't.co' => 'x', 'yt' => 'youtube', 'youtu.be' => 'youtube', 'lnkd.in' => 'linkedin', 'pin.it' => 'pinterest', 'bsky.app' => 'bluesky', 'mastodon.social' => 'mastodon', 't.me' => 'telegram', 'tg' => 'telegram', 'wa' => 'whatsapp', 'wa.me' => 'whatsapp', 'weixin' => 'wechat', 'weixin.qq.com' => 'wechat', 'chatgpt.com' => 'chatgpt', 'chat.openai.com' => 'chatgpt', 'openai' => 'chatgpt', 'gemini.google.com' => 'gemini', 'bard.google.com' => 'gemini', 'copilot.microsoft.com' => 'copilot', 'meta.ai' => 'meta-ai', 'chat.deepseek.com' => 'deepseek', 'chat.mistral.ai' => 'mistral', 'sendinblue' => 'brevo', 'e-mail' => 'email', 'e_mail' => 'email', 'mail' => 'email', ); /** * Known sources: name, website for the icon and kind. * * @return array */ public static function catalog() { return array( 'google' => array( 'Google', 'google.com', 'search' ), 'bing' => array( 'Bing', 'bing.com', 'search' ), 'yahoo' => array( 'Yahoo', 'yahoo.com', 'search' ), 'duckduckgo' => array( 'DuckDuckGo', 'duckduckgo.com', 'search' ), 'ecosia' => array( 'Ecosia', 'ecosia.org', 'search' ), 'brave' => array( 'Brave Search', 'search.brave.com', 'search' ), 'yandex' => array( 'Yandex', 'yandex.com', 'search' ), 'baidu' => array( 'Baidu', 'baidu.com', 'search' ), 'sogou' => array( 'Sogou', 'sogou.com', 'search' ), '360' => array( '360 Search', 'so.com', 'search' ), 'shenma' => array( 'Shenma', 'sm.cn', 'search' ), 'naver' => array( 'Naver', 'naver.com', 'search' ), 'daum' => array( 'Daum', 'daum.net', 'search' ), 'seznam' => array( 'Seznam', 'seznam.cz', 'search' ), 'coccoc' => array( 'Cốc Cốc', 'coccoc.com', 'search' ), 'mail.ru' => array( 'Mail.ru', 'mail.ru', 'search' ), 'qwant' => array( 'Qwant', 'qwant.com', 'search' ), 'startpage' => array( 'Startpage', 'startpage.com', 'search' ), 'ask' => array( 'Ask', 'ask.com', 'search' ), 'aol' => array( 'AOL', 'aol.com', 'search' ), 'petalsearch' => array( 'Petal Search', 'petalsearch.com', 'search' ), 'kagi' => array( 'Kagi', 'kagi.com', 'search' ), 'yep' => array( 'Yep', 'yep.com', 'search' ), 'facebook' => array( 'Facebook', 'facebook.com', 'social' ), 'instagram' => array( 'Instagram', 'instagram.com', 'social' ), 'meta' => array( 'Meta', 'facebook.com', 'social' ), 'an' => array( 'Meta Audience Network', 'facebook.com', 'ads' ), 'threads' => array( 'Threads', 'threads.net', 'social' ), 'linkedin' => array( 'LinkedIn', 'linkedin.com', 'social' ), 'x' => array( 'X (Twitter)', 'x.com', 'social' ), 'bluesky' => array( 'Bluesky', 'bsky.app', 'social' ), 'mastodon' => array( 'Mastodon', 'mastodon.social', 'social' ), 'tiktok' => array( 'TikTok', 'tiktok.com', 'social' ), 'douyin' => array( 'Douyin', 'douyin.com', 'social' ), 'youtube' => array( 'YouTube', 'youtube.com', 'social' ), 'pinterest' => array( 'Pinterest', 'pinterest.com', 'social' ), 'reddit' => array( 'Reddit', 'reddit.com', 'social' ), 'quora' => array( 'Quora', 'quora.com', 'social' ), 'snapchat' => array( 'Snapchat', 'snapchat.com', 'social' ), 'discord' => array( 'Discord', 'discord.com', 'social' ), 'xing' => array( 'XING', 'xing.com', 'social' ), 'vk' => array( 'VK', 'vk.com', 'social' ), 'ok' => array( 'Odnoklassniki', 'ok.ru', 'social' ), 'weibo' => array( 'Weibo', 'weibo.com', 'social' ), 'zhihu' => array( 'Zhihu', 'zhihu.com', 'social' ), 'xiaohongshu' => array( 'Xiaohongshu', 'xiaohongshu.com', 'social' ), 'bilibili' => array( 'Bilibili', 'bilibili.com', 'social' ), 'messenger' => array( 'Messenger', 'messenger.com', 'messenger' ), 'telegram' => array( 'Telegram', 'telegram.org', 'messenger' ), 'whatsapp' => array( 'WhatsApp', 'whatsapp.com', 'messenger' ), 'viber' => array( 'Viber', 'viber.com', 'messenger' ), 'line' => array( 'LINE', 'line.me', 'messenger' ), 'kakao' => array( 'KakaoTalk', 'kakao.com', 'messenger' ), 'wechat' => array( 'WeChat', 'wechat.com', 'messenger' ), 'chatgpt' => array( 'ChatGPT', 'chatgpt.com', 'ai' ), 'perplexity' => array( 'Perplexity', 'perplexity.ai', 'ai' ), 'claude' => array( 'Claude', 'claude.ai', 'ai' ), 'gemini' => array( 'Gemini', 'gemini.google.com', 'ai' ), 'copilot' => array( 'Microsoft Copilot', 'copilot.microsoft.com', 'ai' ), 'meta-ai' => array( 'Meta AI', 'meta.ai', 'ai' ), 'deepseek' => array( 'DeepSeek', 'chat.deepseek.com', 'ai' ), 'grok' => array( 'Grok', 'grok.com', 'ai' ), 'mistral' => array( 'Le Chat', 'chat.mistral.ai', 'ai' ), 'you.com' => array( 'You.com', 'you.com', 'ai' ), 'poe' => array( 'Poe', 'poe.com', 'ai' ), 'kimi' => array( 'Kimi', 'kimi.com', 'ai' ), 'doubao' => array( 'Doubao', 'doubao.com', 'ai' ), 'impact' => array( 'Impact', 'impact.com', 'affiliate' ), 'awin' => array( 'Awin', 'awin.com', 'affiliate' ), 'mailchimp' => array( 'Mailchimp', 'mailchimp.com', 'email' ), 'brevo' => array( 'Brevo', 'brevo.com', 'email' ), 'klaviyo' => array( 'Klaviyo', 'klaviyo.com', 'email' ), 'hubspot' => array( 'HubSpot', 'hubspot.com', 'email' ), 'email' => array( '', '', 'email' ), 'newsletter' => array( '', '', 'email' ), ); } /** * Ad platforms: name and website for the icon. * * @return array */ public static function ad_catalog() { return array( 'google' => array( 'Google Ads', 'ads.google.com' ), 'bing' => array( 'Microsoft Advertising', 'ads.microsoft.com' ), 'meta' => array( 'Meta Ads', 'facebook.com' ), 'linkedin' => array( 'LinkedIn Ads', 'linkedin.com' ), 'tiktok' => array( 'TikTok Ads', 'tiktok.com' ), 'x' => array( 'X Ads', 'x.com' ), 'pinterest' => array( 'Pinterest Ads', 'pinterest.com' ), 'reddit' => array( 'Reddit Ads', 'reddit.com' ), 'snapchat' => array( 'Snapchat Ads', 'snapchat.com' ), ); } /** * The value a visit is grouped by in the reports: its source, and for bought clicks the * channel with it, e.g. "google|paid_search". * * @param string $source Stored source. * @param string $channel Stored channel. * @return string */ public static function paid_key( $source, $channel ) { $source = (string) $source; if ( '' === $source || '(direct)' === $source || ! in_array( (string) $channel, self::PAID_CHANNELS, true ) ) { return $source; } return $source . '|' . $channel; } /** * Source and channel of a key of bought clicks. * * @param string $key Output of key() or paid_key(). * @return array{0: string, 1: string}|null Null for other sources. */ public static function split( $key ) { $key = (string) $key; $at = strrpos( $key, '|' ); if ( false === $at || ! in_array( substr( $key, $at + 1 ), self::PAID_CHANNELS, true ) ) { return null; } return array( substr( $key, 0, $at ), substr( $key, $at + 1 ) ); } /** * The name a source is grouped under: lowercase, a website without www., and the * spellings of a known service as that service. * * @param string $source Stored value. * @return string '' when not set, '(direct)' for direct visits. */ public static function key( $source ) { static $keys = array(); $source = (string) $source; if ( isset( $keys[ $source ] ) ) { return $keys[ $source ]; } $paid = self::split( $source ); if ( $paid ) { $name = self::key( $paid[0] ); $keys[ $source ] = ( self::AD_PLATFORMS[ $name ] ?? $name ) . '|' . $paid[1]; return $keys[ $source ]; } $name = mb_strtolower( trim( $source ) ); $address = (string) preg_replace( '#^[a-z][a-z0-9+.-]*://#', '', $name ); // A website address: only the host counts, without www. and the like. if ( preg_match( '#^[a-z0-9-]+(\.[a-z0-9-]+)*\.[a-z]{2,}(?:[/:?\#]|$)#', $address ) ) { $name = (string) preg_replace( '/^(www|m|l|lm|mobile|web)\./', '', (string) strtok( $address, '/:?#' ) ); } $catalog = self::catalog(); if ( isset( self::ALIASES[ $name ] ) ) { $name = self::ALIASES[ $name ]; } elseif ( ! isset( $catalog[ $name ] ) && preg_match( '/^([a-z0-9-]+)\.(?:[a-z]{2,3}\.)?[a-z]{2,}$/', $name, $match ) ) { // Country domains of a known service, such as google.de or yahoo.co.jp. $known = $catalog[ $match[1] ] ?? null; if ( $known && in_array( $known[2], array( 'search', 'social', 'messenger', 'ai' ), true ) ) { $name = $match[1]; } } $keys[ $source ] = $name; return $name; } /** * Display name. * * @param string $key Output of key(). * @return string */ public static function label( $key ) { $key = (string) $key; if ( '' === $key ) { return __( '(not set)', 'dealroot' ); } if ( '(direct)' === $key ) { return __( 'Direct', 'dealroot' ); } if ( 'email' === $key ) { return __( 'Email', 'dealroot' ); } if ( 'newsletter' === $key ) { return __( 'Newsletter', 'dealroot' ); } $paid = self::split( $key ); if ( $paid ) { $ads = self::ad_catalog(); /* translators: %s: name of a website or service, e.g. "Quora" */ return isset( $ads[ $paid[0] ] ) ? $ads[ $paid[0] ][0] : sprintf( __( '%s ads', 'dealroot' ), self::label( $paid[0] ) ); } $catalog = self::catalog(); return isset( $catalog[ $key ] ) ? $catalog[ $key ][0] : $key; } /** * Kind of source: search, social, messenger, ai, ads, affiliate, email, website, link * (any other utm_source), direct, none, or the channel of bought clicks. * * @param string $key Output of key(). * @return string */ public static function kind( $key ) { $key = (string) $key; $catalog = self::catalog(); if ( '' === $key ) { return 'none'; } if ( '(direct)' === $key ) { return 'direct'; } $paid = self::split( $key ); if ( $paid ) { return $paid[1]; } if ( isset( $catalog[ $key ] ) ) { return $catalog[ $key ][2]; } return self::is_host( $key ) ? 'website' : 'link'; } /** * What a kind of source is, for the line under its name. * * @param string $kind Output of kind(). * @return string */ public static function kind_label( $kind ) { $labels = array( 'paid_search' => __( 'Paid search', 'dealroot' ), 'paid_social' => __( 'Paid social', 'dealroot' ), 'display' => __( 'Display', 'dealroot' ), 'search' => __( 'Search engine', 'dealroot' ), 'social' => __( 'Social network', 'dealroot' ), 'messenger' => __( 'Messenger', 'dealroot' ), 'ai' => __( 'AI assistant', 'dealroot' ), 'ads' => __( 'Ad network', 'dealroot' ), 'affiliate' => __( 'Affiliate network', 'dealroot' ), 'email' => __( 'Email', 'dealroot' ), 'website' => __( 'Website', 'dealroot' ), 'link' => __( 'Campaign link', 'dealroot' ), 'direct' => __( 'Typed in or bookmarked', 'dealroot' ), 'none' => __( 'No source recorded', 'dealroot' ), ); return $labels[ $kind ] ?? ''; } /** * Website the icon of a source comes from. * * @param string $key Output of key(). * @return string Host, or '' when the source has no website. */ public static function domain( $key ) { $key = (string) $key; $catalog = self::catalog(); $paid = self::split( $key ); if ( $paid ) { $ads = self::ad_catalog(); return isset( $ads[ $paid[0] ] ) ? $ads[ $paid[0] ][1] : self::domain( $paid[0] ); } if ( isset( $catalog[ $key ] ) ) { return $catalog[ $key ][1]; } return self::is_host( $key ) ? $key : ''; } /** * Whether a value is a website's host name, e.g. example.co.uk. * * @param string $value Value. * @return bool */ public static function is_host( $value ) { return strlen( (string) $value ) <= 191 && (bool) preg_match( '/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]$/', (string) $value ); } /** * Whether a host is the website of a known source. * * @param string $host Host. * @return bool */ public static function is_known_domain( $host ) { return in_array( (string) $host, array_merge( array_column( self::catalog(), 1 ), array_column( self::ad_catalog(), 1 ) ), true ); } /** * The stored values of a column that are grouped under a name. * * @param string $column Leads column, source or last_source. * @param string $key Output of key(). * @return string[] */ public static function spellings( $column, $key ) { global $wpdb; static $values = array(); if ( ! isset( $values[ $column ] ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $values[ $column ] = (array) $wpdb->get_col( $wpdb->prepare( 'SELECT DISTINCT %i FROM %i', $column, Installer::table() ) ); } // Bought clicks are matched together with their channel, e.g. fb and instagram as Meta Ads. $paid = self::split( $key ); $found = array(); foreach ( $values[ $column ] as $value ) { if ( self::key( $paid ? $value . '|' . $paid[1] : (string) $value ) === $key ) { $found[] = (string) $value; } } if ( $found ) { return $found; } return array( $paid ? $paid[0] : (string) $key ); } }