|null */ private static $cache = null; /** * Default settings. * * @return array */ public static function defaults() { return array( 'enabled' => true, 'layout' => 'bar', 'position' => 'bottom', 'theme' => 'light', 'accent_color' => '#1d4ed8', 'show_reopen' => true, 'honor_gpc' => true, 'service_choice' => false, 'expiry_days' => 365, 'revision' => 1, 'privacy_page' => (int) get_option( 'wp_page_for_privacy_policy', 0 ), 'block_scripts' => true, 'block_embeds' => true, 'gcm_enabled' => true, 'gcm_mode' => 'basic', 'gcm_ads_redaction' => true, 'gcm_url_passthrough' => false, 'ms_consent' => true, 'log_enabled' => true, 'log_retention_days' => 1095, 'texts' => array( 'title' => '', 'message' => '', 'accept' => '', 'reject' => '', 'customize' => '', 'save' => '', ), 'services' => array(), 'custom_services' => array(), 'delete_on_uninstall' => false, ); } /** * Returns current settings merged over defaults. * * @return array */ public static function get() { if ( null === self::$cache ) { $stored = get_option( self::OPTION, array() ); self::$cache = self::sanitize( is_array( $stored ) ? $stored : array() ); } return self::$cache; } /** * Saves settings (sanitized). * * @param array $settings Settings. * @return array Saved settings. */ public static function update( array $settings ) { $clean = self::sanitize( $settings ); update_option( self::OPTION, $clean ); self::$cache = $clean; return $clean; } /** * Settings that belong to one site and are never exported or imported: the policy revision * (consent given on this site), the privacy page ID and the uninstall choice. */ const SITE_ONLY = array( 'revision', 'privacy_page', 'delete_on_uninstall' ); /** * Settings as a portable array, for copying the setup to another site. * * @return array */ public static function export_data() { return array( 'plugin' => 'ariconsent', 'version' => ARICONSENT_VERSION, 'exported' => gmdate( 'c' ), 'settings' => array_diff_key( self::get(), array_flip( self::SITE_ONLY ) ), ); } /** * Imports settings exported with export_data(). Everything goes through sanitize(), and this * site's revision, privacy page and uninstall choice are kept. * * @param mixed $data Decoded export file. * @return array|\WP_Error Saved settings, or an error for a file that is not an AriConsent export. */ public static function import_data( $data ) { if ( ! is_array( $data ) || 'ariconsent' !== ( $data['plugin'] ?? '' ) || ! isset( $data['settings'] ) || ! is_array( $data['settings'] ) ) { return new \WP_Error( 'ariconsent_import', __( 'This file is not an AriConsent settings export.', 'ariconsent' ) ); } $keep = array_intersect_key( self::get(), array_flip( self::SITE_ONLY ) ); return self::update( array_merge( array_diff_key( $data['settings'], $keep ), $keep ) ); } /** * Clears the request cache (for tests and after external updates). */ public static function flush() { self::$cache = null; } /** * Sanitizes a settings array. Unknown keys are dropped. * * @param array $raw Raw settings. * @return array */ public static function sanitize( array $raw ) { $d = self::defaults(); $out = array(); $bool = static function ( $key ) use ( $raw, $d ) { return array_key_exists( $key, $raw ) ? (bool) $raw[ $key ] : $d[ $key ]; }; $enum = static function ( $key, array $allowed ) use ( $raw, $d ) { return ( isset( $raw[ $key ] ) && in_array( $raw[ $key ], $allowed, true ) ) ? $raw[ $key ] : $d[ $key ]; }; $int = static function ( $key, $min, $max ) use ( $raw, $d ) { if ( ! isset( $raw[ $key ] ) || ! is_numeric( $raw[ $key ] ) ) { return $d[ $key ]; } return max( $min, min( $max, (int) $raw[ $key ] ) ); }; foreach ( array( 'enabled', 'show_reopen', 'honor_gpc', 'service_choice', 'block_scripts', 'block_embeds', 'gcm_enabled', 'gcm_ads_redaction', 'gcm_url_passthrough', 'ms_consent', 'log_enabled', 'delete_on_uninstall' ) as $key ) { $out[ $key ] = $bool( $key ); } $out['layout'] = $enum( 'layout', array( 'bar', 'box', 'modal' ) ); $out['position'] = $enum( 'position', array( 'bottom', 'top' ) ); $out['theme'] = $enum( 'theme', array( 'light', 'dark' ) ); $out['gcm_mode'] = $enum( 'gcm_mode', array( 'basic', 'advanced' ) ); $out['expiry_days'] = $int( 'expiry_days', 1, 395 ); $out['revision'] = $int( 'revision', 1, PHP_INT_MAX ); $out['log_retention_days'] = $int( 'log_retention_days', 30, 3650 ); $out['privacy_page'] = isset( $raw['privacy_page'] ) ? absint( $raw['privacy_page'] ) : $d['privacy_page']; $color = isset( $raw['accent_color'] ) ? sanitize_hex_color( (string) $raw['accent_color'] ) : ''; $out['accent_color'] = $color ? $color : $d['accent_color']; $out['texts'] = array(); $texts = isset( $raw['texts'] ) && is_array( $raw['texts'] ) ? $raw['texts'] : array(); foreach ( array_keys( $d['texts'] ) as $key ) { $value = isset( $texts[ $key ] ) ? (string) $texts[ $key ] : ''; $out['texts'][ $key ] = 'message' === $key ? wp_kses( $value, self::allowed_message_html() ) : sanitize_text_field( $value ); } $categories = array_keys( Services::categories() ); $builtin = Services::builtin(); $out['services'] = array(); if ( isset( $raw['services'] ) && is_array( $raw['services'] ) ) { foreach ( $raw['services'] as $id => $state ) { $id = sanitize_key( (string) $id ); if ( ! isset( $builtin[ $id ] ) || ! is_array( $state ) ) { continue; } $category = isset( $state['category'] ) && in_array( $state['category'], $categories, true ) ? $state['category'] : ''; $out['services'][ $id ] = array( 'enabled' => ! empty( $state['enabled'] ), 'category' => $category, ); } } $out['custom_services'] = array(); if ( isset( $raw['custom_services'] ) && is_array( $raw['custom_services'] ) ) { foreach ( $raw['custom_services'] as $custom ) { $clean = self::sanitize_custom_service( $custom, $categories ); if ( $clean && ! isset( $builtin[ $clean['id'] ] ) ) { $out['custom_services'][] = $clean; } } } return $out; } /** * Sanitizes a custom service entered by the site owner. * * @param mixed $raw Raw service. * @param string[] $categories Valid categories. * @return array|null */ private static function sanitize_custom_service( $raw, array $categories ) { if ( ! is_array( $raw ) || empty( $raw['name'] ) ) { return null; } $name = sanitize_text_field( (string) $raw['name'] ); if ( '' === $name ) { return null; } $split = static function ( $value ) { $lines = is_array( $value ) ? $value : preg_split( '/\r\n|\r|\n/', (string) $value ); $lines = array_map( 'trim', array_map( 'sanitize_text_field', array_map( 'strval', $lines ) ) ); // Patterns shorter than 4 characters would match far too much. return array_values( array_filter( $lines, static function ( $line ) { return strlen( $line ) >= 4; } ) ); }; $cookies = array(); $raw_c = isset( $raw['cookies'] ) ? $raw['cookies'] : array(); if ( is_string( $raw_c ) ) { // One cookie per line: name | duration | purpose. foreach ( preg_split( '/\r\n|\r|\n/', $raw_c ) as $line ) { $parts = array_map( 'trim', explode( '|', $line ) ); if ( '' !== $parts[0] ) { $cookies[] = array( sanitize_text_field( $parts[0] ), isset( $parts[1] ) ? sanitize_text_field( $parts[1] ) : '', isset( $parts[2] ) ? sanitize_text_field( $parts[2] ) : '' ); } } } elseif ( is_array( $raw_c ) ) { foreach ( $raw_c as $cookie ) { if ( is_array( $cookie ) && ! empty( $cookie[0] ) ) { $cookies[] = array( sanitize_text_field( (string) $cookie[0] ), sanitize_text_field( (string) ( isset( $cookie[1] ) ? $cookie[1] : '' ) ), sanitize_text_field( (string) ( isset( $cookie[2] ) ? $cookie[2] : '' ) ) ); } } } $id = isset( $raw['id'] ) && '' !== $raw['id'] ? sanitize_key( (string) $raw['id'] ) : 'custom-' . sanitize_title( $name ); return array( 'id' => $id, 'name' => $name, 'provider' => isset( $raw['provider'] ) ? sanitize_text_field( (string) $raw['provider'] ) : '', 'category' => isset( $raw['category'] ) && in_array( $raw['category'], $categories, true ) ? $raw['category'] : 'marketing', 'privacy' => isset( $raw['privacy'] ) ? esc_url_raw( (string) $raw['privacy'] ) : '', 'scripts' => $split( isset( $raw['scripts'] ) ? $raw['scripts'] : array() ), 'iframes' => $split( isset( $raw['iframes'] ) ? $raw['iframes'] : array() ), 'cookies' => $cookies, 'builtin' => false, 'google' => false, ); } /** * HTML allowed in the banner message. * * @return array> */ public static function allowed_message_html() { return array( 'a' => array( 'href' => true, 'target' => true, 'rel' => true, ), 'strong' => array(), 'em' => array(), 'br' => array(), ); } /** * Banner texts with translated defaults where the owner left a field blank. * * @return array */ public static function texts() { $custom = self::get()['texts']; $defaults = array( 'title' => __( 'We value your privacy', 'ariconsent' ), 'message' => __( 'We use cookies and similar technologies to make the website work. With your consent we also use them for statistics and marketing. You can change your choice at any time.', 'ariconsent' ), 'accept' => __( 'Accept all', 'ariconsent' ), 'reject' => __( 'Reject all', 'ariconsent' ), 'customize' => __( 'Choose cookies', 'ariconsent' ), 'save' => __( 'Save my choices', 'ariconsent' ), ); $out = array(); foreach ( $defaults as $key => $default ) { $out[ $key ] = '' !== trim( (string) $custom[ $key ], " \n\r\t\v\0" ) ? Multilingual::translate( 'text_' . $key, $custom[ $key ] ) : $default; } return $out; } }