*/ public static function categories() { $categories = array( 'necessary' => array( 'label' => __( 'Necessary', 'ariconsent' ), 'description' => __( 'Needed for the website to work, for example to keep you logged in, remember your cookie choice and keep the site secure. These cannot be switched off.', 'ariconsent' ), 'required' => true, ), 'preferences' => array( 'label' => __( 'Preferences', 'ariconsent' ), 'description' => __( 'Remember choices you make, such as language or region, to give you a more personal experience.', 'ariconsent' ), 'required' => false, ), 'statistics' => array( 'label' => __( 'Statistics', 'ariconsent' ), 'description' => __( 'Help us understand how visitors use the website so we can improve it.', 'ariconsent' ), 'required' => false, ), 'marketing' => array( 'label' => __( 'Marketing', 'ariconsent' ), 'description' => __( 'Used by us and our partners to show relevant ads and measure campaigns, and by embedded content such as videos and maps.', 'ariconsent' ), 'required' => false, ), ); /** * Filters the consent categories. * * @param array $categories Categories keyed by slug. */ return apply_filters( 'ariconsent_categories', $categories ); } /** * Slugs of categories visitors can choose. * * @return string[] */ public static function optional_categories() { $out = array(); foreach ( self::categories() as $slug => $category ) { if ( empty( $category['required'] ) ) { $out[] = $slug; } } return $out; } /** * Built-in service definitions. * * Patterns are case-insensitive substrings matched against a script's src, an inline * script's body, or an iframe's src. * * @return array> */ public static function builtin() { $year = __( '1 year', 'ariconsent' ); $two = __( '2 years', 'ariconsent' ); $months = static function ( $n ) { /* translators: %d: number of months. */ return sprintf( _n( '%d month', '%d months', $n, 'ariconsent' ), $n ); }; $session = __( 'Session', 'ariconsent' ); $services = array( 'google-analytics' => array( 'name' => 'Google Analytics', 'provider' => 'Google Ireland Ltd.', 'category' => 'statistics', 'privacy' => 'https://policies.google.com/privacy', 'google' => true, 'scripts' => array( 'google-analytics.com/analytics.js', 'googletagmanager.com/gtag/js?id=G-', 'googletagmanager.com/gtag/js?id=UA-', "gtag('config', 'G-", 'gtag("config", "G-', 'ga(\'create\'' ), 'iframes' => array(), 'cookies' => array( array( '_ga', $two, __( 'Distinguishes visitors.', 'ariconsent' ) ), array( '_ga_*', $two, __( 'Keeps the session state.', 'ariconsent' ) ), ), ), 'google-tag-manager' => array( 'name' => 'Google Tag Manager', 'provider' => 'Google Ireland Ltd.', 'category' => 'statistics', 'privacy' => 'https://policies.google.com/privacy', 'google' => true, 'scripts' => array( 'googletagmanager.com/gtm.js', 'googletagmanager.com/gtag/js?id=GT-' ), 'iframes' => array( 'googletagmanager.com/ns.html' ), 'cookies' => array(), ), 'google-ads' => array( 'name' => 'Google Ads', 'provider' => 'Google Ireland Ltd.', 'category' => 'marketing', 'privacy' => 'https://policies.google.com/privacy', 'google' => true, 'scripts' => array( 'googleadservices.com', 'googlesyndication.com', 'googletagmanager.com/gtag/js?id=AW-', 'doubleclick.net' ), 'iframes' => array( 'doubleclick.net' ), 'cookies' => array( array( '_gcl_au', $months( 3 ), __( 'Measures ad conversions.', 'ariconsent' ) ), array( 'IDE', $months( 13 ), __( 'Shows and measures personalised ads.', 'ariconsent' ) ), ), ), 'meta-pixel' => array( 'name' => 'Meta Pixel (Facebook)', 'provider' => 'Meta Platforms Ireland Ltd.', 'category' => 'marketing', 'privacy' => 'https://www.facebook.com/privacy/policy/', 'scripts' => array( 'connect.facebook.net', "fbq('init'", 'fbq("init"' ), 'iframes' => array( 'facebook.com/plugins/', 'facebook.com/tr?' ), 'cookies' => array( array( '_fbp', $months( 3 ), __( 'Measures and targets ads.', 'ariconsent' ) ), array( 'fr', $months( 3 ), __( 'Shows relevant ads.', 'ariconsent' ) ), ), ), 'hotjar' => array( 'name' => 'Hotjar', 'provider' => 'Hotjar Ltd.', 'category' => 'statistics', 'privacy' => 'https://www.hotjar.com/legal/policies/privacy/', 'scripts' => array( 'static.hotjar.com', 'hotjar.com/c/hotjar-' ), 'iframes' => array(), 'cookies' => array( array( '_hjSessionUser_*', $year, __( 'Recognises returning visitors.', 'ariconsent' ) ), array( '_hjSession_*', __( '30 minutes', 'ariconsent' ), __( 'Keeps the current session.', 'ariconsent' ) ), ), ), 'microsoft-clarity' => array( 'name' => 'Microsoft Clarity', 'provider' => 'Microsoft Ireland Operations Ltd.', 'category' => 'statistics', 'privacy' => 'https://privacy.microsoft.com/privacystatement', 'scripts' => array( 'clarity.ms/tag', 'www.clarity.ms' ), 'iframes' => array(), 'cookies' => array( array( '_clck', $year, __( 'Keeps a visitor ID.', 'ariconsent' ) ), array( '_clsk', __( '1 day', 'ariconsent' ), __( 'Connects page views into one session.', 'ariconsent' ) ), ), ), 'microsoft-ads' => array( 'name' => 'Microsoft Advertising (UET)', 'provider' => 'Microsoft Ireland Operations Ltd.', 'category' => 'marketing', 'privacy' => 'https://privacy.microsoft.com/privacystatement', 'scripts' => array( 'bat.bing.com/bat.js' ), 'iframes' => array(), 'cookies' => array( array( '_uetsid', __( '1 day', 'ariconsent' ), __( 'Measures ad conversions in the current session.', 'ariconsent' ) ), array( '_uetvid', $months( 13 ), __( 'Recognises returning visitors to measure ad conversions.', 'ariconsent' ) ), ), ), 'linkedin-insight' => array( 'name' => 'LinkedIn Insight Tag', 'provider' => 'LinkedIn Ireland Unlimited Company', 'category' => 'marketing', 'privacy' => 'https://www.linkedin.com/legal/privacy-policy', 'scripts' => array( 'snap.licdn.com', '_linkedin_partner_id' ), 'iframes' => array(), 'cookies' => array( array( 'li_sugr', $months( 3 ), __( 'Matches visitors to LinkedIn members for ads.', 'ariconsent' ) ), array( 'bcookie', $year, __( 'Identifies the browser.', 'ariconsent' ) ), ), ), 'tiktok-pixel' => array( 'name' => 'TikTok Pixel', 'provider' => 'TikTok Technology Ltd.', 'category' => 'marketing', 'privacy' => 'https://www.tiktok.com/legal/privacy-policy-eea', 'scripts' => array( 'analytics.tiktok.com', 'ttq.load(' ), 'iframes' => array(), 'cookies' => array( array( '_ttp', $months( 13 ), __( 'Measures and targets ads.', 'ariconsent' ) ), ), ), 'pinterest-tag' => array( 'name' => 'Pinterest Tag', 'provider' => 'Pinterest Europe Ltd.', 'category' => 'marketing', 'privacy' => 'https://policy.pinterest.com/privacy-policy', 'scripts' => array( 's.pinimg.com/ct/core.js', "pintrk('load'" ), 'iframes' => array(), 'cookies' => array( array( '_pin_unauth', $year, __( 'Measures ad conversions.', 'ariconsent' ) ), ), ), 'snap-pixel' => array( 'name' => 'Snap Pixel', 'provider' => 'Snap Group Ltd.', 'category' => 'marketing', 'privacy' => 'https://values.snap.com/privacy/privacy-policy', 'scripts' => array( 'sc-static.net/scevent.min.js' ), 'iframes' => array(), 'cookies' => array( array( '_scid', $months( 13 ), __( 'Measures ad conversions.', 'ariconsent' ) ), ), ), 'matomo-cloud' => array( 'name' => 'Matomo', 'provider' => 'InnoCraft Ltd.', 'category' => 'statistics', 'privacy' => 'https://matomo.org/privacy-policy/', 'scripts' => array( 'matomo.cloud', 'cdn.matomo.cloud', '/matomo.js', '/piwik.js' ), 'iframes' => array(), 'cookies' => array( array( '_pk_id.*', $months( 13 ), __( 'Distinguishes visitors.', 'ariconsent' ) ), array( '_pk_ses.*', __( '30 minutes', 'ariconsent' ), __( 'Keeps the current visit.', 'ariconsent' ) ), ), ), 'youtube' => array( 'name' => 'YouTube', 'provider' => 'Google Ireland Ltd.', 'category' => 'marketing', 'privacy' => 'https://policies.google.com/privacy', 'scripts' => array( 'youtube.com/iframe_api' ), 'iframes' => array( 'youtube.com/embed', 'youtube-nocookie.com/embed' ), // Elementor's video widget: an empty element that the YouTube API fills in. 'players' => array( '.elementor-widget-video div.elementor-video' ), 'markers' => array( '"video_type":"youtube"' ), 'cookies' => array( array( 'VISITOR_INFO1_LIVE', $months( 6 ), __( 'Estimates bandwidth and remembers settings.', 'ariconsent' ) ), array( 'YSC', $session, __( 'Counts views of embedded videos.', 'ariconsent' ) ), ), ), 'jetpack-stats' => array( 'name' => 'Jetpack Stats', 'provider' => 'Automattic Inc.', 'category' => 'statistics', 'privacy' => 'https://automattic.com/privacy/', 'scripts' => array( 'stats.wp.com/e-', 'stats.wp.com/w.js', '_stq.push(' ), 'iframes' => array(), 'cookies' => array(), ), 'woo-attribution' => array( 'name' => 'WooCommerce Order Attribution', 'provider' => __( 'This website (WooCommerce)', 'ariconsent' ), // Records which campaign or site a customer came from, to credit orders to marketing channels. 'category' => 'marketing', 'privacy' => '', // Sourcebuster and the attribution script that depends on it, unblocked together in page order. 'scripts' => array( 'woocommerce/assets/js/sourcebuster/', 'woocommerce/assets/js/frontend/order-attribution' ), // With the WP Consent API plugin active, WooCommerce waits for marketing consent itself, and // its own consent script expects the attribution script to be loaded. Blocking it would then // only cause errors, so AriConsent leaves it to WooCommerce. 'handles_consent' => class_exists( 'WP_CONSENT_API' ), 'iframes' => array(), 'cookies' => array( array( 'sbjs_*', $session, __( 'Remembers where the visitor came from, to credit an order to a campaign.', 'ariconsent' ) ), ), ), 'google-recaptcha' => array( 'name' => 'Google reCAPTCHA', 'provider' => 'Google Ireland Ltd.', // Protects forms against spam; blocking it until consent would break those forms. // Owners who see it differently can move it to another category under Services. 'category' => 'necessary', 'privacy' => 'https://policies.google.com/privacy', 'scripts' => array( 'google.com/recaptcha/', 'recaptcha.net/recaptcha/' ), 'iframes' => array( 'google.com/recaptcha/', 'recaptcha.net/recaptcha/' ), 'cookies' => array( array( '_GRECAPTCHA', $months( 6 ), __( 'Tells people and spam bots apart to protect forms.', 'ariconsent' ) ), ), ), 'vimeo' => array( 'name' => 'Vimeo', 'provider' => 'Vimeo.com, Inc.', 'category' => 'marketing', 'privacy' => 'https://vimeo.com/legal/privacy/policy', 'scripts' => array( 'player.vimeo.com/api/player.js' ), 'iframes' => array( 'player.vimeo.com/video' ), 'cookies' => array( array( 'vuid', $two, __( 'Collects viewing statistics.', 'ariconsent' ) ), ), ), 'google-maps' => array( 'name' => 'Google Maps', 'provider' => 'Google Ireland Ltd.', 'category' => 'marketing', 'privacy' => 'https://policies.google.com/privacy', 'scripts' => array( 'maps.googleapis.com/maps/api/js' ), 'iframes' => array( 'google.com/maps/embed', 'maps.google.com/maps?', 'google.com/maps?' ), 'cookies' => array(), ), 'spotify' => array( 'name' => 'Spotify', 'provider' => 'Spotify AB', 'category' => 'marketing', 'privacy' => 'https://www.spotify.com/legal/privacy-policy/', 'scripts' => array(), 'iframes' => array( 'open.spotify.com/embed' ), 'cookies' => array(), ), 'x-twitter' => array( 'name' => 'X (Twitter) embeds', 'provider' => 'Twitter International Unlimited Company', 'category' => 'marketing', 'privacy' => 'https://x.com/privacy', 'scripts' => array( 'platform.twitter.com/widgets.js', 'platform.x.com/widgets.js' ), 'iframes' => array( 'platform.twitter.com/embed' ), 'cookies' => array(), ), 'instagram' => array( 'name' => 'Instagram embeds', 'provider' => 'Meta Platforms Ireland Ltd.', 'category' => 'marketing', 'privacy' => 'https://privacycenter.instagram.com/policy', 'scripts' => array( 'instagram.com/embed.js' ), 'iframes' => array( 'instagram.com/p/', 'instagram.com/reel/' ), 'cookies' => array(), ), ); foreach ( $services as $id => $service ) { $services[ $id ]['id'] = $id; $services[ $id ]['builtin'] = true; $services[ $id ]['google'] = ! empty( $service['google'] ); $services[ $id ]['players'] = isset( $service['players'] ) ? (array) $service['players'] : array(); $services[ $id ]['markers'] = isset( $service['markers'] ) ? (array) $service['markers'] : array(); } /** * Filters the built-in services. AriConsent Pro adds more here. * * @param array $services Services keyed by ID. */ return apply_filters( 'ariconsent_builtin_services', $services ); } /** * Active services: built-in ones the site owner has enabled (with any category override) * plus custom services. * * @return array> */ public static function active() { $settings = Settings::get(); $categories = self::categories(); $out = array(); foreach ( self::builtin() as $id => $service ) { $state = isset( $settings['services'][ $id ] ) ? $settings['services'][ $id ] : array(); if ( isset( $state['enabled'] ) && ! $state['enabled'] ) { continue; } if ( ! empty( $state['category'] ) && isset( $categories[ $state['category'] ] ) ) { $service['category'] = $state['category']; } $out[ $id ] = $service; } foreach ( $settings['custom_services'] as $custom ) { $out[ $custom['id'] ] = $custom; } return $out; } /** * Services to show visitors (dialog and declaration): only those actually in use, * i.e. found while blocking pages, found by the admin scan, added by an active plugin, * or added by the owner. * Listing every known service would make the disclosure inaccurate. * * @return array> */ public static function listed() { $seen = array_merge( (array) get_option( 'ariconsent_seen', array() ), (array) get_option( 'ariconsent_detected', array() ), array_keys( Integrations::detected() ) ); $out = array(); foreach ( self::active() as $id => $service ) { if ( empty( $service['builtin'] ) || in_array( $id, $seen, true ) ) { $out[ $id ] = $service; } } return $out; } /** * Services the visitor turned off inside a category they allowed, cleaned for the cookie and the log. * * Only when the owner lets visitors choose single services. Unknown IDs, necessary services and * services of categories that are refused anyway are dropped. * * @param mixed $raw Service IDs from the request. * @param array $categories Optional category => granted. * @return string[] Sorted service IDs. */ public static function services_off( $raw, array $categories ) { if ( ! Settings::get()['service_choice'] || ! is_array( $raw ) ) { return array(); } $raw = array_map( 'strval', array_filter( $raw, 'is_scalar' ) ); $out = array(); foreach ( self::active() as $id => $service ) { if ( in_array( (string) $id, $raw, true ) && ! empty( $categories[ $service['category'] ] ) ) { $out[] = (string) $id; } } sort( $out ); return $out; } /** * Records services found on a page. Writes only when a new one appears. * * @param string[] $ids Service IDs. */ public static function remember( array $ids ) { if ( ! $ids ) { return; } $seen = (array) get_option( 'ariconsent_seen', array() ); $new = array_diff( array_values( $ids ), $seen ); if ( $new ) { update_option( 'ariconsent_seen', array_values( array_unique( array_merge( $seen, $new ) ) ), false ); } } /** * Finds the service whose patterns match a script src or body. * * @param string $haystack Script src or inline body. * @param string $kind 'scripts' or 'iframes'. * @param array $services Services to check (defaults to active()). * @return array|null */ public static function match( $haystack, $kind = 'scripts', $services = null ) { $services = null === $services ? self::active() : $services; $haystack = strtolower( $haystack ); foreach ( $services as $service ) { foreach ( (array) $service[ $kind ] as $pattern ) { if ( '' !== $pattern && false !== strpos( $haystack, strtolower( $pattern ) ) ) { return $service; } } } return null; } /** * Lists active services that appear in a piece of HTML. Used by the admin "Scan" button. * * @param string $html HTML. * @return string[] Service IDs. */ public static function detect( $html ) { $found = array(); $html = strtolower( $html ); foreach ( self::builtin() as $id => $service ) { foreach ( array_merge( (array) $service['scripts'], (array) $service['iframes'], (array) $service['markers'] ) as $pattern ) { if ( '' !== $pattern && false !== strpos( $html, strtolower( $pattern ) ) ) { $found[] = $id; break; } } } return $found; } /** * Services present on a page that the blocker does not report: necessary services (never * blocked, but still disclosed) and services revealed only by a marker, such as a page * builder's video widget whose player is created later by script. * * @param string $html Page HTML. * @param array $services Active services. * @return string[] Service IDs. */ public static function present_in( $html, array $services ) { $lower = null; $found = array(); foreach ( $services as $id => $service ) { $patterns = isset( $service['markers'] ) ? (array) $service['markers'] : array(); if ( isset( $service['category'] ) && 'necessary' === $service['category'] ) { $patterns = array_merge( $patterns, (array) $service['scripts'], (array) $service['iframes'] ); } if ( ! $patterns ) { continue; } if ( null === $lower ) { $lower = strtolower( $html ); } foreach ( $patterns as $pattern ) { if ( '' !== $pattern && false !== strpos( $lower, strtolower( $pattern ) ) ) { $found[] = (string) $id; break; } } } return $found; } }