'sk-ant-', 'openai' => 'sk-', 'gemini' => 'AIza', ]; /** Canonical whitelist of WCAG standards the axe-core scanner can filter by. */ public const VALID_RULESETS = [ 'wcag20a', 'wcag20aa', 'wcag20aaa', 'wcag21a', 'wcag21aa', 'wcag21aaa', 'wcag22aa', 'section508', 'best-practice', ]; public static function get(): array { $s = get_option( self::OPTION_KEY, [] ); // Migrate legacy single-string ruleset (pre multi-select) to an array. if ( isset( $s['default_rulesets'] ) ) { $default_rulesets = $s['default_rulesets']; } elseif ( isset( $s['default_ruleset'] ) && is_string( $s['default_ruleset'] ) ) { $default_rulesets = [ $s['default_ruleset'] ]; } else { $default_rulesets = [ 'wcag22aa' ]; } return [ 'ai_provider' => $s['ai_provider'] ?? 'none', // boolean — raw key never leaves PHP. Also false for a key saved under an // older version (or any other state) where ai_key_provider doesn't match // ai_provider, so a stale/mismatched key is never reported as usable. 'ai_key_set' => self::has_usable_key( $s ), 'ai_model' => $s['ai_model'] ?? '', 'scan_schedule' => $s['scan_schedule'] ?? 'weekly', 'default_rulesets' => $default_rulesets, 'email_alerts' => $s['email_alerts'] ?? true, ]; } /** * Save settings. If $raw_key is empty the previously stored key is preserved. */ public static function save( string $provider, string $raw_key, string $model, string $schedule, array $default_rulesets = [ 'wcag22aa' ], bool $email_alerts = true, bool $remove_key = false ): void { $current = get_option( self::OPTION_KEY, [] ); // The user has been to Settings and saved — the re-entry prompt has done its job. delete_option( self::REENTRY_NOTICE_OPTION ); $default_rulesets = array_values( array_unique( array_intersect( $default_rulesets, self::VALID_RULESETS ) ) ); if ( empty( $default_rulesets ) ) { $default_rulesets = [ 'wcag22aa' ]; } // A stored key only ever belongs to the provider it was saved for (tracked // explicitly via ai_key_provider, not inferred from ai_provider — keys saved // before this existed are adopted or cleared by migrate_legacy_key()). Keep the key // across a save only when it already belongs to the provider being saved; // otherwise clear it rather than let a provider switch silently reuse the // previous provider's key. $remove_key is a distinct "clear it" signal from // the user (vs. an empty $raw_key, which just means "didn't change it"). if ( $remove_key ) { $ai_key = ''; $ai_key_provider = ''; } elseif ( $raw_key !== '' ) { $ai_key = AccessMate_AI_Proxy::encrypt_key( $raw_key ); $ai_key_provider = $provider; } elseif ( ( $current['ai_key_provider'] ?? '' ) === $provider ) { $ai_key = $current['ai_key'] ?? ''; $ai_key_provider = $provider; } else { $ai_key = ''; $ai_key_provider = ''; } update_option( self::OPTION_KEY, [ 'ai_provider' => $provider, 'ai_key' => $ai_key, 'ai_key_provider' => $ai_key_provider, 'ai_model' => $model, 'scan_schedule' => $schedule, 'default_rulesets' => $default_rulesets, 'email_alerts' => $email_alerts, ]); } /** * One-time migration for keys saved by 1.2.0 and earlier, which have no * ai_key_provider. The key is adopted only when it carries the selected * provider's prefix; otherwise it's cleared (never reused against another * provider) and an admin notice asks the user to re-enter it. */ public static function migrate_legacy_key(): void { $s = get_option( self::OPTION_KEY, [] ); if ( ! is_array( $s ) || empty( $s ) || array_key_exists( 'ai_key_provider', $s ) ) { return; } $provider = $s['ai_provider'] ?? 'none'; $s['ai_key_provider'] = ''; if ( ! empty( $s['ai_key'] ) ) { $raw = AccessMate_AI_Proxy::decrypt_key( $s['ai_key'] ); $prefix = self::KEY_PREFIXES[ $provider ] ?? null; $is_match = $prefix !== null && 0 === strpos( $raw, $prefix ) // OpenAI's 'sk-' prefix is also the start of Anthropic's 'sk-ant-'. && ! ( 'openai' === $provider && 0 === strpos( $raw, self::KEY_PREFIXES['anthropic'] ) ); if ( $is_match ) { $s['ai_key_provider'] = $provider; } else { $s['ai_key'] = ''; if ( $prefix !== null ) { update_option( self::REENTRY_NOTICE_OPTION, 1, false ); } } } update_option( self::OPTION_KEY, $s ); } /** Admin notice shown after migrate_legacy_key() had to clear a key. */ public static function render_reentry_notice(): void { if ( ! get_option( self::REENTRY_NOTICE_OPTION ) || ! current_user_can( 'manage_options' ) ) { return; } printf( '
%s %s