Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
97.16% |
376 / 387 |
|
83.33% |
15 / 18 |
CRAP | |
0.00% |
0 / 1 |
| Config_Schema | |
97.66% |
376 / 385 |
|
83.33% |
15 / 18 |
75 | |
0.00% |
0 / 1 |
| schema | |
100.00% |
131 / 131 |
|
100.00% |
1 / 1 |
1 | |||
| ip_modes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| default_ip_mode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| resolve | |
100.00% |
27 / 27 |
|
100.00% |
1 / 1 |
8 | |||
| defaults_from_schema_node | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| resolve_scalar | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
| resolve_group | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
| resolve_geo | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
14 | |||
| defaults_copy | |
100.00% |
38 / 38 |
|
100.00% |
1 / 1 |
1 | |||
| default_consent_categories | |
100.00% |
27 / 27 |
|
100.00% |
1 / 1 |
2 | |||
| normalize_copy | |
50.00% |
7 / 14 |
|
0.00% |
0 / 1 |
8.12 | |||
| normalize_consent_categories | |
100.00% |
40 / 40 |
|
100.00% |
1 / 1 |
21 | |||
| normalize_consent_category_key | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| normalize_links | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
6.10 | |||
| normalize_gdpr_countries | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
| normalize_ccpa_regions | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
| default_gdpr_countries | |
100.00% |
34 / 34 |
|
100.00% |
1 / 1 |
1 | |||
| default_ccpa_regions | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Declarative configuration schema for Cookie Consent. |
| 4 | * |
| 5 | * @package automattic/jetpack-cookie-consent |
| 6 | */ |
| 7 | |
| 8 | namespace Automattic\Jetpack\CookieConsent; |
| 9 | |
| 10 | if ( ! defined( 'ABSPATH' ) ) { |
| 11 | exit; |
| 12 | } |
| 13 | |
| 14 | /** |
| 15 | * Single source of truth for the package config: shape, types, defaults, and the |
| 16 | * enum lists below. The schema descriptor and every bespoke validator (resolve_geo(), |
| 17 | * Consent_Log_Controller::get_ip_mode()) read their allowed values from the same |
| 18 | * constants, so an enum is declared exactly once. |
| 19 | */ |
| 20 | final class Config_Schema { |
| 21 | |
| 22 | /** |
| 23 | * Allowed geolocation providers. Sourced by the schema and resolve_geo(). |
| 24 | * |
| 25 | * @var string[] |
| 26 | */ |
| 27 | private const GEO_PROVIDERS = array( 'wpcom', 'custom' ); |
| 28 | |
| 29 | /** |
| 30 | * Allowed consent-log IP handling modes. Sourced by the schema and, via |
| 31 | * ip_modes(), by Consent_Log_Controller::get_ip_mode(). |
| 32 | * |
| 33 | * @var string[] |
| 34 | */ |
| 35 | private const IP_MODES = array( 'drop', 'hash', 'truncate', 'raw' ); |
| 36 | |
| 37 | /** |
| 38 | * Default consent-log IP handling mode. Sourced by the schema and default_ip_mode(). |
| 39 | */ |
| 40 | private const DEFAULT_IP_MODE = 'drop'; |
| 41 | |
| 42 | /** |
| 43 | * JSON-Schema-shaped descriptor for the full config. |
| 44 | * |
| 45 | * @return array |
| 46 | */ |
| 47 | public static function schema() { |
| 48 | return array( |
| 49 | 'type' => 'object', |
| 50 | 'properties' => array( |
| 51 | 'enabled' => array( |
| 52 | 'type' => 'boolean', |
| 53 | 'default' => true, |
| 54 | ), |
| 55 | 'schema_version' => array( |
| 56 | 'type' => 'integer', |
| 57 | 'default' => 1, |
| 58 | ), |
| 59 | 'features' => array( |
| 60 | 'type' => 'object', |
| 61 | 'properties' => array( |
| 62 | 'banner' => array( |
| 63 | 'type' => 'boolean', |
| 64 | 'default' => true, |
| 65 | ), |
| 66 | 'ccpa_page' => array( |
| 67 | 'type' => 'boolean', |
| 68 | 'default' => true, |
| 69 | ), |
| 70 | 'footer_links' => array( |
| 71 | 'type' => 'boolean', |
| 72 | 'default' => true, |
| 73 | ), |
| 74 | 'consent_log' => array( |
| 75 | 'type' => 'boolean', |
| 76 | 'default' => true, |
| 77 | ), |
| 78 | 'tracks' => array( |
| 79 | 'type' => 'boolean', |
| 80 | 'default' => true, |
| 81 | ), |
| 82 | 'geo' => array( |
| 83 | 'type' => 'boolean', |
| 84 | 'default' => true, |
| 85 | ), |
| 86 | 'page_deletion_lock' => array( |
| 87 | 'type' => 'boolean', |
| 88 | 'default' => false, |
| 89 | ), |
| 90 | ), |
| 91 | ), |
| 92 | 'geo' => array( |
| 93 | 'type' => 'object', |
| 94 | 'properties' => array( |
| 95 | 'provider' => array( |
| 96 | 'type' => 'string', |
| 97 | 'enum' => self::GEO_PROVIDERS, |
| 98 | 'default' => 'wpcom', |
| 99 | ), |
| 100 | 'api_url' => array( |
| 101 | 'type' => 'string', |
| 102 | 'default' => 'https://public-api.wordpress.com/geo/', |
| 103 | ), |
| 104 | 'country_code_cookie' => array( |
| 105 | 'type' => 'string', |
| 106 | 'default' => 'country_code', |
| 107 | ), |
| 108 | 'region_cookie' => array( |
| 109 | 'type' => 'string', |
| 110 | 'default' => 'region', |
| 111 | ), |
| 112 | 'cookie_duration' => array( |
| 113 | 'type' => 'integer', |
| 114 | 'default' => 6 * HOUR_IN_SECONDS, |
| 115 | ), |
| 116 | 'gdpr_countries' => array( |
| 117 | 'type' => 'array', |
| 118 | 'default' => self::default_gdpr_countries(), |
| 119 | ), |
| 120 | 'ccpa_regions' => array( |
| 121 | 'type' => 'array', |
| 122 | 'default' => self::default_ccpa_regions(), |
| 123 | ), |
| 124 | 'show_on_error' => array( |
| 125 | 'type' => 'boolean', |
| 126 | 'default' => true, |
| 127 | ), |
| 128 | ), |
| 129 | ), |
| 130 | 'gdpr_honors_gpc' => array( |
| 131 | 'type' => 'boolean', |
| 132 | 'default' => true, |
| 133 | ), |
| 134 | 'links' => array( |
| 135 | 'type' => 'object', |
| 136 | 'properties' => array( |
| 137 | 'cookie_policy_url' => array( |
| 138 | 'type' => 'string', |
| 139 | 'default' => '', |
| 140 | ), |
| 141 | ), |
| 142 | ), |
| 143 | 'event_prefix' => array( |
| 144 | 'type' => 'string', |
| 145 | 'default' => 'jetpack', |
| 146 | ), |
| 147 | 'log' => array( |
| 148 | 'type' => 'object', |
| 149 | 'properties' => array( |
| 150 | 'retention_days' => array( |
| 151 | 'type' => 'integer', |
| 152 | 'default' => 30, |
| 153 | ), |
| 154 | 'policy_version' => array( |
| 155 | 'type' => 'string', |
| 156 | 'default' => '1', |
| 157 | ), |
| 158 | 'banner_version' => array( |
| 159 | 'type' => 'string', |
| 160 | 'default' => '1', |
| 161 | ), |
| 162 | 'ip_mode' => array( |
| 163 | 'type' => 'string', |
| 164 | 'enum' => self::IP_MODES, |
| 165 | 'default' => self::DEFAULT_IP_MODE, |
| 166 | ), |
| 167 | ), |
| 168 | ), |
| 169 | 'copy' => array( |
| 170 | 'type' => 'object', |
| 171 | 'default' => self::defaults_copy(), |
| 172 | ), |
| 173 | 'consent' => array( |
| 174 | 'type' => 'object', |
| 175 | // Left empty: resolve() rebuilds categories from the resolved `copy`, so a |
| 176 | // static default here would use default copy and be discarded anyway. |
| 177 | 'default' => array( 'categories' => array() ), |
| 178 | ), |
| 179 | ), |
| 180 | ); |
| 181 | } |
| 182 | |
| 183 | /** |
| 184 | * Allowed consent-log IP handling modes. |
| 185 | * |
| 186 | * @internal For package classes only; not part of the public API. |
| 187 | * |
| 188 | * @return string[] |
| 189 | */ |
| 190 | public static function ip_modes() { |
| 191 | return self::IP_MODES; |
| 192 | } |
| 193 | |
| 194 | /** |
| 195 | * Default consent-log IP handling mode. |
| 196 | * |
| 197 | * @internal For package classes only; not part of the public API. |
| 198 | * |
| 199 | * @return string |
| 200 | */ |
| 201 | public static function default_ip_mode() { |
| 202 | return self::DEFAULT_IP_MODE; |
| 203 | } |
| 204 | |
| 205 | /** |
| 206 | * Resolve a partial consumer config into a fully-defaulted, validated config. |
| 207 | * |
| 208 | * @param mixed $config Partial consumer config; anything other than an array falls back to defaults. |
| 209 | * @return array |
| 210 | */ |
| 211 | public static function resolve( $config = array() ) { |
| 212 | if ( ! is_array( $config ) ) { |
| 213 | $config = array(); |
| 214 | } |
| 215 | |
| 216 | // Build the schema once and index every default and field-schema below into it. |
| 217 | // Each schema() build re-materializes the copy, category, and country/region |
| 218 | // defaults, so resolving off a single materialization avoids rebuilding them per |
| 219 | // field lookup. |
| 220 | $schema = self::schema(); |
| 221 | $properties = $schema['properties']; |
| 222 | $defaults = self::defaults_from_schema_node( $schema ); |
| 223 | |
| 224 | // Scalars, enums, booleans, integers: let the schema coerce them, field by field, |
| 225 | // falling back to the default whenever a value is missing or fails validation. |
| 226 | $features = self::resolve_group( $config['features'] ?? array(), $defaults['features'], $properties['features']['properties'] ); |
| 227 | $geo = self::resolve_geo( $config, $defaults['geo'] ); |
| 228 | // `log` is merged rather than resolved wholesale because `policy_version`/`banner_version` |
| 229 | // keep a deliberately permissive contract: Cookie_Consent::get_log_versions() coerces and |
| 230 | // diagnoses their scalar values (including non-string overrides) downstream. The two fields |
| 231 | // that DO carry a strict schema — `ip_mode` (enum) and `retention_days` (integer) — are |
| 232 | // validated here so a resolved config is schema-valid for them and the filter re-resolve |
| 233 | // re-sanitizes an injected value rather than trusting it verbatim. |
| 234 | $log = isset( $config['log'] ) && is_array( $config['log'] ) ? array_merge( $defaults['log'], $config['log'] ) : $defaults['log']; |
| 235 | $log_properties = $properties['log']['properties']; |
| 236 | $log['ip_mode'] = self::resolve_scalar( $log, 'ip_mode', $defaults['log']['ip_mode'], $log_properties['ip_mode'] ); |
| 237 | $log['retention_days'] = self::resolve_scalar( $log, 'retention_days', $defaults['log']['retention_days'], $log_properties['retention_days'] ); |
| 238 | |
| 239 | $resolved = array( |
| 240 | 'enabled' => self::resolve_scalar( $config, 'enabled', $defaults['enabled'], $properties['enabled'] ), |
| 241 | 'schema_version' => self::resolve_scalar( $config, 'schema_version', $defaults['schema_version'], $properties['schema_version'] ), |
| 242 | 'features' => $features, |
| 243 | 'geo' => $geo, |
| 244 | 'gdpr_honors_gpc' => self::resolve_scalar( $config, 'gdpr_honors_gpc', $defaults['gdpr_honors_gpc'], $properties['gdpr_honors_gpc'] ), |
| 245 | 'links' => self::normalize_links( $config, $defaults['links'] ), |
| 246 | 'event_prefix' => isset( $config['event_prefix'] ) && is_string( $config['event_prefix'] ) ? $config['event_prefix'] : $defaults['event_prefix'], |
| 247 | 'log' => $log, |
| 248 | 'copy' => self::normalize_copy( $config['copy'] ?? array(), $defaults['copy'] ), |
| 249 | ); |
| 250 | |
| 251 | $default_categories = self::default_consent_categories( $resolved['copy'] ); |
| 252 | $consent = isset( $config['consent'] ) && is_array( $config['consent'] ) ? $config['consent'] : array(); |
| 253 | $categories = $consent['categories'] ?? $default_categories; |
| 254 | |
| 255 | $resolved['consent'] = array( 'categories' => self::normalize_consent_categories( $categories, $default_categories ) ); |
| 256 | |
| 257 | return $resolved; |
| 258 | } |
| 259 | |
| 260 | /** |
| 261 | * Recursively pull `default` values out of a schema node. |
| 262 | * |
| 263 | * @param array $node Schema node. |
| 264 | * @return mixed Nested defaults for an object node, or the node's own default. |
| 265 | */ |
| 266 | private static function defaults_from_schema_node( $node ) { |
| 267 | if ( isset( $node['properties'] ) ) { |
| 268 | $defaults = array(); |
| 269 | foreach ( $node['properties'] as $key => $child ) { |
| 270 | $defaults[ $key ] = self::defaults_from_schema_node( $child ); |
| 271 | } |
| 272 | return $defaults; |
| 273 | } |
| 274 | |
| 275 | return $node['default'] ?? null; |
| 276 | } |
| 277 | |
| 278 | /** |
| 279 | * Resolve a single scalar field: sanitize it against its schema, or fall back to the default. |
| 280 | * |
| 281 | * @param array $config Config array that may hold the field. |
| 282 | * @param string $key Field key. |
| 283 | * @param mixed $default Default value to use when the field is missing or invalid. |
| 284 | * @param array $field_schema JSON-Schema descriptor for the field. |
| 285 | * @return mixed |
| 286 | */ |
| 287 | private static function resolve_scalar( $config, $key, $default, $field_schema ) { |
| 288 | if ( ! is_array( $config ) || ! array_key_exists( $key, $config ) ) { |
| 289 | return $default; |
| 290 | } |
| 291 | |
| 292 | $value = $config[ $key ]; |
| 293 | |
| 294 | if ( is_wp_error( rest_validate_value_from_schema( $value, $field_schema, $key ) ) ) { |
| 295 | return $default; |
| 296 | } |
| 297 | |
| 298 | return rest_sanitize_value_from_schema( $value, $field_schema, $key ); |
| 299 | } |
| 300 | |
| 301 | /** |
| 302 | * Resolve every property of an object field via resolve_scalar(). |
| 303 | * |
| 304 | * @param mixed $group Configured group, expected to be an array. |
| 305 | * @param array $defaults Default values for the group, keyed like $properties. |
| 306 | * @param array $properties Schema properties for the group. |
| 307 | * @return array |
| 308 | */ |
| 309 | private static function resolve_group( $group, $defaults, $properties ) { |
| 310 | if ( ! is_array( $group ) ) { |
| 311 | return $defaults; |
| 312 | } |
| 313 | |
| 314 | $resolved = array(); |
| 315 | foreach ( $properties as $key => $field_schema ) { |
| 316 | $resolved[ $key ] = self::resolve_scalar( $group, $key, $defaults[ $key ], $field_schema ); |
| 317 | } |
| 318 | |
| 319 | return $resolved; |
| 320 | } |
| 321 | |
| 322 | /** |
| 323 | * Resolve the `geo` group. |
| 324 | * |
| 325 | * The provider/api_url pair and a couple of the scalar fields need bespoke |
| 326 | * fallback behavior (e.g. an unknown provider resets the api_url too) that a |
| 327 | * plain field-by-field schema validation pass can't express, so this is an |
| 328 | * escape hatch rather than a resolve_group() call. |
| 329 | * |
| 330 | * @param array $config Consumer config that may hold a `geo` group. |
| 331 | * @param array $defaults Default geo configuration. |
| 332 | * @return array Normalized geo configuration. |
| 333 | */ |
| 334 | private static function resolve_geo( $config, $defaults ) { |
| 335 | $nested_geo = isset( $config['geo'] ) && is_array( $config['geo'] ) ? $config['geo'] : array(); |
| 336 | $geo = array_merge( $defaults, $nested_geo ); |
| 337 | |
| 338 | if ( ! in_array( $geo['provider'], self::GEO_PROVIDERS, true ) ) { |
| 339 | $geo['provider'] = $defaults['provider']; |
| 340 | $geo['api_url'] = $defaults['api_url']; |
| 341 | } |
| 342 | if ( ! is_string( $geo['api_url'] ) ) { |
| 343 | $geo['api_url'] = ''; |
| 344 | } |
| 345 | // The default provider uses a shared, canonical endpoint, so an empty api_url is |
| 346 | // refilled from the default; a custom provider may keep an explicitly blank url. |
| 347 | if ( $defaults['provider'] === $geo['provider'] && '' === $geo['api_url'] ) { |
| 348 | $geo['api_url'] = $defaults['api_url']; |
| 349 | } |
| 350 | $geo['country_code_cookie'] = is_string( $geo['country_code_cookie'] ) && '' !== $geo['country_code_cookie'] ? $geo['country_code_cookie'] : $defaults['country_code_cookie']; |
| 351 | $geo['region_cookie'] = is_string( $geo['region_cookie'] ) && '' !== $geo['region_cookie'] ? $geo['region_cookie'] : $defaults['region_cookie']; |
| 352 | $geo['cookie_duration'] = is_numeric( $geo['cookie_duration'] ) ? (int) $geo['cookie_duration'] : $defaults['cookie_duration']; |
| 353 | $geo['gdpr_countries'] = is_array( $geo['gdpr_countries'] ) ? self::normalize_gdpr_countries( $geo['gdpr_countries'] ) : $defaults['gdpr_countries']; |
| 354 | $geo['ccpa_regions'] = is_array( $geo['ccpa_regions'] ) ? self::normalize_ccpa_regions( $geo['ccpa_regions'] ) : $defaults['ccpa_regions']; |
| 355 | $geo['show_on_error'] = (bool) $geo['show_on_error']; |
| 356 | |
| 357 | return $geo; |
| 358 | } |
| 359 | |
| 360 | /** |
| 361 | * Get default UI copy. |
| 362 | * |
| 363 | * Defaults are translated in the package text domain. Consumers can override |
| 364 | * any key through the `copy` config group and translate those overrides in |
| 365 | * their own text domain before returning them from the config filter. |
| 366 | * |
| 367 | * @return array Default copy keyed by semantic UI location. |
| 368 | */ |
| 369 | private static function defaults_copy() { |
| 370 | return array( |
| 371 | 'banner_title' => __( 'Use of your personal data', 'jetpack-cookie-consent' ), |
| 372 | 'banner_description' => __( 'We and our partners process your personal data, such as browsing data, IP addresses, cookie information, and other unique identifiers, based on your consent and/or our legitimate interest to improve our website, marketing activities, and your user experience.', 'jetpack-cookie-consent' ), |
| 373 | 'banner_accept_button' => __( 'Accept', 'jetpack-cookie-consent' ), |
| 374 | 'banner_reject_button' => __( 'Reject', 'jetpack-cookie-consent' ), |
| 375 | 'banner_customize_button' => __( 'Customize', 'jetpack-cookie-consent' ), |
| 376 | 'modal_title' => __( 'Customize preferences', 'jetpack-cookie-consent' ), |
| 377 | 'modal_close_label' => __( 'Close modal', 'jetpack-cookie-consent' ), |
| 378 | 'modal_description' => __( 'Your privacy is important to us. We and our partners use, store, and process your personal data to improve our website, such as by improving security or conducting analytics, marketing activities to help deliver relevant marketing or content, and your user experience, such as by remembering your account name or language settings where applicable. You can customize your cookie settings below.', 'jetpack-cookie-consent' ), |
| 379 | 'privacy_policy_link' => __( 'Privacy Policy', 'jetpack-cookie-consent' ), |
| 380 | 'modal_links_lead' => __( 'Learn more in our', 'jetpack-cookie-consent' ), |
| 381 | 'modal_links_conjunction' => __( 'and', 'jetpack-cookie-consent' ), |
| 382 | 'cookie_policy_link' => __( 'Cookie Policy', 'jetpack-cookie-consent' ), |
| 383 | 'category_toggle_label' => __( 'Toggle category description', 'jetpack-cookie-consent' ), |
| 384 | 'required_category_label' => __( 'Required', 'jetpack-cookie-consent' ), |
| 385 | 'always_active_label' => __( 'Always active', 'jetpack-cookie-consent' ), |
| 386 | 'required_category_description' => __( 'These cookies are essential for our websites and services to perform basic functions and are necessary for us to operate certain features. Examples include your IP address, browser type, requested URLs, response codes, and operating system data.', 'jetpack-cookie-consent' ), |
| 387 | 'analytics_category_label' => __( 'Analytics', 'jetpack-cookie-consent' ), |
| 388 | 'analytics_category_description' => __( 'These cookies allow us to improve performance by collecting information on how users interact with our websites.', 'jetpack-cookie-consent' ), |
| 389 | 'advertising_category_label' => __( 'Advertising', 'jetpack-cookie-consent' ), |
| 390 | 'advertising_category_description' => __( 'These cookies are set by us and our advertising partners to provide you with relevant content and to understand that content\'s effectiveness.', 'jetpack-cookie-consent' ), |
| 391 | 'save_preferences_button' => __( 'Save preferences', 'jetpack-cookie-consent' ), |
| 392 | 'accept_all_button' => __( 'Accept all', 'jetpack-cookie-consent' ), |
| 393 | 'reject_all_button' => __( 'Reject all', 'jetpack-cookie-consent' ), |
| 394 | 'manage_preferences_link' => __( 'Manage Privacy Preferences', 'jetpack-cookie-consent' ), |
| 395 | 'ccpa_page_title' => __( 'Your Privacy Choices', 'jetpack-cookie-consent' ), |
| 396 | 'ccpa_intro' => __( 'We value your privacy and want you to feel in control of your personal information. Like most websites, we use cookies and similar tools to improve your website experience and show you relevant ads. Sometimes we share this information with trusted partners to do so.', 'jetpack-cookie-consent' ), |
| 397 | 'ccpa_laws_notice' => __( 'Some U.S. state laws consider this kind of data sharing a sale or sharing of personal information. Depending on where you live, you may have the right to opt out.', 'jetpack-cookie-consent' ), |
| 398 | 'ccpa_heading' => __( 'How to Opt Out', 'jetpack-cookie-consent' ), |
| 399 | 'ccpa_browser_opt_out' => __( 'Browser Opt-Out: Click the Opt Out button below to stop your browser from sharing this data.', 'jetpack-cookie-consent' ), |
| 400 | 'ccpa_account_opt_out' => __( 'Account Opt-Out: To apply this choice to your account, check the box and enter your email.', 'jetpack-cookie-consent' ), |
| 401 | 'ccpa_gpc_opt_out' => __( 'Automatic Opt-Out: If you use a browser with Global Privacy Control (GPC) turned on, we will recognize it and respect your choice automatically.', 'jetpack-cookie-consent' ), |
| 402 | 'ccpa_preferences_notice' => __( 'Your preferences will only affect how we use your information for personalized ads and similar activities. It will not affect how we use your information for other purposes, like security or site functionality.', 'jetpack-cookie-consent' ), |
| 403 | 'ccpa_button_instruction' => __( 'Click the Opt Out button to stop this browser from sharing personal data.', 'jetpack-cookie-consent' ), |
| 404 | 'ccpa_opt_out_button' => __( 'Opt Out', 'jetpack-cookie-consent' ), |
| 405 | 'ccpa_snackbar_success' => __( 'Your browser has been successfully opted out from sharing personal data.', 'jetpack-cookie-consent' ), |
| 406 | 'ccpa_snackbar_dismiss_label' => __( 'Dismiss', 'jetpack-cookie-consent' ), |
| 407 | ); |
| 408 | } |
| 409 | |
| 410 | /** |
| 411 | * Get the default consent category registry. |
| 412 | * |
| 413 | * Category keys are the internal/persistence names. The frontend keeps the |
| 414 | * existing `required` and `advertising` aliases for the default `functional` |
| 415 | * and `marketing` categories. |
| 416 | * |
| 417 | * @param array|null $copy Optional normalized copy array. |
| 418 | * @return array Default consent categories. |
| 419 | */ |
| 420 | private static function default_consent_categories( $copy = null ) { |
| 421 | $copy = is_array( $copy ) ? self::normalize_copy( $copy, self::defaults_copy() ) : self::defaults_copy(); |
| 422 | |
| 423 | return array( |
| 424 | array( |
| 425 | 'key' => 'functional', |
| 426 | 'label' => $copy['required_category_label'], |
| 427 | 'description' => $copy['required_category_description'], |
| 428 | 'required' => true, |
| 429 | 'default_checked' => true, |
| 430 | 'wp_consent_map' => array( 'functional' ), |
| 431 | ), |
| 432 | array( |
| 433 | 'key' => 'analytics', |
| 434 | 'label' => $copy['analytics_category_label'], |
| 435 | 'description' => $copy['analytics_category_description'], |
| 436 | 'required' => false, |
| 437 | 'default_checked' => true, |
| 438 | 'wp_consent_map' => array( 'statistics', 'statistics-anonymous' ), |
| 439 | ), |
| 440 | array( |
| 441 | 'key' => 'marketing', |
| 442 | 'label' => $copy['advertising_category_label'], |
| 443 | 'description' => $copy['advertising_category_description'], |
| 444 | 'required' => false, |
| 445 | 'default_checked' => false, |
| 446 | 'wp_consent_map' => array( 'marketing' ), |
| 447 | ), |
| 448 | ); |
| 449 | } |
| 450 | |
| 451 | /** |
| 452 | * Normalize configured copy by merging consumer overrides with defaults. |
| 453 | * |
| 454 | * @param array $copy Copy values supplied by configuration. |
| 455 | * @param array $defaults Default copy values. |
| 456 | * @return array Normalized copy. |
| 457 | */ |
| 458 | private static function normalize_copy( $copy, $defaults ) { |
| 459 | if ( ! is_array( $copy ) ) { |
| 460 | return $defaults; |
| 461 | } |
| 462 | |
| 463 | foreach ( $copy as $key => $value ) { |
| 464 | if ( ! is_string( $key ) ) { |
| 465 | continue; |
| 466 | } |
| 467 | |
| 468 | if ( is_scalar( $value ) ) { |
| 469 | $defaults[ $key ] = (string) $value; |
| 470 | continue; |
| 471 | } |
| 472 | |
| 473 | // A non-scalar override can't render, so the default is kept. Surface it |
| 474 | // so an integrating developer notices the override was dropped instead of |
| 475 | // silently shipping the default copy. |
| 476 | _doing_it_wrong( |
| 477 | __METHOD__, |
| 478 | /* translators: %s is the copy configuration key. */ |
| 479 | esc_html( sprintf( __( 'Cookie consent copy override for "%s" was ignored because it is not a scalar value.', 'jetpack-cookie-consent' ), $key ) ), |
| 480 | '' |
| 481 | ); |
| 482 | } |
| 483 | |
| 484 | return $defaults; |
| 485 | } |
| 486 | |
| 487 | /** |
| 488 | * Normalize configured consent categories. |
| 489 | * |
| 490 | * @param array $categories Categories supplied by configuration. |
| 491 | * @param array $defaults Default categories. |
| 492 | * @return array Normalized categories. |
| 493 | */ |
| 494 | private static function normalize_consent_categories( $categories, $defaults ) { |
| 495 | if ( ! is_array( $categories ) ) { |
| 496 | return $defaults; |
| 497 | } |
| 498 | |
| 499 | $normalized = array(); |
| 500 | $seen_keys = array(); |
| 501 | $seen_preference_keys = array(); |
| 502 | |
| 503 | // The frontend aliases the default `functional`/`marketing` categories to the |
| 504 | // `required`/`advertising` preference keys, so those alias strings are reserved. |
| 505 | // A consumer category whose key (or derived preference key) collides with one |
| 506 | // would otherwise silently overwrite a built-in category's consent state. |
| 507 | $reserved_keys = array( 'required', 'advertising' ); |
| 508 | |
| 509 | foreach ( $categories as $category ) { |
| 510 | if ( ! is_array( $category ) || empty( $category['key'] ) ) { |
| 511 | continue; |
| 512 | } |
| 513 | |
| 514 | $key = self::normalize_consent_category_key( $category['key'] ); |
| 515 | if ( '' === $key || isset( $seen_keys[ $key ] ) ) { |
| 516 | continue; |
| 517 | } |
| 518 | |
| 519 | $preference_key = Cookie_Consent::get_category_preference_key( $key ); |
| 520 | if ( in_array( $key, $reserved_keys, true ) || isset( $seen_preference_keys[ $preference_key ] ) ) { |
| 521 | _doing_it_wrong( |
| 522 | __METHOD__, |
| 523 | /* translators: %s is the consent category key. */ |
| 524 | esc_html( sprintf( __( 'Cookie consent category "%s" was ignored because it collides with a reserved preference key.', 'jetpack-cookie-consent' ), $key ) ), |
| 525 | '' |
| 526 | ); |
| 527 | continue; |
| 528 | } |
| 529 | |
| 530 | $wp_consent_map = array(); |
| 531 | if ( isset( $category['wp_consent_map'] ) && is_array( $category['wp_consent_map'] ) ) { |
| 532 | foreach ( $category['wp_consent_map'] as $wp_consent_key ) { |
| 533 | $wp_consent_key = sanitize_key( $wp_consent_key ); |
| 534 | if ( '' !== $wp_consent_key && ! in_array( $wp_consent_key, $wp_consent_map, true ) ) { |
| 535 | $wp_consent_map[] = $wp_consent_key; |
| 536 | } |
| 537 | } |
| 538 | } |
| 539 | |
| 540 | if ( empty( $wp_consent_map ) ) { |
| 541 | $wp_consent_map = array( $key ); |
| 542 | } |
| 543 | |
| 544 | $required = ! empty( $category['required'] ); |
| 545 | |
| 546 | $normalized[] = array( |
| 547 | 'key' => $key, |
| 548 | 'label' => isset( $category['label'] ) && is_scalar( $category['label'] ) ? (string) $category['label'] : $key, |
| 549 | 'description' => isset( $category['description'] ) && is_scalar( $category['description'] ) ? (string) $category['description'] : '', |
| 550 | 'required' => $required, |
| 551 | 'default_checked' => $required || ! empty( $category['default_checked'] ), |
| 552 | 'wp_consent_map' => $wp_consent_map, |
| 553 | ); |
| 554 | |
| 555 | $seen_keys[ $key ] = true; |
| 556 | $seen_preference_keys[ $preference_key ] = true; |
| 557 | } |
| 558 | |
| 559 | return empty( $normalized ) ? $defaults : $normalized; |
| 560 | } |
| 561 | |
| 562 | /** |
| 563 | * Normalize a consent category key for PHP arrays and Interactivity paths. |
| 564 | * |
| 565 | * @param mixed $key Consent category key. |
| 566 | * @return string Normalized key. |
| 567 | */ |
| 568 | private static function normalize_consent_category_key( $key ) { |
| 569 | $key = sanitize_key( (string) $key ); |
| 570 | return preg_replace( '/[^a-z0-9_]/', '_', $key ); |
| 571 | } |
| 572 | |
| 573 | /** |
| 574 | * Normalize configured links by merging consumer overrides with defaults. |
| 575 | * |
| 576 | * @param array $config Configuration array supplied through filters. |
| 577 | * @param array $defaults Default link values. |
| 578 | * @return array Normalized links. |
| 579 | */ |
| 580 | private static function normalize_links( $config, $defaults ) { |
| 581 | $links = isset( $config['links'] ) && is_array( $config['links'] ) ? $config['links'] : array(); |
| 582 | |
| 583 | foreach ( $links as $key => $value ) { |
| 584 | if ( ! is_string( $key ) ) { |
| 585 | continue; |
| 586 | } |
| 587 | |
| 588 | if ( is_scalar( $value ) ) { |
| 589 | $defaults[ $key ] = trim( (string) $value ); |
| 590 | } |
| 591 | } |
| 592 | |
| 593 | return $defaults; |
| 594 | } |
| 595 | |
| 596 | /** |
| 597 | * Normalize GDPR country codes for case-insensitive matching. |
| 598 | * |
| 599 | * @param array $countries Country codes; non-scalar entries are dropped. |
| 600 | * @return string[] Upper-case country codes. |
| 601 | */ |
| 602 | private static function normalize_gdpr_countries( $countries ) { |
| 603 | $countries = array_values( array_filter( $countries, 'is_scalar' ) ); |
| 604 | return array_map( |
| 605 | static function ( $country ) { |
| 606 | return strtoupper( (string) $country ); |
| 607 | }, |
| 608 | $countries |
| 609 | ); |
| 610 | } |
| 611 | |
| 612 | /** |
| 613 | * Normalize CCPA region names for case-insensitive matching. |
| 614 | * |
| 615 | * @param array $regions Region names; non-scalar entries are dropped. |
| 616 | * @return string[] Lower-case region names. |
| 617 | */ |
| 618 | private static function normalize_ccpa_regions( $regions ) { |
| 619 | $regions = array_values( array_filter( $regions, 'is_scalar' ) ); |
| 620 | return array_map( |
| 621 | static function ( $region ) { |
| 622 | return strtolower( (string) $region ); |
| 623 | }, |
| 624 | $regions |
| 625 | ); |
| 626 | } |
| 627 | |
| 628 | /** |
| 629 | * Get default GDPR country list. |
| 630 | * |
| 631 | * @return string[] Country codes where opt-in consent applies. |
| 632 | */ |
| 633 | private static function default_gdpr_countries() { |
| 634 | return array( |
| 635 | // European Member countries. |
| 636 | 'AT', // Austria. |
| 637 | 'BE', // Belgium. |
| 638 | 'BG', // Bulgaria. |
| 639 | 'CY', // Cyprus. |
| 640 | 'CZ', // Czech Republic. |
| 641 | 'DE', // Germany. |
| 642 | 'DK', // Denmark. |
| 643 | 'EE', // Estonia. |
| 644 | 'ES', // Spain. |
| 645 | 'FI', // Finland. |
| 646 | 'FR', // France. |
| 647 | 'GR', // Greece. |
| 648 | 'HR', // Croatia. |
| 649 | 'HU', // Hungary. |
| 650 | 'IE', // Ireland. |
| 651 | 'IT', // Italy. |
| 652 | 'LT', // Lithuania. |
| 653 | 'LU', // Luxembourg. |
| 654 | 'LV', // Latvia. |
| 655 | 'MT', // Malta. |
| 656 | 'NL', // Netherlands. |
| 657 | 'PL', // Poland. |
| 658 | 'PT', // Portugal. |
| 659 | 'RO', // Romania. |
| 660 | 'SE', // Sweden. |
| 661 | 'SI', // Slovenia. |
| 662 | 'SK', // Slovakia. |
| 663 | 'GB', // United Kingdom. |
| 664 | // Single Market Countries that GDPR applies to. |
| 665 | 'CH', // Switzerland. |
| 666 | 'IS', // Iceland. |
| 667 | 'LI', // Liechtenstein. |
| 668 | 'NO', // Norway. |
| 669 | ); |
| 670 | } |
| 671 | |
| 672 | /** |
| 673 | * Get default CCPA-style region list. |
| 674 | * |
| 675 | * @return string[] Lower-case region names where opt-out consent applies. |
| 676 | */ |
| 677 | private static function default_ccpa_regions() { |
| 678 | return array( |
| 679 | /* US regions/states that are treated like California for Do Not Sell requests. */ |
| 680 | 'california', |
| 681 | 'utah', |
| 682 | 'virginia', |
| 683 | 'colorado', |
| 684 | 'connecticut', |
| 685 | 'texas', |
| 686 | 'tennessee', |
| 687 | 'oregon', |
| 688 | 'new jersey', |
| 689 | 'montana', |
| 690 | 'iowa', |
| 691 | 'indiana', |
| 692 | 'delaware', |
| 693 | ); |
| 694 | } |
| 695 | } |