'GET', 'callback' => array( __CLASS__, 'get_requests' ), 'permission_callback' => array( __CLASS__, 'check_permission' ), 'args' => self::get_request_args(), ) ); register_rest_route( self::NAMESPACE, '/filters', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'get_filters' ), 'permission_callback' => array( __CLASS__, 'check_permission' ), ) ); register_rest_route( self::NAMESPACE, '/requests', array( 'methods' => 'DELETE', 'callback' => array( __CLASS__, 'clear_requests' ), 'permission_callback' => array( __CLASS__, 'check_permission' ), ) ); register_rest_route( self::NAMESPACE, '/stats', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'get_stats' ), 'permission_callback' => array( __CLASS__, 'check_permission' ), 'args' => array( 'date_from' => array( 'type' => 'string', 'sanitize_callback' => array( __CLASS__, 'sanitize_date' ), ), 'date_to' => array( 'type' => 'string', 'sanitize_callback' => array( __CLASS__, 'sanitize_date' ), ), ), ) ); register_rest_route( self::NAMESPACE, '/sessions/(?P[a-zA-Z0-9_.:-]+)', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'get_session' ), 'permission_callback' => array( __CLASS__, 'check_permission' ), 'args' => array( 'session_id' => array( 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_text_field', ), 'page' => array( 'type' => 'integer', 'default' => 1, 'minimum' => 1, 'sanitize_callback' => 'absint', ), 'per_page' => array( 'type' => 'integer', 'default' => 50, 'minimum' => 1, 'maximum' => 500, 'sanitize_callback' => 'absint', ), 'exclude_bodies' => array( 'type' => 'boolean', 'default' => false, ), ), ) ); register_rest_route( self::NAMESPACE, '/search', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'search_requests' ), 'permission_callback' => array( __CLASS__, 'check_permission' ), 'args' => array( 'query' => array( 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_text_field', ), 'page' => array( 'type' => 'integer', 'default' => 1, 'minimum' => 1, 'sanitize_callback' => 'absint', ), 'per_page' => array( 'type' => 'integer', 'default' => 20, 'minimum' => 1, 'maximum' => 500, 'sanitize_callback' => 'absint', ), ), ) ); register_rest_route( self::NAMESPACE, '/errors', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'get_errors' ), 'permission_callback' => array( __CLASS__, 'check_permission' ), 'args' => array( 'limit' => array( 'type' => 'integer', 'default' => 20, 'minimum' => 1, 'maximum' => 500, 'sanitize_callback' => 'absint', ), ), ) ); register_rest_route( self::NAMESPACE, '/tool-performance', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'get_tool_performance' ), 'permission_callback' => array( __CLASS__, 'check_permission' ), 'args' => array( 'tool_name' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), ), ) ); register_rest_route( self::NAMESPACE, '/retention', array( 'methods' => 'DELETE', 'callback' => array( __CLASS__, 'delete_before' ), 'permission_callback' => array( __CLASS__, 'check_permission' ), 'args' => array( 'before_date' => array( 'type' => 'string', 'required' => true, 'sanitize_callback' => array( __CLASS__, 'sanitize_date' ), ), ), ) ); register_rest_route( self::NAMESPACE, '/export-csv', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'export_csv' ), 'permission_callback' => array( __CLASS__, 'check_permission' ), 'args' => array( 'date_from' => array( 'type' => 'string', 'sanitize_callback' => array( __CLASS__, 'sanitize_date' ), ), 'date_to' => array( 'type' => 'string', 'sanitize_callback' => array( __CLASS__, 'sanitize_date' ), ), 'tool' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'tool_status' => array( 'type' => 'string', 'enum' => array( 'success', 'error', 'unknown' ), 'sanitize_callback' => 'sanitize_text_field', ), 'user_id' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'orderby' => array( 'type' => 'string', 'default' => 'created_at', 'enum' => array( 'created_at', 'user_id', 'route' ), 'sanitize_callback' => 'sanitize_text_field', ), 'order' => array( 'type' => 'string', 'default' => 'DESC', 'enum' => array( 'ASC', 'DESC' ), 'sanitize_callback' => 'sanitize_text_field', ), ), ) ); // Security events endpoints. register_rest_route( self::NAMESPACE, '/security-events', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'get_security_events' ), 'permission_callback' => array( __CLASS__, 'check_permission' ), 'args' => array( 'page' => array( 'type' => 'integer', 'default' => 1, 'minimum' => 1, 'sanitize_callback' => 'absint', ), 'per_page' => array( 'type' => 'integer', 'default' => 20, 'minimum' => 1, 'maximum' => 100, 'sanitize_callback' => 'absint', ), 'rule_name' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'severity' => array( 'type' => 'string', 'enum' => array( 'warning', 'critical' ), 'sanitize_callback' => 'sanitize_text_field', ), 'date_from' => array( 'type' => 'string', 'sanitize_callback' => array( __CLASS__, 'sanitize_date' ), ), 'date_to' => array( 'type' => 'string', 'sanitize_callback' => array( __CLASS__, 'sanitize_date' ), ), 'orderby' => array( 'type' => 'string', 'default' => 'created_at', 'enum' => array( 'created_at' ), 'sanitize_callback' => 'sanitize_text_field', ), 'order' => array( 'type' => 'string', 'default' => 'DESC', 'enum' => array( 'ASC', 'DESC' ), 'sanitize_callback' => 'sanitize_text_field', ), ), ) ); register_rest_route( self::NAMESPACE, '/security-events/export-csv', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'export_security_csv' ), 'permission_callback' => array( __CLASS__, 'check_permission' ), 'args' => array( 'rule_name' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'severity' => array( 'type' => 'string', 'enum' => array( 'warning', 'critical' ), 'sanitize_callback' => 'sanitize_text_field', ), 'date_from' => array( 'type' => 'string', 'sanitize_callback' => array( __CLASS__, 'sanitize_date' ), ), 'date_to' => array( 'type' => 'string', 'sanitize_callback' => array( __CLASS__, 'sanitize_date' ), ), ), ) ); register_rest_route( self::NAMESPACE, '/security-events/stats', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'get_security_stats' ), 'permission_callback' => array( __CLASS__, 'check_permission' ), ) ); register_rest_route( self::NAMESPACE, '/security-events/(?P\d+)/acknowledge', array( 'methods' => 'PUT', 'callback' => array( __CLASS__, 'acknowledge_security_event' ), 'permission_callback' => array( __CLASS__, 'check_permission' ), 'args' => array( 'id' => array( 'type' => 'integer', 'required' => true, 'sanitize_callback' => 'absint', ), 'acknowledged' => array( 'type' => 'boolean', 'default' => true, ), ), ) ); register_rest_route( self::NAMESPACE, '/security-events/clear-acknowledged', array( 'methods' => 'DELETE', 'callback' => array( __CLASS__, 'clear_acknowledged_events' ), 'permission_callback' => array( __CLASS__, 'check_permission' ), ) ); // Security settings endpoints. register_rest_route( self::NAMESPACE, '/security-settings', array( array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'get_security_settings' ), 'permission_callback' => array( __CLASS__, 'check_permission' ), ), array( 'methods' => 'PUT', 'callback' => array( __CLASS__, 'save_security_settings' ), 'permission_callback' => array( __CLASS__, 'check_permission' ), ), ) ); register_rest_route( self::NAMESPACE, '/security-settings/test-alert', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'send_test_alert' ), 'permission_callback' => array( __CLASS__, 'check_permission' ), ) ); } /** * Check if user has permission to access endpoints. * * @return bool */ public static function check_permission() { return current_user_can( 'manage_options' ); } /** * Get request query arguments. * * @return array */ private static function get_request_args() { return array( 'page' => array( 'type' => 'integer', 'default' => 1, 'minimum' => 1, 'sanitize_callback' => 'absint', ), 'per_page' => array( 'type' => 'integer', 'default' => 20, 'minimum' => 1, 'maximum' => 500, 'sanitize_callback' => 'absint', ), 'date_from' => array( 'type' => 'string', 'sanitize_callback' => array( __CLASS__, 'sanitize_date' ), ), 'date_to' => array( 'type' => 'string', 'sanitize_callback' => array( __CLASS__, 'sanitize_date' ), ), 'tool' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'tool_status' => array( 'type' => 'string', 'enum' => array( 'success', 'error', 'unknown' ), 'sanitize_callback' => 'sanitize_text_field', ), 'user_id' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'search' => array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), 'orderby' => array( 'type' => 'string', 'default' => 'created_at', 'enum' => array( 'created_at', 'user_id', 'route' ), 'sanitize_callback' => 'sanitize_text_field', ), 'order' => array( 'type' => 'string', 'default' => 'DESC', 'enum' => array( 'ASC', 'DESC' ), 'sanitize_callback' => 'sanitize_text_field', ), ); } /** * Sanitize and validate date or datetime input. * * Accepts Y-m-d (date only) or Y-m-d H:i:s / ISO 8601 (with time). * Returns Y-m-d when only a date is provided, Y-m-d H:i:s when time is included. * * @param string $date Date or datetime string. * @return string|null */ public static function sanitize_date( $date ) { if ( empty( $date ) ) { return null; } $timestamp = strtotime( $date ); if ( false === $timestamp ) { return null; } // If the input is longer than a bare date (Y-m-d = 10 chars), preserve time. if ( strlen( trim( $date ) ) > 10 ) { return gmdate( 'Y-m-d H:i:s', $timestamp ); } return gmdate( 'Y-m-d', $timestamp ); } /** * Get requests endpoint handler. * * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ public static function get_requests( $request ) { $args = array( 'page' => $request->get_param( 'page' ), 'per_page' => $request->get_param( 'per_page' ), 'date_from' => $request->get_param( 'date_from' ), 'date_to' => $request->get_param( 'date_to' ), 'tool' => $request->get_param( 'tool' ), 'tool_status' => $request->get_param( 'tool_status' ), 'user_id' => $request->get_param( 'user_id' ), 'search' => $request->get_param( 'search' ), 'orderby' => $request->get_param( 'orderby' ), 'order' => $request->get_param( 'order' ), ); $result = ALFMCP_DB::get_requests( $args ); $formatted_data = array_map( array( __CLASS__, 'format_request_item' ), $result['data'] ); return new \WP_REST_Response( array( 'data' => $formatted_data, 'total' => $result['total'], 'page' => $args['page'], 'per_page' => $args['per_page'], ), 200 ); } /** * Get filters data endpoint handler. * * @return \WP_REST_Response */ public static function get_filters() { $tools = ALFMCP_DB::get_unique_tools(); $user_ids = ALFMCP_DB::get_unique_users(); $users = array(); foreach ( $user_ids as $user_id ) { $user = get_userdata( $user_id ); if ( $user ) { $users[] = array( 'id' => $user_id, 'name' => $user->display_name, ); } } return new \WP_REST_Response( array( 'tools' => $tools, 'users' => $users, ), 200 ); } /** * Clear all requests endpoint handler. * * @return \WP_REST_Response */ public static function clear_requests() { $result = ALFMCP_DB::clear_all_requests(); if ( false === $result ) { return new \WP_REST_Response( array( 'message' => 'Failed to clear logs.' ), 500 ); } return new \WP_REST_Response( array( 'message' => 'All logs cleared.' ), 200 ); } /** * Get aggregate statistics. * * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ public static function get_stats( $request ) { $args = array( 'date_from' => $request->get_param( 'date_from' ), 'date_to' => $request->get_param( 'date_to' ), ); return new \WP_REST_Response( ALFMCP_DB::get_stats( $args ), 200 ); } /** * Get requests for a specific session. * * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ public static function get_session( $request ) { $result = ALFMCP_DB::get_by_session( array( 'session_id' => $request->get_param( 'session_id' ), 'page' => $request->get_param( 'page' ), 'per_page' => $request->get_param( 'per_page' ), 'exclude_bodies' => (bool) $request->get_param( 'exclude_bodies' ), ) ); return new \WP_REST_Response( $result, 200 ); } /** * Search requests by keyword. * * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ public static function search_requests( $request ) { $result = ALFMCP_DB::search_requests( array( 'query' => $request->get_param( 'query' ), 'page' => $request->get_param( 'page' ), 'per_page' => $request->get_param( 'per_page' ), ) ); return new \WP_REST_Response( $result, 200 ); } /** * Get recent errors. * * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ public static function get_errors( $request ) { $result = ALFMCP_DB::get_errors( $request->get_param( 'limit' ) ); return new \WP_REST_Response( $result, 200 ); } /** * Get per-tool performance metrics. * * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ public static function get_tool_performance( $request ) { $tool_name = $request->get_param( 'tool_name' ); $result = ALFMCP_DB::get_tool_performance( $tool_name ); return new \WP_REST_Response( $result, 200 ); } /** * Delete logs before a given date. * * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ public static function delete_before( $request ) { $before_date = $request->get_param( 'before_date' ); $result = ALFMCP_DB::delete_before( $before_date ); if ( false === $result ) { return new \WP_REST_Response( array( 'message' => 'Failed to delete logs.' ), 500 ); } return new \WP_REST_Response( array( 'deleted' => (int) $result ), 200 ); } /** * Export CSV endpoint handler. * * @param \WP_REST_Request $request Request object. */ public static function export_csv( $request ) { $args = array( 'date_from' => $request->get_param( 'date_from' ), 'date_to' => $request->get_param( 'date_to' ), 'tool' => $request->get_param( 'tool' ), 'tool_status' => $request->get_param( 'tool_status' ), 'user_id' => $request->get_param( 'user_id' ), 'orderby' => $request->get_param( 'orderby' ), 'order' => $request->get_param( 'order' ), ); $download_name = 'activity-log-for-mcp-export-' . gmdate( 'Y-m-d' ) . '.csv'; $csv_headers = array( 'ID', 'Date', 'MCP Method', 'Tool Name', 'Tool Status', 'User', 'Method', 'Route', 'Response Status' ); $csv_content = self::csv_row( $csv_headers ); $batch_size = 500; $page = 1; while ( true ) { $result = ALFMCP_DB::get_requests( array_merge( $args, array( 'page' => $page, 'per_page' => $batch_size, ) ) ); $rows = $result['data']; if ( empty( $rows ) ) { break; } foreach ( $rows as $item ) { $mcp_method = ''; if ( ! empty( $item['request_body'] ) ) { $body = json_decode( $item['request_body'], true ); if ( is_array( $body ) && isset( $body['method'] ) ) { $mcp_method = $body['method']; } } $csv_content .= self::csv_row( array( $item['id'], $item['created_at'], $mcp_method, $item['tool_name'] ?? '', $item['tool_status'] ?? 'unknown', ALFMCP_Helper::get_user_display( $item['user_id'] ), $item['method'], $item['route'], $item['response_status'] ?? '', ) ); } if ( count( $rows ) < $batch_size ) { break; } ++$page; } header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="' . $download_name . '"' ); header( 'Content-Length: ' . strlen( $csv_content ) ); header( 'Pragma: no-cache' ); header( 'Expires: 0' ); // wp_die with an empty title and a custom handler terminates output cleanly. // The CSV content is pre-escaped via csv_row() using RFC 4180 quoting. print $csv_content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV file data, pre-escaped via csv_row(). exit; } /** * Convert an array of values to a single CSV row string. * * @param array $fields Row values. * @return string CSV row with newline. */ private static function csv_row( $fields ) { $escaped = array_map( function ( $field ) { $field = (string) $field; // Neutralize CSV formula injection: prefix fields that begin with // =, +, -, @, tab, or CR with a single quote so spreadsheet apps // treat them as text, not formulas. if ( '' !== $field && false !== strpbrk( $field[0], "=+-@\t\r" ) ) { $field = "'" . $field; } if ( strpbrk( $field, ",\"\r\n" ) !== false ) { return '"' . str_replace( '"', '""', $field ) . '"'; } return $field; }, $fields ); return implode( ',', $escaped ) . "\n"; } /** * Format a single request item for API response. * * @param array $item Database row. * @return array */ private static function format_request_item( $item ) { $mcp_method = null; if ( ! empty( $item['request_body'] ) ) { $body = json_decode( $item['request_body'], true ); if ( is_array( $body ) && isset( $body['method'] ) ) { $mcp_method = $body['method']; } } return array( 'id' => absint( $item['id'] ), 'route' => esc_html( $item['route'] ), 'server_namespace' => esc_html( $item['server_namespace'] ), 'tool_name' => ( 'tools/call' === $mcp_method && $item['tool_name'] ) ? esc_html( $item['tool_name'] ) : null, 'user_id' => $item['user_id'] ? absint( $item['user_id'] ) : null, 'user_display' => ALFMCP_Helper::get_user_display( $item['user_id'] ), 'method' => esc_html( $item['method'] ), 'mcp_method' => $mcp_method ? esc_html( $mcp_method ) : '—', 'mcp_session_id' => esc_html( $item['mcp_session_id'] ), 'headers' => $item['headers'], 'request_body' => $item['request_body'], 'response_status' => $item['response_status'] ? absint( $item['response_status'] ) : null, 'response_body' => $item['response_body'], 'tool_status' => isset( $item['tool_status'] ) ? esc_html( $item['tool_status'] ) : 'unknown', 'created_at' => esc_html( $item['created_at'] ), 'created_at_formatted' => ALFMCP_Helper::format_date( $item['created_at'] ), ); } /** * Get security events with filters. * * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ public static function get_security_events( $request ) { $result = ALFMCP_DB::get_security_events( array( 'page' => $request->get_param( 'page' ), 'per_page' => $request->get_param( 'per_page' ), 'rule_name' => $request->get_param( 'rule_name' ), 'severity' => $request->get_param( 'severity' ), 'date_from' => $request->get_param( 'date_from' ), 'date_to' => $request->get_param( 'date_to' ), 'orderby' => $request->get_param( 'orderby' ), 'order' => $request->get_param( 'order' ), ) ); $result['data'] = array_map( function ( $item ) { $item['created_at_formatted'] = ALFMCP_Helper::format_date( $item['created_at'] ); return $item; }, $result['data'] ); return new \WP_REST_Response( $result, 200 ); } /** * Get security stats for dashboard cards. * * @return \WP_REST_Response */ public static function get_security_stats() { return new \WP_REST_Response( ALFMCP_DB::get_security_stats(), 200 ); } /** * Acknowledge a security event. * * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ public static function acknowledge_security_event( $request ) { $id = $request->get_param( 'id' ); $acknowledged = (bool) $request->get_param( 'acknowledged' ); $result = ALFMCP_DB::acknowledge_security_event( $id, $acknowledged ); if ( ! $result ) { return new \WP_REST_Response( array( 'message' => 'Failed to update event.' ), 500 ); } $label = $acknowledged ? 'acknowledged' : 'unacknowledged'; return new \WP_REST_Response( array( 'message' => "Event {$label}." ), 200 ); } /** * Clear all acknowledged security events. * * @return \WP_REST_Response */ public static function clear_acknowledged_events() { $result = ALFMCP_DB::clear_acknowledged_events(); if ( false === $result ) { return new \WP_REST_Response( array( 'message' => 'Failed to clear events.' ), 500 ); } return new \WP_REST_Response( array( 'deleted' => (int) $result ), 200 ); } /** * Export security events as CSV. * * @param \WP_REST_Request $request Request object. */ public static function export_security_csv( $request ) { $args = array( 'rule_name' => $request->get_param( 'rule_name' ), 'severity' => $request->get_param( 'severity' ), 'date_from' => $request->get_param( 'date_from' ), 'date_to' => $request->get_param( 'date_to' ), ); $download_name = 'activity-log-for-mcp-security-events-' . gmdate( 'Y-m-d' ) . '.csv'; $csv_headers = array( 'ID', 'Date', 'Rule', 'Severity', 'Session ID', 'Details', 'Request Status', 'Acknowledged' ); $csv_content = self::csv_row( $csv_headers ); $batch_size = 500; $page = 1; while ( true ) { $result = ALFMCP_DB::get_security_events( array_merge( $args, array( 'page' => $page, 'per_page' => $batch_size, ) ) ); $rows = $result['data']; if ( empty( $rows ) ) { break; } $rule_labels = array( 'velocity' => 'Velocity', 'destructive_tool' => 'Destructive Tool', 'error_storm' => 'Error Storm', 'mcp_auto_disabled' => 'MCP Auto-Disabled', ); foreach ( $rows as $item ) { $details = ''; $request_status = ''; if ( ! empty( $item['details'] ) ) { $d = json_decode( $item['details'], true ); if ( is_array( $d ) ) { if ( isset( $d['triggered_by'] ) ) { $details = 'Triggered by: ' . ( $rule_labels[ $d['triggered_by'] ] ?? $d['triggered_by'] ); } elseif ( isset( $d['tool_name'] ) ) { $details = 'Tool: ' . $d['tool_name']; } elseif ( isset( $d['count'] ) ) { $details = $d['count'] . ' in ' . $d['window_minutes'] . 'min (limit: ' . $d['threshold'] . ')'; } if ( isset( $d['request_status'] ) ) { $request_status = $d['request_status']; } if ( isset( $d['note'] ) ) { $details .= ' — ' . $d['note']; } } } $csv_content .= self::csv_row( array( $item['id'], $item['created_at'], $rule_labels[ $item['rule_name'] ] ?? $item['rule_name'], $item['severity'], $item['session_id'] ?? '', $details, $request_status, $item['acknowledged'] ? 'Yes' : 'No', ) ); } if ( count( $rows ) < $batch_size ) { break; } ++$page; } header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="' . $download_name . '"' ); header( 'Content-Length: ' . strlen( $csv_content ) ); header( 'Pragma: no-cache' ); header( 'Expires: 0' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV file data, pre-escaped via csv_row(). print $csv_content; exit; } /** * Get security settings. * * @return \WP_REST_Response */ public static function get_security_settings() { $settings = ALFMCP_Security::get_settings(); // Auto-populate destructive tools if still at defaults. $default_tools = array( 'wp_delete_post', 'wp_delete_page', 'wp_delete_user', 'wp_delete_comment', 'wp_delete_media_item', 'clear-old-logs' ); if ( $settings['rules']['destructive_tool']['tools'] === $default_tools ) { $auto = ALFMCP_Security::auto_populate_destructive_tools(); if ( ! empty( $auto ) ) { $settings['rules']['destructive_tool']['tools'] = array_unique( array_merge( $default_tools, $auto ) ); } } return new \WP_REST_Response( $settings, 200 ); } /** * Save security settings. * * @param \WP_REST_Request $request Request object. * @return \WP_REST_Response */ public static function save_security_settings( $request ) { $body = $request->get_json_params(); if ( empty( $body ) || ! is_array( $body ) ) { return new \WP_REST_Response( array( 'message' => 'Invalid settings.' ), 400 ); } $settings = ALFMCP_Security::get_settings(); if ( isset( $body['enabled'] ) ) { $settings['enabled'] = (bool) $body['enabled']; } if ( isset( $body['email_alerts'] ) ) { $settings['email_alerts'] = (bool) $body['email_alerts']; } if ( isset( $body['daily_digest'] ) ) { $settings['daily_digest'] = (bool) $body['daily_digest']; } if ( isset( $body['mcp_disabled'] ) ) { $new_disabled = (bool) $body['mcp_disabled']; if ( $new_disabled && ! $settings['mcp_disabled'] ) { $settings['mcp_disabled_reason'] = 'Manually disabled by administrator via settings.'; $settings['mcp_disabled_at'] = current_time( 'mysql' ); } elseif ( ! $new_disabled && $settings['mcp_disabled'] ) { $settings['mcp_disabled_reason'] = ''; $settings['mcp_disabled_at'] = ''; } $settings['mcp_disabled'] = $new_disabled; } if ( isset( $body['alert_emails'] ) && is_array( $body['alert_emails'] ) ) { $settings['alert_emails'] = array_values( array_filter( array_map( 'sanitize_email', $body['alert_emails'] ) ) ); } if ( isset( $body['digest_emails'] ) && is_array( $body['digest_emails'] ) ) { $settings['digest_emails'] = array_values( array_filter( array_map( 'sanitize_email', $body['digest_emails'] ) ) ); } if ( isset( $body['alert_email'] ) ) { $settings['alert_email'] = sanitize_email( $body['alert_email'] ); } if ( isset( $body['rules'] ) && is_array( $body['rules'] ) ) { foreach ( array( 'velocity', 'error_storm' ) as $rule_key ) { if ( isset( $body['rules'][ $rule_key ] ) && is_array( $body['rules'][ $rule_key ] ) ) { $r = $body['rules'][ $rule_key ]; if ( isset( $r['enabled'] ) ) { $settings['rules'][ $rule_key ]['enabled'] = (bool) $r['enabled']; } if ( isset( $r['threshold'] ) ) { $settings['rules'][ $rule_key ]['threshold'] = max( 1, absint( $r['threshold'] ) ); } if ( isset( $r['window_minutes'] ) ) { $settings['rules'][ $rule_key ]['window_minutes'] = min( 60, max( 1, absint( $r['window_minutes'] ) ) ); } if ( isset( $r['severity'] ) && in_array( $r['severity'], array( 'warning', 'critical' ), true ) ) { $settings['rules'][ $rule_key ]['severity'] = $r['severity']; } if ( isset( $r['auto_disable'] ) ) { $settings['rules'][ $rule_key ]['auto_disable'] = (bool) $r['auto_disable']; } } } if ( isset( $body['rules']['destructive_tool'] ) && is_array( $body['rules']['destructive_tool'] ) ) { $dt = $body['rules']['destructive_tool']; if ( isset( $dt['enabled'] ) ) { $settings['rules']['destructive_tool']['enabled'] = (bool) $dt['enabled']; } if ( isset( $dt['severity'] ) && in_array( $dt['severity'], array( 'warning', 'critical' ), true ) ) { $settings['rules']['destructive_tool']['severity'] = $dt['severity']; } if ( isset( $dt['auto_disable'] ) ) { $settings['rules']['destructive_tool']['auto_disable'] = (bool) $dt['auto_disable']; } if ( isset( $dt['tools'] ) && is_array( $dt['tools'] ) ) { $settings['rules']['destructive_tool']['tools'] = array_values( array_filter( array_map( 'sanitize_text_field', $dt['tools'] ) ) ); } } } ALFMCP_Security::save_settings( $settings ); return new \WP_REST_Response( $settings, 200 ); } /** * Send a test alert email. * * @return \WP_REST_Response */ public static function send_test_alert() { $result = ALFMCP_Security::send_test_alert(); if ( ! $result ) { return new \WP_REST_Response( array( 'message' => 'Email delivery failed. Check your wp_mail configuration.' ), 500 ); } return new \WP_REST_Response( array( 'message' => 'Test email sent successfully.' ), 200 ); } }