*/ function rwpai_get_mcp_feature_option_map() { return array( 'features_adapter' => 'features_adapter_enabled', 'create_tools' => 'enable_create_tools', 'update_tools' => 'enable_update_tools', 'delete_tools' => 'enable_delete_tools', 'rest_api_crud_tools' => 'enable_rest_api_crud_tools', ); } /** * Return the default enabled MCP feature ids. * * @return string[] */ function rwpai_get_default_mcp_enabled_features() { $defaults = RWPAI_DEFAULT_CONFIG['mcp']['enabled_features'] ?? array(); return is_array( $defaults ) ? array_values( array_map( 'sanitize_key', $defaults ) ) : array(); } /** * Return default MCP runtime settings. * * @return array */ function rwpai_get_default_mcp_settings() { $enabled_features = rwpai_get_default_mcp_enabled_features(); $feature_map = rwpai_get_mcp_feature_option_map(); $defaults = array( 'enabled' => true, ); foreach ( $feature_map as $feature_id => $option_key ) { $defaults[ $option_key ] = in_array( $feature_id, $enabled_features, true ); } return $defaults; } /** * Return default MCP plugin config. * * @return array */ function rwpai_get_default_mcp_config() { return array( 'enabled' => true, 'enabled_features' => rwpai_get_default_mcp_enabled_features(), 'mcp_tokens' => array(), ); } /** * Sanitize a single MCP token record. * * @param mixed $token_record Raw token record. * @return array|null */ function rwpai_sanitize_mcp_token_record( $token_record ) { if ( ! is_array( $token_record ) ) { return null; } $token = isset( $token_record['token'] ) ? sanitize_text_field( (string) $token_record['token'] ) : ''; if ( '' === $token ) { return null; } $expires_at_raw = null; if ( array_key_exists( 'expires_at', $token_record ) ) { $expires_at_raw = $token_record['expires_at']; } elseif ( array_key_exists( 'expiresAt', $token_record ) ) { $expires_at_raw = $token_record['expiresAt']; } elseif ( array_key_exists( 'expiredAt', $token_record ) ) { $expires_at_raw = $token_record['expiredAt']; } $expires_at = null; if ( null !== $expires_at_raw && '' !== $expires_at_raw ) { $expires_at = max( 0, intval( $expires_at_raw ) ); if ( 0 === $expires_at ) { $expires_at = null; } } return array( 'token' => $token, 'expires_at' => $expires_at, ); } /** * Sanitize MCP token records from config. * * @param array $mcp_config Raw MCP config. * @return array> */ function rwpai_sanitize_mcp_tokens( $mcp_config ) { $records = array(); if ( isset( $mcp_config['mcp_tokens'] ) && is_array( $mcp_config['mcp_tokens'] ) ) { foreach ( $mcp_config['mcp_tokens'] as $token_record ) { $sanitized = rwpai_sanitize_mcp_token_record( $token_record ); if ( null !== $sanitized ) { $records[] = $sanitized; } } } elseif ( ! empty( $mcp_config['client_token'] ) ) { $legacy_token = rwpai_sanitize_mcp_token_record( array( 'token' => $mcp_config['client_token'], 'expires_at' => null, ) ); if ( null !== $legacy_token ) { $records[] = $legacy_token; } } $unique_records = array(); foreach ( $records as $record ) { $unique_records[ $record['token'] ] = $record; } return array_values( $unique_records ); } /** * Sanitize MCP config stored in the plugin option. * * @param mixed $mcp_config Raw MCP config. * @return array */ function rwpai_sanitize_mcp_config( $mcp_config ) { $defaults = rwpai_get_default_mcp_config(); $feature_map = rwpai_get_mcp_feature_option_map(); if ( ! is_array( $mcp_config ) ) { $mcp_config = array(); } $enabled_features = array(); if ( isset( $mcp_config['enabled_features'] ) && is_array( $mcp_config['enabled_features'] ) ) { foreach ( $mcp_config['enabled_features'] as $feature_id ) { $feature_id = sanitize_key( $feature_id ); if ( isset( $feature_map[ $feature_id ] ) ) { $enabled_features[] = $feature_id; } } } else { foreach ( $feature_map as $feature_id => $option_key ) { if ( array_key_exists( $option_key, $mcp_config ) && rest_sanitize_boolean( $mcp_config[ $option_key ] ) ) { $enabled_features[] = $feature_id; } } } if ( empty( $enabled_features ) && ! isset( $mcp_config['enabled_features'] ) ) { $enabled_features = $defaults['enabled_features']; } $sanitized = array( 'enabled' => array_key_exists( 'enabled', $mcp_config ) ? rest_sanitize_boolean( $mcp_config['enabled'] ) : $defaults['enabled'], 'enabled_features' => array_values( array_unique( $enabled_features ) ), 'mcp_tokens' => rwpai_sanitize_mcp_tokens( $mcp_config ), ); return $sanitized; } /** * Extract MCP runtime settings from the plugin config. * * @param array|null $config Optional plugin config. * @return array */ function rwpai_get_mcp_runtime_settings( $config = null ) { if ( ! is_array( $config ) ) { $config = rwpai_get_plugin_config(); } $mcp = isset( $config['mcp'] ) && is_array( $config['mcp'] ) ? $config['mcp'] : array(); $mcp = rwpai_sanitize_mcp_config( $mcp ); $enabled_features = $mcp['enabled_features'] ?? array(); $feature_map = rwpai_get_mcp_feature_option_map(); $runtime = array( 'enabled' => ! empty( $mcp['enabled'] ), ); foreach ( $feature_map as $feature_id => $option_key ) { $runtime[ $option_key ] = in_array( $feature_id, $enabled_features, true ); } return $runtime; } /** * Sync the MCP runtime option used by the embedded wordpress-mcp code. * * @param array|null $config Optional plugin config. * @return void */ function rwpai_sync_mcp_settings_option( $config = null ) { $settings = rwpai_get_mcp_runtime_settings( $config ); $current = get_option( 'wordpress_mcp_settings', null ); if ( false === $current || null === $current ) { add_option( 'wordpress_mcp_settings', $settings, '', false ); return; } if ( $current !== $settings ) { update_option( 'wordpress_mcp_settings', $settings ); } } /** * Unified function to get plugin config * * @param array $except Remove that key from plugin config. * * @return array Plugin config. */ function rwpai_get_plugin_config( $except = array() ) { $data = get_option( RWPAI_KV_STORAGE, RWPAI_DEFAULT_CONFIG ); if ( ! is_array( $data ) ) { $data = RWPAI_DEFAULT_CONFIG; } if ( isset( $data['credentials'] ) ) { $security_tools = new \RWPAI\Components\Tools\Rwpai_Security_Tools(); $data['credentials'] = $security_tools->decrypt( $data['credentials'] ); } $legacy_mcp_settings = get_option( 'wordpress_mcp_settings', array() ); $current_mcp = isset( $data['mcp'] ) && is_array( $data['mcp'] ) ? $data['mcp'] : array(); if ( isset( $current_mcp['client_token'] ) ) { $security_tools = new \RWPAI\Components\Tools\Rwpai_Security_Tools(); $current_mcp['client_token'] = $security_tools->decrypt( $current_mcp['client_token'] ); } if ( isset( $current_mcp['mcp_tokens'] ) && is_array( $current_mcp['mcp_tokens'] ) ) { $security_tools = new \RWPAI\Components\Tools\Rwpai_Security_Tools(); foreach ( $current_mcp['mcp_tokens'] as $index => $token_record ) { if ( ! is_array( $token_record ) || empty( $token_record['token'] ) ) { continue; } $current_mcp['mcp_tokens'][ $index ]['token'] = $security_tools->decrypt( $token_record['token'] ); } } $data['mcp'] = rwpai_sanitize_mcp_config( array_merge( rwpai_get_default_mcp_config(), is_array( $legacy_mcp_settings ) ? $legacy_mcp_settings : array(), $current_mcp ) ); foreach ( $except as $key => $value ) { unset( $data[ $value ] ); } return $data; } /** * Unified function to set plugin config * * @param array $config Plugin config. * @return void */ function rwpai_set_plugin_config( $config ) { if ( ! is_array( $config ) ) { $config = array(); } // Encrypt credentials before saving. if ( isset( $config['credentials'] ) ) { $security_tools = new \RWPAI\Components\Tools\Rwpai_Security_Tools(); $config['credentials'] = $security_tools->encrypt( $config['credentials'] ); } if ( isset( $config['mcp'] ) ) { $security_tools = new \RWPAI\Components\Tools\Rwpai_Security_Tools(); $mcp_config = rwpai_sanitize_mcp_config( $config['mcp'] ); if ( ! empty( $mcp_config['mcp_tokens'] ) ) { foreach ( $mcp_config['mcp_tokens'] as $index => $token_record ) { if ( empty( $token_record['token'] ) ) { continue; } $mcp_config['mcp_tokens'][ $index ]['token'] = $security_tools->encrypt( $token_record['token'] ); } } $config['mcp'] = $mcp_config; } update_option( RWPAI_KV_STORAGE, $config ); rwpai_sync_mcp_settings_option( $config ); } /** * Sets default options upon plugin activation. */ function rwpai_set_default_plugin_config() { // Only add the option if it doesn't already exist. if ( false === get_option( RWPAI_KV_STORAGE ) ) { rwpai_set_plugin_config( RWPAI_DEFAULT_CONFIG ); return; } rwpai_sync_mcp_settings_option(); } /** * REST API handler to get settings. * * @return WP_REST_Response JSON response. */ function rwpai_rest_get_settings() { $settings = rwpai_get_plugin_config(); return rest_ensure_response( array( 'status' => true, 'message' => 'Plugin config retrieved successfully.', 'data' => $settings, ) ); } /** * REST permission callback for saving settings. * * @param WP_REST_Request $request Request object. * @return bool|WP_Error True if permission granted, WP_Error otherwise. */ function rwpai_rest_only_admin( $request ) { $result = rwpai_verify_request_using_nonce_rest( $request, RWPAI_NONCE_NAME_ADMIN ); return true === $result ? true : $result; } /** * REST API handler to save settings. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error Response or error. */ function rwpai_rest_save_settings( $request ) { $settings = $request->get_param( 'settings' ); if ( empty( $settings ) ) { return new WP_Error( 'missing_settings', 'No settings data provided.', array( 'status' => 400 ) ); } // Validate that settings is an array or object, not a string. if ( ! is_array( $settings ) && ! is_object( $settings ) ) { return new WP_REST_Response( array( 'status' => false, 'message' => 'Settings must be an object or array, not a string or primitive value.', ), 400 ); } $validated = rwpai_validate_and_sanitize_settings( $settings ); if ( is_wp_error( $validated ) ) { return new WP_REST_Response( array( 'status' => false, 'message' => $validated->get_error_message(), ), 400 ); } $config_before = rwpai_get_plugin_config(); // Override old config with new settings. $new_config = array_merge( $config_before, $validated['settings'] ); rwpai_set_plugin_config( $new_config ); $response = array( 'status' => true, 'message' => empty( $validated['unrecognized_keys'] ) ? 'Settings saved successfully.' : 'Settings saved, but some keys were not recognized.', ); if ( ! empty( $validated['unrecognized_keys'] ) ) { $response['unrecognized_keys'] = $validated['unrecognized_keys']; } return rest_ensure_response( $response ); } /** * Validate and sanitize plugin settings. * * @param array $settings Raw settings input. * @return array|WP_Error Sanitized settings or WP_Error. */ function rwpai_validate_and_sanitize_settings( $settings ) { $sanitized = array(); $string_array_keys = array( 'exclude_from_post_categories', 'auto_add_for_post_types', 'exclude_elements', 'exclude_texts' ); $integrations_config = array( 'credentials', 'credentials_valid', 'ai_source', 'ai_config', 'mcp', ); $look_config = array( // 'player_style', ); // String / number / any. $tts_config_keys = array( 'plan', ...$integrations_config, ...$look_config, ); $allowed_keys = array_merge( $string_array_keys, $tts_config_keys, ); $unrecognized_keys = array(); // Track extra/unknown keys. foreach ( $settings as $key => $value ) { if ( ! in_array( $key, $allowed_keys, true ) ) { $unrecognized_keys[] = $key; } } // Sanitize string array fields. foreach ( $string_array_keys as $key ) { if ( isset( $settings[ $key ] ) ) { if ( ! is_array( $settings[ $key ] ) ) { return new WP_Error( 'invalid_field', "Invalid field: {$key}" ); } $sanitized[ $key ] = array_map( 'sanitize_text_field', $settings[ $key ] ); } } // Sanitize TTS config fields. foreach ( $tts_config_keys as $key ) { if ( isset( $settings[ $key ] ) ) { if ( 'mcp' === $key ) { $sanitized[ $key ] = rwpai_sanitize_mcp_config( $settings[ $key ] ); continue; } $sanitized[ $key ] = $settings[ $key ]; } } return array( 'settings' => $sanitized, 'unrecognized_keys' => $unrecognized_keys, ); } /** * Allow CORS from localhost:3001 and localhost:3002 * Only active when WordPress runs on localhost (for development only) */ function rwpai_allow_localhost_cors_headers() { $allowed_origins = array( 'http://localhost:3001', 'http://localhost:3002', ); // Only run when WP is on localhost. $site_url = get_site_url(); $is_local_site = ( str_contains( $site_url, 'localhost' ) || str_contains( $site_url, '127.0.0.1' ) ); if ( ! $is_local_site ) { return; } // Sanitize and validate HTTP_ORIGIN. $origin = ''; if ( isset( $_SERVER['HTTP_ORIGIN'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash $origin = esc_url_raw( wp_unslash( $_SERVER['HTTP_ORIGIN'] ) ); } // If origin is allowed → set headers. if ( $origin && in_array( $origin, $allowed_origins, true ) ) { header( 'Access-Control-Allow-Origin: ' . $origin ); header( 'Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE' ); header( 'Access-Control-Allow-Credentials: true' ); header( 'Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With, X-WP-Nonce' ); } // Handle preflight OPTIONS. if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'OPTIONS' === $_SERVER['REQUEST_METHOD'] ) { status_header( 200 ); exit; } } add_action( 'init', 'rwpai_allow_localhost_cors_headers' );