'POST', 'callback' => [ __CLASS__, 'detect' ], 'permission_callback' => [ __CLASS__, 'can_manage' ], 'args' => [ 'content' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_textarea_field', ], ], ] ); register_rest_route( self::NS, '/scan-post', [ 'methods' => 'POST', 'callback' => [ __CLASS__, 'scan_post' ], 'permission_callback' => [ __CLASS__, 'can_manage' ], 'args' => [ 'post_id' => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint', ], ], ] ); register_rest_route( self::NS, '/stats', [ 'methods' => 'GET', 'callback' => [ __CLASS__, 'get_stats' ], 'permission_callback' => [ __CLASS__, 'can_manage' ], ] ); register_rest_route( self::NS, '/compliance', [ 'methods' => 'GET', 'callback' => [ __CLASS__, 'get_compliance' ], 'permission_callback' => [ __CLASS__, 'can_manage' ], ] ); register_rest_route( self::NS, '/post-status/(?P\d+)', [ 'methods' => 'GET', 'callback' => [ __CLASS__, 'get_post_status' ], 'permission_callback' => [ __CLASS__, 'can_manage' ], 'args' => [ 'id' => [ 'type' => 'integer', 'sanitize_callback' => 'absint', ], ], ] ); register_rest_route( self::NS, '/bulk-scan', [ 'methods' => 'POST', 'callback' => [ __CLASS__, 'bulk_scan' ], 'permission_callback' => [ __CLASS__, 'can_manage' ], 'args' => [ 'post_type' => [ 'type' => 'string', 'default' => 'post', 'enum' => [ 'post', 'page', 'all' ] ], 'offset' => [ 'type' => 'integer', 'default' => 0 ], 'limit' => [ 'type' => 'integer', 'default' => 10 ], ], ] ); } public static function can_manage() { return current_user_can( 'manage_options' ); } public static function detect( $request ) { $content = $request->get_param( 'content' ); if ( empty( $content ) ) { return new WP_REST_Response( [ 'error' => 'Content is required.' ], 400 ); } $engine = AI_Explainer_Pro_Detection_Engine::get_instance(); $result = $engine->detect( $content ); return new WP_REST_Response( $result, 200 ); } public static function scan_post( $request ) { $post_id = $request->get_param( 'post_id' ); $post = get_post( $post_id ); if ( ! $post || ! in_array( $post->post_status, [ 'publish', 'draft' ], true ) ) { return new WP_REST_Response( [ 'error' => 'Post not found.' ], 404 ); } $engine = AI_Explainer_Pro_Detection_Engine::get_instance(); $content = wp_strip_all_tags( $post->post_content ); $result = $engine->detect( $content ); $human_score = 1 - $result['score']; update_post_meta( $post_id, '_ai_explainer_human_score', round( $human_score, 3 ) ); update_post_meta( $post_id, '_ai_explainer_ai_score', round( $result['score'], 3 ) ); update_post_meta( $post_id, '_ai_explainer_confidence', $result['confidence'] ); update_post_meta( $post_id, '_ai_explainer_scanned_date', current_time( 'mysql' ) ); update_post_meta( $post_id, '_ai_explainer_word_count', $result['word_count'] ); update_post_meta( $post_id, '_ai_explainer_methods', wp_json_encode( $result['methods'] ) ); update_post_meta( $post_id, '_ai_explainer_detection_data', $result ); if ( ! empty( $result['content_hash'] ) ) { update_post_meta( $post_id, '_ai_explainer_content_hash', $result['content_hash'] ); } $result['post_id'] = $post_id; $result['post_title'] = $post->post_title; $result['human_score'] = $human_score; /** Post-scan hook (OpenTimestamps, agency push, etc.) */ do_action( 'aiep_after_scan_post', $post_id, $result ); return new WP_REST_Response( $result, 200 ); } public static function get_stats() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Aggregate read on postmeta for REST stats endpoint; not cached because freshness is required. $total_scanned = (int) $wpdb->get_var( "SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key = '_ai_explainer_scanned_date'" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Aggregate read on postmeta for REST stats endpoint. $human_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key = '_ai_explainer_human_score' AND CAST(meta_value AS DECIMAL(5,3)) >= %f", 0.7 ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Aggregate read on postmeta for REST stats endpoint. $ai_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key = '_ai_explainer_human_score' AND CAST(meta_value AS DECIMAL(5,3)) < %f", 0.4 ) ); return new WP_REST_Response( [ 'total_published' => (int) wp_count_posts( 'post' )->publish + (int) wp_count_posts( 'page' )->publish, 'total_scanned' => $total_scanned, 'human_content' => $human_count, 'ai_content' => $ai_count, 'mixed_content' => $total_scanned - $human_count - $ai_count, ], 200 ); } public static function get_compliance() { return new WP_REST_Response( AI_Explainer_Pro_Compliance::evaluate(), 200 ); } public static function get_post_status( $request ) { $post_id = $request->get_param( 'id' ); $post = get_post( $post_id ); if ( ! $post ) { return new WP_REST_Response( [ 'error' => 'Post not found.' ], 404 ); } $scanned_date = get_post_meta( $post_id, '_ai_explainer_scanned_date', true ); if ( empty( $scanned_date ) ) { return new WP_REST_Response( [ 'post_id' => $post_id, 'scanned' => false, 'message' => 'Post has not been scanned yet.', ], 200 ); } $methods_json = get_post_meta( $post_id, '_ai_explainer_methods', true ); return new WP_REST_Response( [ 'post_id' => $post_id, 'post_title' => $post->post_title, 'scanned' => true, 'scanned_date' => $scanned_date, 'human_score' => (float) get_post_meta( $post_id, '_ai_explainer_human_score', true ), 'ai_score' => (float) get_post_meta( $post_id, '_ai_explainer_ai_score', true ), 'confidence' => get_post_meta( $post_id, '_ai_explainer_confidence', true ), 'word_count' => (int) get_post_meta( $post_id, '_ai_explainer_word_count', true ), 'declaration' => get_post_meta( $post_id, '_ai_explainer_content_type', true ), 'methods' => $methods_json ? json_decode( $methods_json, true ) : null, ], 200 ); } public static function bulk_scan( $request ) { $post_type = $request->get_param( 'post_type' ); $offset = (int) $request->get_param( 'offset' ); $limit = min( (int) $request->get_param( 'limit' ), 50 ); $args = [ 'post_status' => 'publish', 'posts_per_page' => $limit, 'offset' => $offset, 'orderby' => 'ID', 'order' => 'ASC', 'fields' => 'ids', ]; $args['post_type'] = ( 'all' === $post_type ) ? [ 'post', 'page' ] : $post_type; $post_ids = get_posts( $args ); $engine = AI_Explainer_Pro_Detection_Engine::get_instance(); $results = []; foreach ( $post_ids as $pid ) { $post = get_post( $pid ); $content = wp_strip_all_tags( $post->post_content ); $det = $engine->detect( $content ); $human = 1 - $det['score']; update_post_meta( $pid, '_ai_explainer_human_score', round( $human, 3 ) ); update_post_meta( $pid, '_ai_explainer_ai_score', round( $det['score'], 3 ) ); update_post_meta( $pid, '_ai_explainer_confidence', $det['confidence'] ); update_post_meta( $pid, '_ai_explainer_scanned_date', current_time( 'mysql' ) ); update_post_meta( $pid, '_ai_explainer_word_count', $det['word_count'] ); update_post_meta( $pid, '_ai_explainer_methods', wp_json_encode( $det['methods'] ) ); update_post_meta( $pid, '_ai_explainer_detection_data', $det ); if ( ! empty( $det['content_hash'] ) ) { update_post_meta( $pid, '_ai_explainer_content_hash', $det['content_hash'] ); } $engine->clear_cache(); /** Post-scan hook (OpenTimestamps, agency push, etc.) */ do_action( 'aiep_after_scan_post', $pid, $det ); $results[] = [ 'post_id' => $pid, 'title' => $post->post_title, 'ai_score' => round( $det['score'], 3 ), 'human_score' => round( $human, 3 ), 'confidence' => $det['confidence'], ]; } $count_args = $args; $count_args['offset'] = $offset + count( $results ); $count_args['fields'] = 'ids'; $remaining = count( get_posts( $count_args ) ); return new WP_REST_Response( [ 'scanned' => $results, 'count' => count( $results ), 'offset' => $offset + count( $results ), 'has_more' => $remaining > 0, 'remaining' => $remaining, ], 200 ); } }