sanitize_settings($settings); if ($user_id || is_user_logged_in()) { // WordPress user meta (highest priority) $user_id = $user_id ?: get_current_user_id(); $existing_settings = get_user_meta($user_id, $this->user_meta_key, true) ?: []; if (!is_array($existing_settings)) { $existing_settings = []; } $merged_settings = array_merge($existing_settings, $clean_settings); $result = update_user_meta($user_id, $this->user_meta_key, $merged_settings); return [ 'success' => true, 'storage_type' => 'wordpress_user_meta', 'user_id' => $user_id, 'settings' => $merged_settings, ]; } else { // Guest settings (session-based, stored via transient) $session_id = $this->get_or_create_session_id(); $existing_settings = get_transient($this->guest_settings_key . '_' . $session_id) ?: []; if (!is_array($existing_settings)) { $existing_settings = []; } $merged_settings = array_merge($existing_settings, $clean_settings); // 30 days TTL for guest settings set_transient( $this->guest_settings_key . '_' . $session_id, $merged_settings, 30 * DAY_IN_SECONDS ); return [ 'success' => true, 'storage_type' => 'wordpress_transient', 'session_id' => $session_id, 'settings' => $merged_settings, ]; } } /** * Load user settings with fallback: * - logged-in: user meta + defaults * - guest: transient + defaults * * @param int|null $user_id * @return array */ public function get_user_settings($user_id = null) { $default_settings = $this->get_default_settings(); if ($user_id || is_user_logged_in()) { // Load from WordPress user meta $user_id = $user_id ?: get_current_user_id(); $user_settings = get_user_meta($user_id, $this->user_meta_key, true) ?: []; if (!is_array($user_settings)) { $user_settings = []; } return [ 'success' => true, 'storage_type' => 'wordpress_user_meta', 'user_id' => $user_id, 'settings' => array_merge($default_settings, $user_settings), 'is_logged_in' => true, ]; } else { // Load guest settings $session_id = $this->get_or_create_session_id(); $guest_settings = get_transient($this->guest_settings_key . '_' . $session_id) ?: []; if (!is_array($guest_settings)) { $guest_settings = []; } return [ 'success' => true, 'storage_type' => 'wordpress_transient', 'session_id' => $session_id, 'settings' => array_merge($default_settings, $guest_settings), 'is_logged_in' => false, ]; } } /** * AJAX handler: save settings */ public function ajax_save_settings() { // Nonce check for security $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''; if (!wp_verify_nonce($nonce, 'a11ybridge_settings_nonce')) { wp_send_json_error([ 'message' => __('Security check failed.', 'a11ybridge'), ]); return; } // JSON string from browser: sanitize as text before parsing to satisfy PHPCS. $settings_raw = isset($_POST['settings']) ? sanitize_textarea_field(wp_unslash($_POST['settings'])) : ''; if (empty($settings_raw)) { wp_send_json_error([ 'message' => __('No settings provided.', 'a11ybridge'), ]); return; } // Decode JSON $settings = json_decode($settings_raw, true); if (json_last_error() !== JSON_ERROR_NONE) { wp_send_json_error([ 'message' => sprintf( /* translators: %s = JSON error message */ __('JSON parsing error: %s', 'a11ybridge'), json_last_error_msg() ), ]); return; } $result = $this->save_user_settings($settings); if (!empty($result['success'])) { wp_send_json_success($result); } else { wp_send_json_error($result); } } /** * AJAX handler: load settings */ public function ajax_get_settings() { $result = $this->get_user_settings(); wp_send_json_success($result); } /** * AJAX handler: sync settings (local <-> server) */ public function ajax_sync_settings() { // Optional nonce for sync; recommended for write-like actions $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''; if (!wp_verify_nonce($nonce, 'a11ybridge_settings_nonce')) { wp_send_json_error([ 'message' => __('Security check failed.', 'a11ybridge'), ]); return; } // JSON string from browser: sanitize as text before parsing to satisfy PHPCS. $local_raw = isset($_POST['local_settings']) ? sanitize_textarea_field(wp_unslash($_POST['local_settings'])) : '{}'; $local_settings = json_decode($local_raw, true); if (json_last_error() !== JSON_ERROR_NONE || !is_array($local_settings)) { $local_settings = []; } $server_result = $this->get_user_settings(); if (empty($server_result['success'])) { wp_send_json_error([ 'message' => __('Server settings could not be loaded.', 'a11ybridge'), ]); return; } $server_settings = isset($server_result['settings']) && is_array($server_result['settings']) ? $server_result['settings'] : []; // Merge strategy: server has priority, but local changes are considered. // Here local overrides server on key conflicts: $merged_settings = array_merge($server_settings, $local_settings); // Only save if there are differences if ($merged_settings !== $server_settings) { $save_result = $this->save_user_settings($merged_settings); wp_send_json_success([ 'message' => __('Settings synchronized.', 'a11ybridge'), 'merged_settings' => $merged_settings, 'changes_saved' => true, 'storage_info' => $save_result, ]); } else { wp_send_json_success([ 'message' => __('Settings already synchronized.', 'a11ybridge'), 'merged_settings' => $server_settings, 'changes_saved' => false, ]); } } /** * Sanitize and validate settings. * * @param array $settings * @return array */ private function sanitize_settings($settings) { if (!is_array($settings)) { return []; } $allowed_keys = [ 'fontSize', 'fontFamily', 'lineHeight', 'letterSpacing', 'colorMode', 'colorModeEnabled', 'colorFilter', 'keyboardNavigation', 'textToSpeech', 'focusMode', 'voiceCommands', 'voiceCommandsEnabled', 'hideAnimations', 'hideImages', 'allVoiceCommands', 'altTextEnabled', 'simpleLanguageEnabled', 'simplificationLevel', 'simplificationTargetLanguage', 'simplificationConsentGrantedAt', 'altTextConsentGrantedAt', 'ttsEnabled', 'ttsVoice', 'ttsSpeed', 'ttsPitch', 'ttsVolume', 'ttsLanguage', 'imageDescriptionEnabled', ]; $clean = []; foreach ($settings as $key => $value) { if (in_array($key, $allowed_keys, true)) { $clean[$key] = $this->sanitize_setting_value($key, $value); } } return $clean; } /** * Sanitize a single setting value by key. * * @param string $key * @param mixed $value * @return mixed */ private function sanitize_setting_value($key, $value) { switch ($key) { case 'fontSize': // CSS font-size: number + unit if (is_string($value) && preg_match('/^\d+(\.\d+)?(px|em|rem|%)$/', $value)) { return $value; } return '16px'; case 'fontFamily': return sanitize_text_field((string) $value); case 'lineHeight': // CSS line-height can be numeric or a unitized value, keep simple numeric if (is_numeric($value)) { return (string) $value; } return '1.5'; case 'letterSpacing': // Either "normal" or numeric + unit if ($value === 'normal') { return 'normal'; } if (is_string($value) && preg_match('/^\d+(\.\d+)?(px|em|rem|%)$/', $value)) { return $value; } return 'normal'; case 'colorMode': $allowed_modes = ['light', 'dark', 'high-contrast']; return in_array($value, $allowed_modes, true) ? $value : 'light'; case 'colorFilter': $allowed_filters = [ 'none', 'deuteranomaly', 'protanomaly', 'tritanomaly', 'achromatopsia', ]; return in_array($value, $allowed_filters, true) ? $value : 'none'; case 'allVoiceCommands': // Keep structure, but ensure it's an array return is_array($value) ? $value : []; default: // For boolean-ish switches, normalize to bool if (is_bool($value)) { return $value; } // Accept "0"/"1"/"true"/"false" from JS etc. if (is_string($value)) { $lower = strtolower($value); if ($lower === 'true' || $lower === '1') { return true; } if ($lower === 'false' || $lower === '0') { return false; } } return (bool) $value; } } /** * Default settings. * * @return array */ private function get_default_settings() { return [ 'fontSize' => '16px', 'fontFamily' => 'Arial, sans-serif', 'lineHeight' => '1.5', 'letterSpacing' => 'normal', 'colorMode' => 'light', 'colorModeEnabled' => false, 'colorFilter' => 'none', 'keyboardNavigation' => false, 'textToSpeech' => false, 'focusMode' => false, 'voiceCommands' => false, 'voiceCommandsEnabled' => false, 'hideAnimations' => false, 'hideImages' => false, 'allVoiceCommands' => [], ]; } /** * Create or retrieve a session ID for guests. * * @return string */ private function get_or_create_session_id() { // WordPress.org review note: Avoid PHP sessions on the frontend. // Sessions bypass page caching and can severely hurt performance on managed hosts. // We use a lightweight anonymous guest-id cookie instead. $cookie_name = 'a11ybridge_guest_id'; $raw = isset($_COOKIE[$cookie_name]) ? sanitize_text_field(wp_unslash($_COOKIE[$cookie_name])) : ''; $sid = (string) $raw; // Expect a UUID-based guest id. if ($sid === '' || !preg_match('/^guest_[a-f0-9-]{36}$/i', $sid)) { $sid = 'guest_' . wp_generate_uuid4(); if (!headers_sent()) { $secure = is_ssl(); $path = defined('COOKIEPATH') && COOKIEPATH ? COOKIEPATH : '/'; $domain = defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : ''; // Keep it simple: 30 days, HttpOnly, secure on HTTPS. // (No SameSite flag here for PHP<7.3 compatibility.) @setcookie($cookie_name, $sid, time() + (30 * DAY_IN_SECONDS), $path, $domain, $secure, true); } // Mirror into superglobal so subsequent logic in this request sees it. $_COOKIE[$cookie_name] = $sid; } return $sid; } } /** * Liefert ein Frontend-Snapshot der User-Settings für JS. * * A11YBRIDGE_USER = { * [flag_id]: { * enabled: bool, * value: mixed, * source: 'wp'|'local' * } * } */ function a11ybridge_get_frontend_user_snapshot(?int $user_id = null): array { if (!class_exists('A11yBridge_User_Settings_Manager')) { return []; } $manager = A11yBridge_User_Settings_Manager::getInstance(); $result = $manager->get_user_settings($user_id); if (empty($result['success']) || empty($result['settings']) || !is_array($result['settings'])) { return []; } $settings = $result['settings']; $storageType = isset($result['storage_type']) ? (string) $result['storage_type'] : 'wordpress_transient'; $source = ($storageType === 'wordpress_user_meta') ? 'wp' : 'local'; $out = []; foreach ($settings as $key => $value) { // Hier setzt du fest: "flag_id" == Setting-Key (z.B. 'fontSize', 'colorMode', 'voiceCommandsEnabled', ...) $out[$key] = [ 'enabled' => is_bool($value) ? $value : true, 'value' => $value, 'source' => $source, ]; } return $out; } // Bootstrap singleton A11yBridge_User_Settings_Manager::getInstance();