'Version']); $a11ybridge_ver = !empty($a11ybridge_hdr['Version']) ? $a11ybridge_hdr['Version'] : '0.0.0'; } else { $a11ybridge_ver = '0.0.0'; } define('A11YBRIDGE_VERSION', $a11ybridge_ver); } // ----------------------------------------------------------------------------- // Versioning (single source of truth + context-specific aliases) // ----------------------------------------------------------------------------- // 1) Plugin release version (already defined by you from plugin header) if (!defined('A11YBRIDGE_VERSION')) { define('A11YBRIDGE_VERSION', '1.0.50'); // fallback, should never be used } // 2) Backward-compatible alias (some files still use this) if (!defined('A11YBRIDGE_PLUGIN_VERSION')) { define('A11YBRIDGE_PLUGIN_VERSION', A11YBRIDGE_VERSION); } // 3) Asset version for cache busting (CSS/JS). Keep equal to release version in production. if (!defined('A11YBRIDGE_ASSET_VERSION')) { define('A11YBRIDGE_ASSET_VERSION', A11YBRIDGE_VERSION); } // 4) Data/Schema version for plugin-side storage changes (options/custom tables). // Bump ONLY when you change stored data structures and need a migration. if (!defined('A11YBRIDGE_SCHEMA_VERSION')) { define('A11YBRIDGE_SCHEMA_VERSION', 1); } // 5) Protocol/API version between plugin <-> backend (only bump when you break payload/headers) if (!defined('A11YBRIDGE_API_VERSION')) { define('A11YBRIDGE_API_VERSION', 1); } // PHP-Version prüfen if (version_compare(PHP_VERSION, '7.4', '<')) { wp_die('This plugin demands at least PHP 7.4. Please update your PHP version.'); } require_once plugin_dir_path(__FILE__) . 'includes/catalog.php'; /** * Debug logger (never logs personal data). * Enable with WP_DEBUG on your site. */ if (!function_exists('a11ybridge_debug_log')) { function a11ybridge_debug_log(string $message): void { if (defined('WP_DEBUG') && WP_DEBUG) { // Keep logs generic. Never append request payloads, emails, texts, audio, etc. // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Only logs when WP_DEBUG is enabled. error_log($message); } } } // Create a stable, pseudonymous install ID (used for free-tier quotas / abuse prevention). if (!function_exists('a11ybridge_get_install_id')) { function a11ybridge_get_install_id(): string { $id = (string) get_option('a11yb_install_id', ''); if (empty($id)) { $id = wp_generate_uuid4(); update_option('a11yb_install_id', $id, true); } return $id; } } /** * Cloud / external service consent (opt-in). * * WP.org policy: the plugin must not phone home or register domains automatically. * Cloud requests are only performed after explicit admin opt-in. * (Entering/activating a license is treated as an explicit admin action, but does not automatically enable cloud processing for visitors.) */ if (!function_exists('a11ybridge_get_cloud_settings')) { function a11ybridge_get_cloud_settings(): array { $opt = get_option('a11ybridge_cloud_settings', []); return is_array($opt) ? $opt : []; } } if (!function_exists('a11ybridge_cloud_enabled')) { function a11ybridge_cloud_enabled(): bool { $opt = a11ybridge_get_cloud_settings(); return !empty($opt['enabled']); } } if (!function_exists('a11ybridge_any_ai_feature_enabled')) { function a11ybridge_any_ai_feature_enabled(): bool { $ai = get_option('a11ybridge_ai_settings', []); $simpl = get_option('a11ybridge_simplification_settings', []); if (!is_array($ai)) { $ai = []; } if (!is_array($simpl)) { $simpl = []; } // WP.org build: MVP cloud features. return (!empty($ai['enable_ai_image_description']) || !empty($simpl['enable_text_simplification'])); } } if (!function_exists('a11ybridge_text_simplification_admin_enabled')) { function a11ybridge_text_simplification_admin_enabled(): bool { $simpl = get_option('a11ybridge_simplification_settings', []); if (!is_array($simpl)) { $simpl = []; } return !empty($simpl['enable_text_simplification']); } } if (!function_exists('a11ybridge_text_simplification_allowed')) { function a11ybridge_text_simplification_allowed(): bool { // Requirement: admin must explicitly enable cloud services AND the feature. return a11ybridge_cloud_enabled() && a11ybridge_text_simplification_admin_enabled(); } } if (!function_exists('a11ybridge_has_paid_license')) { function a11ybridge_has_paid_license(): bool { $pro = get_option('a11ybridge_pro_license_settings', []); if (!is_array($pro)) { $pro = []; } $license_hash = !empty($pro['license_hash']) ? (string) $pro['license_hash'] : ''; if ($license_hash === '') { $license_hash = (string) get_option('a11yb_license_key_hash', ''); } $plan = !empty($pro['plan']) ? strtolower(trim((string) $pro['plan'])) : ''; return ($license_hash !== '' && $plan !== 'free'); } } if (!function_exists('a11ybridge_cloud_opted_in')) { function a11ybridge_cloud_opted_in(): bool { // Explicit opt-in only (WP.org compliance): do not treat feature toggles as consent. return a11ybridge_cloud_enabled(); } } register_activation_hook(__FILE__, function () { // Ensure install_id exists early. a11ybridge_get_install_id(); }); /** * Adds suggested privacy policy text to the site's Privacy Policy page (WP "Privacy Policy Guide"). * Site owners remain responsible for their own GDPR/ePrivacy compliance. */ add_action('admin_init', 'a11ybridge_add_privacy_policy_content'); function a11ybridge_add_privacy_policy_content(): void { if (!function_exists('wp_add_privacy_policy_content')) { return; } $content = '
A11yBridge is an accessibility plugin. Some optional cloud features may send content to A11yBridge servers (https://a11ybridge.de) for processing (for example: AI text simplification), and return the result to the site visitor. Cloud features are disabled by default and are only used after the site owner enables them.
'; $content .= 'When a cloud feature is used, the selected text and the chosen settings (e.g. language and simplification level) are transmitted for processing.
'; $content .= 'A11yBridge does not use analytics cookies. The plugin may store accessibility preferences locally (e.g. browser storage) and may set a session cookie for guest users if needed to persist settings between requests.
'; $content .= 'As with any HTTPS request, IP addresses and request metadata may be processed in server logs for security and troubleshooting.
'; $content .= 'More information: https://a11ybridge.de/en/datenschutzerklaerung/
'; wp_add_privacy_policy_content('A11yBridge', wp_kses_post($content)); } /* if (!is_admin()) { } */ function a11ybridge_should_show_toolbar(): bool { $raw = get_option('a11ybridge_plugin_general_settings', []); if (!is_array($raw)) return true; $v = $raw['show_toolbar'] ?? 1; // Dein Setting ist aktuell "0"/"1" als String -> das passt so: if (is_array($v)) return !empty($v['enabled']); return !in_array((string)$v, ['0', 'false', ''], true); } function a11ybridge_should_show_credit_link(): bool { $raw = get_option('a11ybridge_plugin_general_settings', []); if (!is_array($raw)) return false; // Guideline: front-end credits must be opt-in; default OFF $v = $raw['show_credit_link'] ?? 0; return (int)$v === 1; } function a11ybridge_admin_enqueue_settings_assets($hook) { // Nur auf deiner Settings-Seite laden if ($hook !== 'toplevel_page_a11ybridge-settings') { return; } // Logger init (wie im Frontend) wp_register_script( 'a11ybridge-logger-init', plugins_url('frontend/assets/js/logger-init.js', __FILE__), [], A11YBRIDGE_VERSION, false ); // Catalog (optional, aber sauber) if (function_exists('a11ybridge_load_catalog')) { $catalog = a11ybridge_load_catalog(); wp_add_inline_script( 'a11ybridge-logger-init', 'window.A11YBRIDGE_CATALOG = ' . wp_json_encode($catalog) . ';', 'before' ); } // Debug-Flag (optional) $debug = (defined('WP_DEBUG') && WP_DEBUG) ? 'true' : 'false'; wp_add_inline_script( 'a11ybridge-logger-init', "globalThis.A11YBRIDGE_DEBUG = {$debug};", 'before' ); // Fallback: damit Admin-UI nie crasht, selbst wenn logger-init aus irgendeinem Grund nicht greift wp_add_inline_script( 'a11ybridge-logger-init', 'globalThis.A11yBridgeLogger = globalThis.A11yBridgeLogger || {debug(){},info(){},warn(){},error(){},setLevel(){}};', 'before' ); wp_enqueue_script('a11ybridge-logger-init'); // ------------------------------------------------------------ // Admin inline assets (WP.org compliance: no script/style tags in templates) // ------------------------------------------------------------ // Inline styles handle wp_register_style('a11ybridge-admin-inline', false, [], A11YBRIDGE_VERSION); wp_enqueue_style('a11ybridge-admin-inline'); // Admin CSS (migrated from old template style blocks; WP.org compliant) $__a11ybridge_admin_css = ''; ob_start(); ?> /* WordPress admin dashboard compatible styles */ .a11y-admin-wrapper { background: #fff; margin: 20px 0; padding: 0; } .a11y-admin-wrapper .nav-tab-wrapper { border-bottom: 1px solid #c3c4c7; margin: 0; padding: 0 20px; background: #f6f7f7; } .a11y-admin-wrapper .nav-tab { background: #f6f7f7; border: 1px solid #c3c4c7; color: #50575e; text-decoration: none; padding: 12px 16px; margin: 0 4px -1px 0; font-size: 14px; font-weight: 600; line-height: 1.4; display: inline-block; border-radius: 4px 4px 0 0; } .a11y-admin-wrapper .nav-tab:hover { background: #fff; color: #0073aa; } .a11y-admin-wrapper .nav-tab-active { background: #fff; border-bottom: 1px solid #fff; color: #000; } .a11y-tab-content { padding: 20px; background: #fff; min-height: 600px; } .a11y-settings-section { background: #fff; border: 1px solid #c3c4c7; border-radius: 6px; margin-bottom: 20px; padding: 20px; box-shadow: 0 1px 1px rgba(0,0,0,.04); } .a11y-settings-section h3 { margin-top: 0; color: #1d2327; border-bottom: 1px solid #e0e0e0; padding-bottom: 10px; font-size: 16px; font-weight: 600; } .a11y-field-group { display: flex; align-items: center; gap: 15px; margin-bottom: 15px; padding: 10px; background: #f6f7f7; border-radius: 4px; border: 1px solid #dcdcde; } .a11y-field-group:hover { background: #f0f6fc; border-color: #0073aa; } .a11y-field-group input[type="checkbox"] { margin: 0; transform: scale(1.1); } .a11y-field-group label { flex: 1; margin: 0; font-weight: 500; color: #1d2327; } .a11y-field-group select, .a11y-field-group input[type="text"], .a11y-field-group input[type="number"] { min-width: 180px; padding: 6px 8px; border: 1px solid #8c8f94; border-radius: 3px; font-size: 14px; } .a11y-tooltip { cursor: help; color: #0073aa; font-size: 16px; font-weight: bold; width: 20px; height: 20px; text-align: center; border-radius: 50%; border: 1px solid #0073aa; display: inline-flex; align-items: center; justify-content: center; background: #f0f6fc; } .a11y-tooltip:hover { background: #0073aa; color: white; } .a11y-action-section { background: #f0f6fc; border: 1px solid #0073aa; border-radius: 6px; padding: 20px; margin-top: 20px; } .a11y-button { background: #0073aa !important; color: white !important; border: none !important; padding: 10px 20px !important; border-radius: 4px !important; cursor: pointer; font-size: 14px !important; margin-right: 10px !important; text-decoration: none !important; display: inline-block; font-weight: 500; } .a11y-button:hover { background: #005a87 !important; color: white !important; } .a11y-button:disabled { background: #c3c4c7 !important; cursor: not-allowed; } .a11y-button.secondary { background: #6c757d !important; } .a11y-button.secondary:hover { background: #5a6268 !important; } .a11y-status { margin-top: 15px; padding: 10px; border-radius: 4px; display: none; } .a11y-status.success { background: #d1edff; border: 1px solid #0073aa; color: #0073aa; } .a11y-status.error { background: #fcf2f2; border: 1px solid #d63638; color: #d63638; } .a11y-admin-header { background: linear-gradient(135deg, #0073aa 0%, #005a87 100%); color: white; padding: 20px; margin: 0 0 20px 0; border-radius: 6px; } .a11y-admin-header h1 { margin: 0; color: white; font-size: 24px; font-weight: 600; } .a11y-admin-header p { margin: 8px 0 0 0; color: rgba(255,255,255,0.9); font-size: 14px; } .a11y-legal-badge { display:inline-block; margin-left:8px; padding:2px 6px; border-radius:6px; background:#eef; font-size:11px; } .a11y-legal-badge.accent { background:#efe; } .a11y-legal-badge.is-unavailable { background:#f6f6f6; color:#888; border:1px dashed #ccc; } .a11y-legal-privacy { display:inline-block; margin-left:6px; font-size:11px; color:#666; } .a11yb-card { margin-top: 18px; } .a11yb-stats-grid { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 12px; margin: 10px 0 14px; } .a11yb-stat { background:#fff; border:1px solid #e5e5e5; border-radius:6px; padding:10px 12px; } .a11yb-stat-label { font-size:12px; color:#666; margin-bottom:4px; } .a11yb-stat-value { font-size:16px; font-weight:600; } .a11yb-progress { background:#f1f1f1; border-radius:999px; height:10px; overflow:hidden; margin-top:6px; } .a11yb-progress-bar { background:#2271b1; height:10px; } .a11yb-progress-meta { margin-top:8px; color:#333; } .a11yb-inline-msg { margin-left:8px; } .a11y-disabled { opacity:.55; pointer-events:none; } .a11y-disabled-badge{ display:inline-block; margin-left:8px; padding:2px 8px; font-size:12px; border-radius:12px; background:#edf2f7; color:#2d3748; border:1px solid #cbd5e0; vertical-align:middle; } .a11y-disabled-wrap{ position:relative; } .a11y-disabled-wrap .a11y-disabled-overlay{ position:absolute; inset:0; background:transparent; cursor:not-allowed; } admin_url('admin-ajax.php'), 'noncePro' => wp_create_nonce('a11ybridge_pro_nonce'), 'nonceScanner' => wp_create_nonce('a11ybridge_scanner_nonce'), 'nonceUser' => wp_create_nonce('a11ybridge_user_nonce'), 'nonceTestAjax'=> wp_create_nonce('a11ybridge_test_ajax'), ]) . ';', 'before' ); } add_action('admin_enqueue_scripts', 'a11ybridge_admin_enqueue_settings_assets', 20); function a11ybridge_enqueue_frontend_assets() { // Guard: Toolbar komplett aus? Dann nichts laden. if (!a11ybridge_should_show_toolbar()) { // später durch Logger ersetzen (PHP-Logger), aber ok fürs Debug a11ybridge_debug_log('A11yBridge: Toolbar disabled, skipping asset enqueue.'); return; } // ------------------------------------------------------------ // 0) Styles // ------------------------------------------------------------ wp_enqueue_style( 'a11ybridge-styles', A11YBRIDGE_URL . 'frontend/assets/css/styles.css', [], A11YBRIDGE_VERSION ); // ------------------------------------------------------------ // 1) Logger init (muss vor allem anderen kommen) // ------------------------------------------------------------ wp_register_script( 'a11ybridge-logger-init', plugins_url('frontend/assets/js/logger-init.js', __FILE__), [], A11YBRIDGE_VERSION, false ); // Catalog inline BEFORE everything else $catalog = a11ybridge_load_catalog(); wp_add_inline_script( 'a11ybridge-logger-init', 'window.A11YBRIDGE_CATALOG = ' . wp_json_encode($catalog) . ';', 'before' ); // Debug-Flag VOR logger-init setzen (damit logger-init direkt den Level wählen kann) $debug = (defined('WP_DEBUG') && WP_DEBUG) ? 'true' : 'false'; wp_add_inline_script( 'a11ybridge-logger-init', "globalThis.A11YBRIDGE_DEBUG = {$debug};", 'before' ); // Fallback: falls irgendwas schief läuft, crasht nix wegen fehlendem Logger wp_add_inline_script( 'a11ybridge-logger-init', 'globalThis.A11yBridgeLogger = globalThis.A11yBridgeLogger || {debug(){},info(){},warn(){},error(){},setLevel(){}};', 'before' ); // 2) Settings handler (klassisch, NICHT module!) wp_register_script( 'a11ybridge-settings-handler', plugins_url('frontend/assets/js/settings-handler.js', __FILE__), ['a11ybridge-logger-init'], A11YBRIDGE_VERSION, true ); // a11ybridgePlugin config dahin, wo settings-handler es garantiert sieht $a11ybridge_gs = get_option('a11ybridge_plugin_general_settings', []); $a11ybridge_show_credit = (is_array($a11ybridge_gs) && !empty($a11ybridge_gs['show_credit_link'])) ? 1 : 0; wp_localize_script('a11ybridge-settings-handler', 'a11ybridgePlugin', [ 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('a11ybridge_settings_nonce'), 'isUserLoggedIn' => is_user_logged_in(), 'cloudEnabled' => (int) a11ybridge_cloud_enabled(), 'adminTextSimplificationEnabled' => (int) a11ybridge_text_simplification_admin_enabled(), 'simplifyProxyUrl' => esc_url_raw( rest_url('a11ybridge/v1/ai/simplify') ), 'aiTokenUrl' => esc_url_raw( rest_url('a11ybridge/v1/ai/token') ), 'showCreditLink' => (int) $a11ybridge_show_credit, 'brandUrl' => apply_filters('a11ybridge_brand_url', 'https://a11ybridge.de/'), 'brandText' => apply_filters('a11ybridge_brand_text', 'A11yBridge®'), ]); // TTS (falls toolbar.js window.textToSpeechManager nutzt) wp_register_script( 'a11ybridge-text-to-speech', plugins_url('frontend/assets/js/text-to-speech.js', __FILE__), ['a11ybridge-logger-init', 'a11ybridge-settings-handler'], A11YBRIDGE_VERSION, true ); // Text Simplification (falls du externes Script hast) wp_register_script( 'a11ybridge-text-simplification', plugins_url('frontend/assets/js/text-simplification.js', __FILE__), ['a11ybridge-logger-init', 'a11ybridge-settings-handler'], A11YBRIDGE_VERSION, true ); wp_script_add_data('a11ybridge-text-to-speech', 'type', 'module'); wp_script_add_data('a11ybridge-text-simplification', 'type', 'module'); // toolbar deps erweitern (settings-handler MUSS vor toolbar laufen) $toolbar_deps = ['a11ybridge-logger-init', 'a11ybridge-settings-handler', 'a11ybridge-text-to-speech', 'a11ybridge-text-simplification']; // ------------------------------------------------------------ // (ES Module) hängt von Logger + Settings + Features ab // ------------------------------------------------------------ $toolbar_deps = [ 'a11ybridge-logger-init', 'a11ybridge-settings-handler', 'a11ybridge-text-to-speech', 'a11ybridge-text-simplification', ];wp_register_script( 'a11ybridge-toolbar', plugins_url('frontend/assets/js/toolbar.js', __FILE__), $toolbar_deps, A11YBRIDGE_VERSION, true ); // Toolbar ist ES-Module: wp_script_add_data('a11ybridge-toolbar', 'type', 'module'); // ------------------------------------------------------------ // 6) Enqueue nur den Einstiegspunkt. Der Rest kommt über deps. // ------------------------------------------------------------ wp_enqueue_script('a11ybridge-toolbar'); } add_action('wp_enqueue_scripts', 'a11ybridge_enqueue_frontend_assets', 20); // **Textdomain für Übersetzungen laden** // Zentrale Registrierung für Voice Recognition // (WP.org build) Voice recognition registration removed in MVP. // Frontend + Admin sollen die Registrierung kennen // ✅ WICHTIG: User Settings Manager IMMER laden (sowohl Frontend als auch Admin für AJAX) require_once plugin_dir_path(__FILE__) . 'includes/user-settings-manager.php'; new A11yBridge_User_Settings_Manager(); // AJAX-Hooks registrieren // ✅ AJAX-Handler DIREKT hier registrieren (nicht in toolbar.php) add_action('wp_ajax_a11ybridge_test_ajax', 'a11ybridge_test_ajax_handler'); function a11ybridge_test_ajax_handler() { // Admin-only debug endpoint if (!current_user_can('manage_options')) { wp_send_json_error(['message' => 'forbidden'], 403); } // Optional nonce check (supports GET/POST) $nonce = isset($_REQUEST['_wpnonce']) ? sanitize_text_field(wp_unslash($_REQUEST['_wpnonce'])) : ''; if ($nonce && !wp_verify_nonce($nonce, 'a11ybridge_test_ajax')) { wp_send_json_error(['message' => 'bad_nonce'], 403); } a11ybridge_debug_log('[A11yBridge] a11ybridge_test_ajax called'); wp_send_json_success(['message' => 'OK']); } // **Admin-Einstellungen einbinden** if( is_admin() ) { require_once plugin_dir_path(__FILE__) . 'admin/settings.php'; } /** * AI proxy helpers (public visitors). * * The AI simplification feature uses a public REST proxy endpoint. * To reduce trivial unauthenticated abuse (quota/costs), we require a short-lived token. * * Token flow: * - Frontend requests /ai/token (GET) to receive a short-lived token. * - Frontend calls /ai/simplify (POST) with header X-A11YB-Token. * - Server validates token (and binds it to the requester IP) before proxying upstream. */ function a11ybridge_ai_proxy_token_key( string $token ): string { return 'a11yb_ai_tok_' . hash_hmac( 'sha256', $token, wp_salt( 'auth' ) ); } function a11ybridge_ai_proxy_issue_token(): string { try { $token = bin2hex( random_bytes( 16 ) ); // 32 hex chars } catch ( Exception $e ) { $token = wp_generate_password( 32, false, false ); } $ip_raw = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; $ip = rest_is_ip_address( $ip_raw ) ? $ip_raw : '0.0.0.0'; $ip_sig = hash_hmac( 'sha256', $ip, wp_salt( 'auth' ) ); // Token valid for 10 minutes. set_transient( a11ybridge_ai_proxy_token_key( $token ), $ip_sig, 10 * MINUTE_IN_SECONDS ); return $token; } function a11ybridge_ai_proxy_check_rate_limit( string $bucket, int $limit, int $window_seconds ): bool { $ip_raw = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; $ip = rest_is_ip_address( $ip_raw ) ? $ip_raw : '0.0.0.0'; $key = 'a11yb_rl_' . sanitize_key( $bucket ) . '_' . hash_hmac( 'sha256', $ip, wp_salt( 'auth' ) ); $count = (int) get_transient( $key ); if ( $count >= $limit ) { return false; } set_transient( $key, $count + 1, $window_seconds ); return true; } function a11ybridge_rest_ai_token( WP_REST_Request $req ) { // Public endpoint, but rate-limit to avoid being used as a token vending machine. if ( ! a11ybridge_ai_proxy_check_rate_limit( 'ai_token', 30, MINUTE_IN_SECONDS ) ) { return new WP_REST_Response( [ 'ok' => false, 'error' => 'rate_limited' ], 429 ); } // If cloud services are not enabled, do not issue tokens (no AI without explicit opt-in). if (function_exists('a11ybridge_text_simplification_allowed') && !a11ybridge_text_simplification_allowed()) { return new WP_REST_Response([ 'ok' => false, 'error' => 'cloud_or_feature_disabled' ], 403); } $token = a11ybridge_ai_proxy_issue_token(); $resp = new WP_REST_Response( [ 'ok' => true, 'token' => $token ], 200 ); $resp->header( 'Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0' ); $resp->header( 'Pragma', 'no-cache' ); return $resp; } function a11ybridge_rest_ai_simplify_permission( WP_REST_Request $req ) { // Admin must explicitly enable cloud services AND the feature (double opt-in). if (function_exists('a11ybridge_text_simplification_allowed') && !a11ybridge_text_simplification_allowed()) { return new WP_Error('a11ybridge_cloud_disabled', 'Cloud services are disabled. Enable Cloud connection in the plugin settings first.', [ 'status' => 403 ]); } $simpl = get_option('a11ybridge_simplification_settings', []); if (!is_array($simpl) || empty($simpl['enable_text_simplification'])) { return new WP_Error('a11ybridge_feature_disabled', 'AI text simplification is disabled by the site administrator', [ 'status' => 403 ]); } $token = sanitize_text_field( (string) $req->get_header( 'x-a11yb-token' ) ); if ( $token === '' ) { return new WP_Error( 'a11ybridge_missing_token', 'Missing AI token', [ 'status' => 403 ] ); } $key = a11ybridge_ai_proxy_token_key( $token ); $ip_sig_expected = (string) get_transient( $key ); if ( $ip_sig_expected === '' ) { return new WP_Error( 'a11ybridge_invalid_token', 'Invalid or expired AI token', [ 'status' => 403 ] ); } $ip_raw = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; $ip = rest_is_ip_address( $ip_raw ) ? $ip_raw : '0.0.0.0'; $ip_sig = hash_hmac( 'sha256', $ip, wp_salt( 'auth' ) ); if ( ! hash_equals( $ip_sig_expected, $ip_sig ) ) { return new WP_Error( 'a11ybridge_token_mismatch', 'Token mismatch', [ 'status' => 403 ] ); } // One-time token: consume it. delete_transient( $key ); return true; } add_action('rest_api_init', function () { register_rest_route('a11ybridge/v1', '/ai/token', [ 'methods' => WP_REST_Server::READABLE, // GET 'permission_callback' => '__return_true', 'callback' => 'a11ybridge_rest_ai_token', ]); register_rest_route('a11ybridge/v1', '/ai/simplify', [ 'methods' => WP_REST_Server::CREATABLE, // POST 'permission_callback' => 'a11ybridge_rest_ai_simplify_permission', 'callback' => function (WP_REST_Request $req) { // JSON body $json = $req->get_json_params(); if (!is_array($json)) $json = []; $text = trim((string)($json['text'] ?? '')); if ($text === '') { return new WP_REST_Response(['ok'=>false,'error'=>'missing_text'], 400); } // --- License hash server-side (NICHT im Frontend) --- $pro = get_option('a11ybridge_pro_license_settings', []); if (!is_array($pro)) $pro = []; $plan = ''; if (!empty($pro['plan'])) { $plan = strtolower(trim((string)$pro['plan'])); } $license_hash = ''; if (!empty($pro['license_hash'])) { $license_hash = (string)$pro['license_hash']; } if (!$license_hash) { $license_hash = (string)get_option('a11yb_license_key_hash', ''); } // Harden paid/free detection: ignore polluted free settings with fake hashes. $is_paid = (!empty($license_hash) && $plan !== 'free'); $install_id = ''; if (!$is_paid) { // Free: stable Installation-ID (no fake license hash) $install_id = a11ybridge_get_install_id(); } // Optional: Minimales Rate-Limit pro IP (MVP-Schutz) $ip_raw = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : ''; $ip = rest_is_ip_address($ip_raw) ? $ip_raw : '0.0.0.0'; $rl_key = 'a11yb_rl_' . hash_hmac('sha256', (string)$ip, wp_salt('auth')); $count = (int) get_transient($rl_key); if ($count > 60) { // z.B. 60/h return new WP_REST_Response(['ok'=>false,'error'=>'rate_limited'], 429); } set_transient($rl_key, $count + 1, HOUR_IN_SECONDS); // Payload an Upstream (a11ybridge.de) weiterreichen $payload = [ 'text' => $text, 'promptKey' => (string)($json['promptKey'] ?? ''), 'level' => (string)($json['level'] ?? 'basic'), 'targetLanguage' => (string)($json['targetLanguage'] ?? 'original'), // 'meta' => is_array($json['meta'] ?? null) ? $json['meta'] : [], 'meta' => [], // minimized (do not forward client metadata) ]; if (!$is_paid && $install_id) { $payload['meta']['installation_id'] = $install_id; } // Normalize legacy values (frontend/admin might still send aliases) $payload['level'] = a11ybridge_catalog_canonical('textSimplification.level', (string)$payload['level'], 'basic'); $payload['targetLanguage'] = a11ybridge_catalog_canonical('textSimplification.targetLanguage', (string)$payload['targetLanguage'], 'original'); $timeout = (int) apply_filters('a11ybridge_ai_simplify_timeout', 120); $headers = [ 'Content-Type' => 'application/json; charset=utf-8', 'X-AB-Origin' => home_url('/'), ]; if ($is_paid) { $headers['X-A11Y-License-Hash'] = $license_hash; } else { $headers['X-AB-Installation-Id'] = $install_id; } $remote = wp_remote_post('https://a11ybridge.de/wp-json/a11ybridge/v1/chat/simplify', [ 'timeout' => 120, 'headers' => $headers, 'body' => wp_json_encode($payload), ]); if (is_wp_error($remote)) { return new WP_REST_Response(['ok'=>false,'error'=>'upstream_error','detail'=>$remote->get_error_message()], 502); } $code = (int) wp_remote_retrieve_response_code($remote); $body = (string) wp_remote_retrieve_body($remote); $decoded = json_decode($body, true); if (is_array($decoded)) { return new WP_REST_Response($decoded, $code ?: 200); } return new WP_REST_Response(['ok'=>false,'error'=>'bad_upstream_response','raw'=>$body], 502); }, ]); }); /** * Frontend Toolbar Markup (WP.org compliant: markup only; assets are enqueued via wp_enqueue_scripts). * toolbar.js expects #a11ybridge-icon and #a11ybridge-settings-list to exist. */ function a11ybridge_render_frontend_toolbar_markup() { static $rendered = false; if ($rendered) { return; } $rendered = true; if (is_admin()) { return; } if (wp_doing_ajax()) { return; } if (!a11ybridge_should_show_toolbar()) { return; } // Load settings definitions (titles/icons/scripts) $settings_fields = []; $settings_file = A11YBRIDGE_DIR . 'includes/settings-fields.php'; if (file_exists($settings_file)) { $settings_fields = require $settings_file; } // Admin-enabled basic features (checkboxes in Dashboard → Basics) $general_settings = get_option('a11ybridge_plugin_general_settings', []); if (!is_array($general_settings)) { $general_settings = []; } // Settings list panel (content is static markup; interactions handled by toolbar.js) echo ''; // Floating button (icon rendered via CSS ::before; keep markup empty to avoid double icons) echo ''; } add_action('wp_body_open', 'a11ybridge_render_frontend_toolbar_markup', 20); add_action('wp_footer', 'a11ybridge_render_frontend_toolbar_markup', 20); // Frontend toolbar is injected by frontend/assets/js/toolbar.js via a11ybridge_enqueue_frontend_assets(). // Legacy wp_footer toolbar hook removed (WP.org compliance + avoids missing legacy functions). /** * Liefert die aktivierten Voice-Command-Keys für den aktuellen Nutzer, * unter Berücksichtigung von Service-Status und Defaults. * * @return string[] Liste von Command-Keys (z. B. ['scroll_down','go_back', ...]) */ // (WP.org build) Voice command helpers removed in MVP. // Nonce für Frontend bereitstellen function a11ybridge_enqueue_scripts() { if (!a11ybridge_should_show_toolbar()) { return; } wp_enqueue_script('a11ybridge-settings', A11YBRIDGE_URL . 'frontend/assets/js/settings-handler.js', [], A11YBRIDGE_ASSET_VERSION, true); wp_localize_script('a11ybridge-settings-handler', 'a11ybridgePlugin', [ 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('a11ybridge_settings_nonce'), 'isUserLoggedIn' => is_user_logged_in(), 'cloudEnabled' => (int) a11ybridge_cloud_enabled(), 'adminTextSimplificationEnabled' => (int) a11ybridge_text_simplification_admin_enabled(), 'simplifyProxyUrl' => esc_url_raw( rest_url('a11ybridge/v1/ai/simplify') ), 'aiTokenUrl' => esc_url_raw( rest_url('a11ybridge/v1/ai/token') ), ]); wp_enqueue_script('a11ybridge-voice-recognition'); /* */ } // **REST-API-Routen registrieren** // === REST API routes (ersetzen): a11ybridge/v1 === add_action('rest_api_init', function () { // 1) Öffentliche READ-Route (GET) -> jeder darf lesen register_rest_route('a11ybridge/v1', '/settings', [ 'methods' => WP_REST_Server::READABLE, 'permission_callback' => '__return_true', 'callback' => 'a11ybridge_rest_get_settings', ]); // 2) Admin-only WRITE-Route (POST/PATCH) -> nur Admins register_rest_route('a11ybridge/v1', '/settings', [ 'methods' => WP_REST_Server::EDITABLE, 'permission_callback' => function () { return current_user_can('manage_options'); }, 'callback' => 'a11ybridge_rest_update_settings', 'args' => [ 'settings' => [ 'required' => true, 'type' => 'array', ], ], ]); }); function a11ybridge_rest_get_settings( WP_REST_Request $req ) { // TODO: aus DB/Optionen lesen – hier Dummy: return new WP_REST_Response([ ['name'=>'Schriftgröße','description'=>'Ändern der Schriftgröße','enabled'=>true], ['name'=>'Farbschema','description'=>'Farbschemata wechseln','enabled'=>true], ['name'=>'Navigation','description'=>'Navigationseinstellungen','enabled'=>false], ], 200); } function a11ybridge_rest_update_settings( WP_REST_Request $req ) { $settings = $req->get_param('settings'); // TODO: validieren & speichern return new WP_REST_Response(['ok'=>true,'saved'=>$settings], 200); } // **Plugin-Aktivierungs- und Deaktivierungs-Hooks** function a11ybridge_plugin_activate() { // Aktionen beim Aktivieren des Plugins a11ybridge_debug_log('[A11yBridge] activated'); } register_activation_hook(__FILE__, 'a11ybridge_plugin_activate'); function a11ybridge_plugin_deactivate() { // Aktionen beim Deaktivieren des Plugins a11ybridge_debug_log('[A11yBridge] deactivated'); } register_deactivation_hook(__FILE__, 'a11ybridge_plugin_deactivate'); add_filter('script_loader_tag', 'a11ybridge_force_module_type', 10, 3); add_filter('a11ybridge_ai_simplify_timeout', function($t) { return 120; }); function a11ybridge_force_module_type($tag, $handle, $src) { // Nur die Handles, die WIRKLICH Module sind $module_handles = [ 'a11ybridge-toolbar', 'a11ybridge-text-to-speech', 'a11ybridge-text-simplification', // 'a11ybridge-logger-init', // nur wenn logger-init.js wirklich import/export nutzt ]; if (!in_array($handle, $module_handles, true)) { return $tag; } // Wenn schon type="..." existiert -> ersetzen if (preg_match('/\btype=(["\']).*?\1/i', $tag)) { return preg_replace('/\btype=(["\']).*?\1/i', 'type="module"', $tag, 1); } // Sonst type="module" einfügen return str_replace('script ', 'script type="module" ', $tag); }