Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion projects/packages/cookie-consent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,36 @@ clear `jetpack_cookie_consent_consent_log_db_version`, call:

## Configuration

Filter `jetpack_cookie_consent_config` to override defaults (geo API URL, GDPR/CCPA region lists, cookie policy URL, and the Tracks `event_prefix`). The Tracks event prefix defaults to `jetpack`; set it to `woocommerceanalytics` to keep continuity with the WooCommerce/Unified Analytics Tracks stream.
Filter `jetpack_cookie_consent_config` to override defaults. Geo controls are grouped under `geo`:

```php
add_filter(
'jetpack_cookie_consent_config',
static function ( $config ) {
$config['geo'] = array_merge(
$config['geo'],
array(
'provider' => 'custom',
'api_url' => 'https://example.com/geo/',
'country_code_cookie' => 'shopper_country',
'region_cookie' => 'shopper_region',
'cookie_duration' => 6 * HOUR_IN_SECONDS,
'gdpr_countries' => array( 'GB', 'FR' ),
'ccpa_regions' => array( 'california' ),
'show_on_error' => true,
)
);

$config['event_prefix'] = 'woocommerceanalytics';

return $config;
}
);
```

The default geo provider is `wpcom`, which resolves shoppers through `https://public-api.wordpress.com/geo/`. Set `geo.provider` to `custom` and provide `geo.api_url` to use a different source. The endpoint is fetched client-side with `cache: 'no-store'`, must be reachable from the browser, and must return JSON with `country_short` as a two-letter country code and `region` as a region/state name. The configured `geo.country_code_cookie` and `geo.region_cookie` values are written as host-only cookies and ignored by Jetpack Boost's page-cache key.

The Tracks event prefix defaults to `jetpack`; set it to `woocommerceanalytics` to keep continuity with the WooCommerce/Unified Analytics Tracks stream.

User-facing banner, preferences modal, footer link, CCPA page, and CCPA snackbar strings are configured through the `copy` group. Package defaults are translated with the `jetpack-cookie-consent` text domain. Consumers that override strings should translate those overrides before returning them from the filter, using their own text domain:

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: added

Geo: Add configurable provider and region settings.
295 changes: 210 additions & 85 deletions projects/packages/cookie-consent/src/class-cookie-consent.php
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,8 @@ public static function register_ccpa_page_setting() {
*/
public static function ignore_geo_cookies_in_page_cache( $cookies ) {
$config = self::get_config();
$country_code = $config['country_code_cookie'] ?? 'country_code';
$region = $config['region_cookie'] ?? 'region';
$country_code = $config['geo']['country_code_cookie'];
$region = $config['geo']['region_cookie'];
$cookies[] = preg_quote( $country_code, '/' );
$cookies[] = preg_quote( $region, '/' );

Expand Down Expand Up @@ -762,91 +762,214 @@ private static function normalize_copy( $copy, $defaults ) {
}

/**
* Get configuration with filters
* Get default GDPR country list.
*
* @return array Configuration array
* @return string[] Country codes where opt-in consent applies.
*/
private static function get_config() {
$default_copy = self::get_default_copy();
$default_config = array(
'geo_api_url' => 'https://public-api.wordpress.com/geo/',
'geo_cookie_duration' => 6 * HOUR_IN_SECONDS, // 6 hours.
private static function get_default_gdpr_countries() {
return array(
// European Member countries.
'AT', // Austria.
'BE', // Belgium.
'BG', // Bulgaria.
'CY', // Cyprus.
'CZ', // Czech Republic.
'DE', // Germany.
'DK', // Denmark.
'EE', // Estonia.
'ES', // Spain.
'FI', // Finland.
'FR', // France.
'GR', // Greece.
'HR', // Croatia.
'HU', // Hungary.
'IE', // Ireland.
'IT', // Italy.
'LT', // Lithuania.
'LU', // Luxembourg.
'LV', // Latvia.
'MT', // Malta.
'NL', // Netherlands.
'PL', // Poland.
'PT', // Portugal.
'RO', // Romania.
'SE', // Sweden.
'SI', // Slovenia.
'SK', // Slovakia.
'GB', // United Kingdom.
// Single Market Countries that GDPR applies to.
'CH', // Switzerland.
'IS', // Iceland.
'LI', // Liechtenstein.
'NO', // Norway.
);
}

/**
* Get default CCPA-style region list.
*
* @return string[] Lower-case region names where opt-out consent applies.
*/
private static function get_default_ccpa_regions() {
return array(
/* US regions/states that are treated like California for Do Not Sell requests. */
'california',
'utah',
'virginia',
'colorado',
'connecticut',
'texas',
'tennessee',
'oregon',
'new jersey',
'montana',
'iowa',
'indiana',
'delaware',
);
}

/**
* Get default geo provider configuration.
*
* @return array Geo configuration.
*/
private static function get_default_geo_config() {
return array(
'provider' => 'wpcom',
'api_url' => 'https://public-api.wordpress.com/geo/',
'country_code_cookie' => 'country_code',
'region_cookie' => 'region',
'cookie_policy_url' => 'https://automattic.com/cookies/',
'gdpr_countries' => array(
// European Member countries.
'AT', // Austria.
'BE', // Belgium.
'BG', // Bulgaria.
'CY', // Cyprus.
'CZ', // Czech Republic.
'DE', // Germany.
'DK', // Denmark.
'EE', // Estonia.
'ES', // Spain.
'FI', // Finland.
'FR', // France.
'GR', // Greece.
'HR', // Croatia.
'HU', // Hungary.
'IE', // Ireland.
'IT', // Italy.
'LT', // Lithuania.
'LU', // Luxembourg.
'LV', // Latvia.
'MT', // Malta.
'NL', // Netherlands.
'PL', // Poland.
'PT', // Portugal.
'RO', // Romania.
'SE', // Sweden.
'SI', // Slovenia.
'SK', // Slovakia.
'GB', // United Kingdom.
// Single Market Countries that GDPR applies to.
'CH', // Switzerland.
'IS', // Iceland.
'LI', // Liechtenstein.
'NO', // Norway.
),
'ccpa_regions' => array(
/* US regions/states that are treated like California for Do Not Sell requests. */
'california',
'utah',
'virginia',
'colorado',
'connecticut',
'texas',
'tennessee',
'oregon',
'new jersey',
'montana',
'iowa',
'indiana',
'delaware',
),
'cookie_duration' => 6 * HOUR_IN_SECONDS, // 6 hours.
'gdpr_countries' => self::get_default_gdpr_countries(),
'ccpa_regions' => self::get_default_ccpa_regions(),
'show_on_error' => true, // Show banner if geolocation fails.
);
}

/**
* Get default configuration.
*
* Legacy top-level geo keys are included so existing filters that append to the
* defaults keep working. normalize_config() folds them into the nested geo schema.
*
* @return array Configuration array.
*/
private static function get_default_config() {
$default_copy = self::get_default_copy();
$geo_config = self::get_default_geo_config();

return array(
'geo' => $geo_config,
'geo_provider' => $geo_config['provider'],
'geo_api_url' => $geo_config['api_url'],
'geo_cookie_duration' => $geo_config['cookie_duration'],
'country_code_cookie' => $geo_config['country_code_cookie'],
'region_cookie' => $geo_config['region_cookie'],
'gdpr_countries' => $geo_config['gdpr_countries'],
'ccpa_regions' => $geo_config['ccpa_regions'],
'show_on_error' => $geo_config['show_on_error'],
'gdpr_honors_gpc' => true, // Honor a Global Privacy Control signal as an opt-out in GDPR regions.
'cookie_policy_url' => 'https://automattic.com/cookies/',
'event_prefix' => 'jetpack', // Tracks event name prefix; set to 'woocommerceanalytics' for Unified Analytics continuity.
'copy' => $default_copy,
);
}

/**
* Filter cookie consent configuration
*
* @param array $config Configuration array
*/
$config = apply_filters( 'jetpack_cookie_consent_config', $default_config );
/**
* Normalize GDPR country codes for case-insensitive matching.
*
* @param string[] $countries Country codes.
* @return string[] Upper-case country codes.
*/
private static function normalize_gdpr_countries( $countries ) {
return array_map( 'strtoupper', array_values( $countries ) );
}

/**
* Normalize CCPA region names for case-insensitive matching.
*
* @param string[] $regions Region names.
* @return string[] Lower-case region names.
*/
private static function normalize_ccpa_regions( $regions ) {
return array_map( 'strtolower', array_values( $regions ) );
}

/**
* Normalize filtered configuration into the current schema.
*
* @param array $config Filtered configuration.
* @param array $default_config Default configuration.
* @return array Normalized configuration.
*/
private static function normalize_config( $config, $default_config ) {
if ( ! is_array( $config ) ) {
$config = $default_config;
$config = array();
}

$config['copy'] = self::normalize_copy( $config['copy'] ?? array(), $default_copy );
$nested_geo = isset( $config['geo'] ) && is_array( $config['geo'] ) ? $config['geo'] : array();
$geo = array_merge( $default_config['geo'], $nested_geo );

$legacy_geo_keys = array(
'geo_provider' => 'provider',
'geo_api_url' => 'api_url',
'geo_cookie_duration' => 'cookie_duration',
'country_code_cookie' => 'country_code_cookie',
'region_cookie' => 'region_cookie',
'gdpr_countries' => 'gdpr_countries',
'ccpa_regions' => 'ccpa_regions',
'show_on_error' => 'show_on_error',
);

foreach ( $legacy_geo_keys as $legacy_key => $geo_key ) {
$has_nested_override = array_key_exists( $geo_key, $nested_geo );
if ( ! $has_nested_override && array_key_exists( $legacy_key, $config ) && $config[ $legacy_key ] !== $default_config[ $legacy_key ] ) {
$geo[ $geo_key ] = $config[ $legacy_key ];
}
}

if ( ! in_array( $geo['provider'], array( 'wpcom', 'custom' ), true ) ) {
$geo['provider'] = 'wpcom';
$geo['api_url'] = $default_config['geo']['api_url'];
}
if ( ! is_string( $geo['api_url'] ) ) {
$geo['api_url'] = '';
}
if ( 'wpcom' === $geo['provider'] && '' === $geo['api_url'] ) {
$geo['api_url'] = $default_config['geo']['api_url'];
}
Comment thread
Copilot marked this conversation as resolved.
$geo['country_code_cookie'] = is_string( $geo['country_code_cookie'] ) && '' !== $geo['country_code_cookie'] ? $geo['country_code_cookie'] : $default_config['geo']['country_code_cookie'];
$geo['region_cookie'] = is_string( $geo['region_cookie'] ) && '' !== $geo['region_cookie'] ? $geo['region_cookie'] : $default_config['geo']['region_cookie'];
$geo['cookie_duration'] = is_numeric( $geo['cookie_duration'] ) ? (int) $geo['cookie_duration'] : $default_config['geo']['cookie_duration'];
$geo['gdpr_countries'] = is_array( $geo['gdpr_countries'] ) ? self::normalize_gdpr_countries( $geo['gdpr_countries'] ) : $default_config['geo']['gdpr_countries'];
$geo['ccpa_regions'] = is_array( $geo['ccpa_regions'] ) ? self::normalize_ccpa_regions( $geo['ccpa_regions'] ) : $default_config['geo']['ccpa_regions'];
$geo['show_on_error'] = (bool) $geo['show_on_error'];

$config['geo'] = $geo;
$config['cookie_policy_url'] = $config['cookie_policy_url'] ?? $default_config['cookie_policy_url'];
$config['event_prefix'] = $config['event_prefix'] ?? $default_config['event_prefix'];
$config['copy'] = self::normalize_copy( $config['copy'] ?? array(), $default_config['copy'] );

return $config;
}

/**
* Get configuration with filters.
*
* @return array Configuration array.
*/
private static function get_config() {
$default_config = self::get_default_config();

/**
* Filter cookie consent configuration
*
* @param array $config Configuration array
*/
return self::normalize_config( apply_filters( 'jetpack_cookie_consent_config', $default_config ), $default_config );
}

/**
* Enqueue scripts and styles
*/
Expand Down Expand Up @@ -919,22 +1042,24 @@ public static function enqueue_assets() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$force_preview = isset( $_GET['preview_cookie_consent'] ) && '1' === $_GET['preview_cookie_consent'];

// Pass configuration to frontend using wp_interactivity_config.
wp_interactivity_config(
'jetpack/cookie-consent',
array(
'geoApiUrl' => $config['geo_api_url'],
'geoCookieDuration' => $config['geo_cookie_duration'],
'countryCodeCookie' => $config['country_code_cookie'],
'regionCookie' => $config['region_cookie'],
'cookiePolicyUrl' => $config['cookie_policy_url'],
'gdprCountries' => $config['gdpr_countries'],
'ccpaRegions' => $config['ccpa_regions'],
'gdprHonorsGpc' => $config['gdpr_honors_gpc'] ?? true,
'showOnError' => $config['show_on_error'],
'forcePreview' => $force_preview,
)
$frontend_config = array(
'cookiePolicyUrl' => $config['cookie_policy_url'],
'gdprHonorsGpc' => $config['gdpr_honors_gpc'] ?? true,
'forcePreview' => $force_preview,
'geo' => array(
'provider' => $config['geo']['provider'],
'apiUrl' => $config['geo']['api_url'],
'countryCodeCookie' => $config['geo']['country_code_cookie'],
'regionCookie' => $config['geo']['region_cookie'],
'cookieDuration' => $config['geo']['cookie_duration'],
'gdprCountries' => $config['geo']['gdpr_countries'],
'ccpaRegions' => $config['geo']['ccpa_regions'],
'showOnError' => $config['geo']['show_on_error'],
),
);

// Pass configuration to frontend using wp_interactivity_config.
wp_interactivity_config( 'jetpack/cookie-consent', $frontend_config );
}

/**
Expand Down
Loading
Loading