false, 'analytics_mode' => 'off', 'ga4_enabled' => false, 'ga4_transport' => 'gtag', 'ga4_measurement_id' => '', 'analytics_retention_days' => 365, 'delete_audio_on_post_delete' => true, 'backup_retention_days' => 30, 'allow_restricted_content' => false, 'documentation_language' => $language, ); } /** Accept the documented WordPress/import representations, never arbitrary truthy values. */ public static function enabled( $value ) { return in_array( $value, array( true, 1, '1' ), true ); } public static function openai_enabled() { return self::enabled( get_option( 'accessibility_audio_tts_chatgpt_enabled', '0' ) ); } public static function can_process_post( $post ) { return ! Audio_Access::is_restricted( $post ) || self::get( 'allow_restricted_content' ); } public static function get( $key ) { $defaults = self::defaults(); if ( ! array_key_exists( $key, $defaults ) ) { return null; } if ( 'analytics_mode' === $key ) { $mode = get_option( 'accessibility_audio_tts_privacy_analytics_mode', null ); if ( null === $mode ) { // An upgrade must never turn previously consented measurement into consent-free collection. return self::enabled( get_option( 'accessibility_audio_tts_privacy_analytics_enabled', false ) ) ? 'consented' : 'off'; } return in_array( $mode, array( 'off', 'anonymous', 'consented' ), true ) ? $mode : 'off'; } if ( 'analytics_enabled' === $key ) { return 'off' !== self::get( 'analytics_mode' ); } if ( 'ga4_transport' === $key ) { return 'datalayer' === get_option( 'accessibility_audio_tts_privacy_ga4_transport', 'gtag' ) ? 'datalayer' : 'gtag'; } if ( 'ga4_measurement_id' === $key ) { $id = get_option( 'accessibility_audio_tts_privacy_ga4_measurement_id', '' ); return is_string( $id ) && preg_match( '/^G-[A-Z0-9]{4,20}$/D', $id ) ? $id : ''; } if ( 'ga4_enabled' === $key ) { return 'consented' === self::get( 'analytics_mode' ) && self::enabled( get_option( 'accessibility_audio_tts_privacy_ga4_enabled', false ) ) && ( 'datalayer' === self::get( 'ga4_transport' ) || '' !== self::get( 'ga4_measurement_id' ) ); } // Retention, deletion and site language are policies, not saved preferences. if ( 'allow_restricted_content' !== $key ) { return $defaults[ $key ]; } return self::enabled( get_option( 'accessibility_audio_tts_privacy_' . $key, $defaults[ $key ] ) ); } public static function all() { $settings = array(); foreach ( self::defaults() as $key => $value ) { $settings[ $key ] = self::get( $key ); } return $settings; } public static function init() { add_action( 'wp_ajax_accessibility_audio_tts_save_privacy', array( __CLASS__, 'save_ajax' ) ); add_action( 'admin_init', array( '\\Accessibility_Audio_TTS\\Core\\Privacy_Documentation', 'register_policy_text' ) ); add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_admin' ) ); } public static function enqueue_admin() { if ( ! isset( $_GET['page'] ) || 'accessibility-audio-tts' !== $_GET['page'] ) { return; } wp_enqueue_script( 'accessibility-audio-tts-privacy-settings', ACCESSIBILITY_AUDIO_TTS_ASSETS_URL . 'admin/ui/components/privacy-settings.min.js', array(), ACCESSIBILITY_AUDIO_TTS_VERSION, true ); wp_localize_script( 'accessibility-audio-tts-privacy-settings', 'aatPrivacySettings', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'accessibility_audio_tts_privacy' ), 'saved' => __( 'Privacy settings saved. The policy text below now reflects these settings.', 'accessibility-audio-tts-text-to-speech-for-articles' ), 'error' => __( 'Privacy settings could not be saved. Please retry.', 'accessibility-audio-tts-text-to-speech-for-articles' ), 'copied' => __( 'Copied.', 'accessibility-audio-tts-text-to-speech-for-articles' ), 'copyFallback' => __( 'Select the text and copy it manually.', 'accessibility-audio-tts-text-to-speech-for-articles' ), 'unsaved' => __( 'Save privacy settings to refresh the policy text before copying it.', 'accessibility-audio-tts-text-to-speech-for-articles' ), ) ); } public static function save_ajax() { if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Access denied.' ), 403 ); return; } if ( ! check_ajax_referer( 'accessibility_audio_tts_privacy', 'nonce', false ) ) { wp_send_json_error( array( 'message' => 'Invalid security token.' ), 403 ); return; } if ( 'POST' !== ( $_SERVER['REQUEST_METHOD'] ?? '' ) ) { wp_send_json_error( array( 'message' => 'POST required.' ), 405 ); return; } $input = isset( $_POST['settings'] ) && is_array( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : array(); $validated = array(); foreach ( array( 'analytics_enabled', 'allow_restricted_content' ) as $key ) { if ( ! array_key_exists( $key, $input ) || ! is_scalar( $input[ $key ] ) ) { wp_send_json_error( array( 'message' => 'Incomplete privacy settings.' ), 400 ); return; } $value = $input[ $key ]; if ( ! in_array( $value, array( '0', '1', 0, 1, false, true ), true ) ) { wp_send_json_error( array( 'message' => 'Invalid privacy toggle.' ), 400 ); return; } $validated[ $key ] = self::enabled( $value ) ? '1' : '0'; } $mode = $input['analytics_mode'] ?? ( '1' === $validated['analytics_enabled'] ? 'consented' : 'off' ); if ( ! is_string( $mode ) || ! in_array( $mode, array( 'off', 'anonymous', 'consented' ), true ) ) { wp_send_json_error( array( 'message' => 'Invalid analytics mode.' ), 400 ); return; } // The explicit mode is authoritative; keep the historical switch for portable backups. $validated['analytics_mode'] = $mode; $validated['analytics_enabled'] = 'off' === $mode ? '0' : '1'; $ga_enabled = $input['ga4_enabled'] ?? '0'; $transport = $input['ga4_transport'] ?? 'gtag'; $id = $input['ga4_measurement_id'] ?? ''; if ( ! in_array( $ga_enabled, array( '0', '1', 0, 1, false, true ), true ) || ! is_string( $transport ) || ! in_array( $transport, array( 'gtag', 'datalayer' ), true ) || ! is_string( $id ) ) { wp_send_json_error( array( 'message' => 'Invalid analytics integration settings.' ), 400 ); return; } $id = strtoupper( trim( $id ) ); $ga_enabled = 'consented' === $mode && self::enabled( $ga_enabled ); if ( '' !== $id && ! preg_match( '/^G-[A-Z0-9]{4,20}$/D', $id ) ) { if ( $ga_enabled && 'gtag' === $transport ) { wp_send_json_error( array( 'message' => 'Invalid GA4 measurement ID.' ), 400 ); return; } $id = ''; // An abandoned, hidden destination must not prevent disabling measurement. } if ( $ga_enabled && 'gtag' === $transport && '' === $id ) { wp_send_json_error( array( 'message' => 'A GA4 measurement ID is required for Google tag events.' ), 400 ); return; } $validated['ga4_transport'] = $transport; $validated['ga4_measurement_id'] = $id; // Disable delivery first so a partial save cannot send to an old destination. update_option( 'accessibility_audio_tts_privacy_ga4_enabled', '0', false ); if ( '0' !== (string) get_option( 'accessibility_audio_tts_privacy_ga4_enabled' ) ) { wp_send_json_error( array( 'message' => 'Privacy settings were not fully saved. Reload and retry.' ), 500 ); return; } $validated['ga4_enabled'] = $ga_enabled ? '1' : '0'; foreach ( $validated as $key => $value ) { $option = 'accessibility_audio_tts_privacy_' . $key; update_option( $option, $value, false ); if ( (string) get_option( $option ) !== (string) $value ) { wp_send_json_error( array( 'message' => 'Privacy settings were not fully saved. Reload and retry.' ), 500 ); return; } } wp_send_json_success( array( 'settings' => self::all(), 'documents' => Privacy_Documentation::texts(), 'inventory' => Privacy_Documentation::storage_inventory() ) ); } }