[ 'category' => 'necessary', 'hosts' => [], 'names' => ['wordpress_', 'wp-settings-', 'wp_lang', 'comment_author_', 'wp-postpass_', 'wordpress_test_cookie', 'WP_DATA_USER_', 'wp-autosave-', 'wpEmojiSettings'], 'about' => 'Logged-in sessions, admin preferences and comment details set by WordPress.', ], 'WooCommerce' => [ 'category' => 'necessary', 'hosts' => [], 'names' => ['woocommerce_', 'wp_woocommerce_session_', 'store_notice', 'wc_cart_hash_', 'wc_fragments_', 'wc_cart_created'], 'about' => 'Cart and session state set by WooCommerce.', ], 'WooCommerce order attribution' => [ 'category' => 'analytics', 'hosts' => [], 'names' => ['sbjs_'], 'about' => 'Records where the visitor came from so WooCommerce can show the source of each order.', ], 'WP Zbridge cookie banner' => [ 'category' => 'necessary', 'hosts' => [], 'names' => ['wp_zaraz_consent', 'wp_zbridge_country'], 'about' => 'The visitor\'s cookie banner choices, and their country for EU-only targeting.', ], 'Cloudflare' => [ 'category' => 'necessary', 'hosts' => [], 'names' => ['__cf_bm', 'cf_clearance', '_cfuvid', '__cflb', 'cf_ob_info', 'cf_use_ob'], 'about' => 'Bot protection and load balancing set by Cloudflare.', ], 'Cloudflare Zaraz consent' => [ 'category' => 'necessary', 'hosts' => [], 'names' => ['zaraz-consent', 'cf_consent', 'zarazDebug'], 'about' => 'Consent choices Zaraz applies to your tools.', ], 'Google Analytics' => [ 'category' => 'analytics', 'hosts' => ['google-analytics.com', 'googletagmanager.com', 'analytics.google.com'], 'names' => ['_ga', '_gid', '_gat', 'cfz_google-analytics'], 'about' => 'Measures visits and how visitors use the site.', ], 'Google Ads' => [ 'category' => 'marketing', 'hosts' => ['googleadservices.com', 'doubleclick.net', 'googlesyndication.com'], 'names' => ['_gcl_', '_gac_', 'cfz_google-ads'], 'about' => 'Links visits and purchases to Google Ads clicks.', ], 'Meta (Facebook)' => [ 'category' => 'marketing', 'hosts' => ['facebook.net', 'facebook.com'], 'names' => ['_fbp', '_fbc', 'cfz_facebook-pixel'], 'about' => 'Links visits and purchases to Meta ads.', ], 'TikTok' => [ 'category' => 'marketing', 'hosts' => ['tiktok.com', 'tiktokw.us'], 'names' => ['_ttp', '_tt_enable_cookie', 'ttclid', 'cfz_tiktok'], 'about' => 'Links visits and purchases to TikTok ads.', ], 'Snapchat' => [ 'category' => 'marketing', 'hosts' => ['snapchat.com', 'sc-static.net'], 'names' => ['_scid', 'sc_at', 'cfz_snapchat'], 'about' => 'Links visits and purchases to Snapchat ads.', ], 'Pinterest' => [ 'category' => 'marketing', 'hosts' => ['pinterest.com', 'pinimg.com'], 'names' => ['_pinterest_', '_pin_unauth', '_epik', 'cfz_pinterest'], 'about' => 'Links visits and purchases to Pinterest ads.', ], 'Microsoft Advertising (Bing)' => [ 'category' => 'marketing', 'hosts' => ['bat.bing.com', 'bing.com'], 'names' => ['_uetsid', '_uetvid', '_uetmsclkid', 'msclkid', 'cfz_bing'], 'about' => 'Links visits and purchases to Microsoft ads.', ], 'Microsoft Clarity' => [ 'category' => 'analytics', 'hosts' => ['clarity.ms'], 'names' => ['_clck', '_clsk'], 'about' => 'Session recordings and heatmaps.', ], 'LinkedIn' => [ 'category' => 'marketing', 'hosts' => ['linkedin.com', 'licdn.com'], 'names' => ['li_', 'lidc', 'bcookie', 'cfz_linkedin'], 'about' => 'Links visits to LinkedIn ads.', ], 'Instagram' => [ 'category' => 'marketing', 'hosts' => ['instagram.com', 'cdninstagram.com'], 'names' => [], 'about' => 'Embedded Instagram content loaded from Instagram.', ], 'YouTube' => [ 'category' => 'marketing', 'hosts' => ['youtube.com', 'youtube-nocookie.com', 'ytimg.com'], 'names' => ['YSC', 'VISITOR_INFO1_LIVE'], 'about' => 'Embedded YouTube videos.', ], 'Gravatar' => [ 'category' => 'functional', 'hosts' => ['gravatar.com', 'wp.com'], 'names' => [], 'about' => 'Profile pictures for comments and users, loaded from Gravatar (Automattic).', ], 'Google Fonts' => [ 'category' => 'functional', 'hosts' => ['fonts.googleapis.com', 'fonts.gstatic.com'], 'names' => [], 'about' => 'Fonts loaded from Google; sends the visitor\'s IP address to Google.', ], 'Cloudflare Zaraz (other tool storage)' => [ 'category' => 'functional', 'hosts' => [], 'names' => ['cfz_'], 'about' => 'Data a Zaraz tool keeps for the visitor.', ], ]; /** * Runs the scan. * * @param array $browser What the admin's browser saw: ['cookies' => string[], 'local' => string[], * 'session' => string[], 'hosts' => string[], 'consent' => string] * @param string $url Page to fetch from the server (defaults to the homepage) * @return array */ public function scan(array $browser = [], string $url = ''): array { $targetUrl = !empty($url) ? esc_url_raw($url) : home_url('/'); $items = array_fill_keys(self::CATEGORIES, []); // 1. Cookies the homepage sets in its HTTP response (includes HttpOnly cookies) $serverNote = ''; $response = wp_remote_get($targetUrl, [ 'timeout' => 15, 'user-agent' => 'Zbridge-Cookie-Scanner/1.0 (+https://wp-zbridge.com)', ]); if (is_wp_error($response)) { $serverNote = $response->get_error_message(); } else { foreach ($this->extractSetCookies(wp_remote_retrieve_headers($response)) as $name) { $this->add($items, $name, 'Cookie (set by the server)'); } } // 2. What the browser saw foreach ((array) ($browser['cookies'] ?? []) as $name) { $this->add($items, (string) $name, 'Cookie'); } foreach ((array) ($browser['local'] ?? []) as $name) { $this->add($items, (string) $name, 'localStorage'); } foreach ((array) ($browser['session'] ?? []) as $name) { $this->add($items, (string) $name, 'sessionStorage'); } foreach ((array) ($browser['hosts'] ?? []) as $host) { $this->addHost($items, (string) $host); } $counts = []; foreach (self::CATEGORIES as $category) { $counts[$category] = count($items[$category]); } $counts['total'] = array_sum($counts); return [ 'success' => true, 'url' => $targetUrl, 'timestamp' => current_time('mysql'), 'browser_scanned' => !empty($browser), 'server_note' => $serverNote, 'consent' => (string) ($browser['consent'] ?? ''), 'counts' => $counts, 'items' => $items, ]; } /** * Classifies a cookie or storage name by prefix. * * @param string $name * @return array ['provider', 'category', 'about'] or provider '' when not recognised */ public static function classifyName(string $name): array { $best = null; $bestLength = 0; foreach (self::KNOWN_PROVIDERS as $provider => $config) { foreach ($config['names'] as $prefix) { // Longest matching prefix wins, so cfz_google-ads beats cfz_ if (stripos($name, $prefix) === 0 && strlen($prefix) > $bestLength) { $best = ['provider' => $provider, 'category' => $config['category'], 'about' => $config['about']]; $bestLength = strlen($prefix); } } } return $best ?? ['provider' => '', 'category' => 'unrecognised', 'about' => '']; } /** * Classifies a request host by domain suffix. * * @param string $host * @return array ['provider', 'category', 'about'] or provider '' when not recognised */ public static function classifyHost(string $host): array { $host = strtolower($host); foreach (self::KNOWN_PROVIDERS as $provider => $config) { foreach ($config['hosts'] as $suffix) { if ($host === $suffix || substr($host, -strlen('.' . $suffix)) === '.' . $suffix) { return ['provider' => $provider, 'category' => $config['category'], 'about' => $config['about']]; } } } return ['provider' => '', 'category' => 'unrecognised', 'about' => '']; } /** * @param array &$items * @param string $name * @param string $type */ private function add(array &$items, string $name, string $type): void { $name = trim($name); if ($name === '') { return; } $match = self::classifyName($name); $key = $type . ':' . $name; foreach ($items as $existing) { if (isset($existing[$key])) { return; } } $items[$match['category']][$key] = [ 'name' => $name, 'provider' => $match['provider'] ?: 'Not recognised', 'category' => $match['category'], 'type' => $type, 'description' => $match['about'] ?: 'Not in WP Zbridge\'s list. Check which plugin, theme or script sets it.', ]; } /** * @param array &$items * @param string $host */ private function addHost(array &$items, string $host): void { $host = strtolower(trim($host)); if ($host === '') { return; } $match = self::classifyHost($host); $key = 'host:' . $host; $items[$match['category']][$key] = [ 'name' => $host, 'provider' => $match['provider'] ?: 'Not recognised', 'category' => $match['category'], 'type' => 'Third-party request', 'description' => $match['about'] ?: 'The page loaded something from this site. Check which plugin, theme or embed uses it.', ]; } /** * Extracts cookie names from HTTP response headers. * * @param mixed $headers * @return array */ private function extractSetCookies($headers): array { $cookies = []; $headerValue = $headers['set-cookie'] ?? null; if (!$headerValue) { return $cookies; } $rawList = is_array($headerValue) ? $headerValue : [$headerValue]; foreach ($rawList as $raw) { if (preg_match('/^([^=;]+)=/i', trim((string) $raw), $m)) { $cookies[] = trim($m[1]); } } return array_values(array_unique($cookies)); } }