\d+)', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'analyze_content' ), 'permission_callback' => array( __CLASS__, 'check_edit_permission' ), 'args' => array( 'post_id' => array( 'required' => true, 'type' => 'integer', 'validate_callback' => function( $param ) { return is_numeric( $param ) && $param > 0; }, ), 'content' => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => 'wp_kses_post', ), ), ) ); // Score endpoint - get only score data register_rest_route( self::NAMESPACE, '/score/(?P\d+)', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'get_score' ), 'permission_callback' => array( __CLASS__, 'check_read_permission' ), 'args' => array( 'post_id' => array( 'required' => true, 'type' => 'integer', 'validate_callback' => function( $param ) { return is_numeric( $param ) && $param > 0; }, ), ), ) ); // Tips endpoint - get only improvement tips register_rest_route( self::NAMESPACE, '/tips/(?P\d+)', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'get_tips' ), 'permission_callback' => array( __CLASS__, 'check_read_permission' ), 'args' => array( 'post_id' => array( 'required' => true, 'type' => 'integer', 'validate_callback' => function( $param ) { return is_numeric( $param ) && $param > 0; }, ), ), ) ); // Visibility endpoint - get only visibility checks register_rest_route( self::NAMESPACE, '/visibility/(?P\d+)', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'get_visibility' ), 'permission_callback' => array( __CLASS__, 'check_read_permission' ), 'args' => array( 'post_id' => array( 'required' => true, 'type' => 'integer', 'validate_callback' => function( $param ) { return is_numeric( $param ) && $param > 0; }, ), ), ) ); } /** * Check if user can edit the post. * * @param WP_REST_Request $request Request object. * @return bool True if user can edit. */ public static function check_edit_permission( $request ) { $post_id = $request->get_param( 'post_id' ); if ( ! $post_id ) { return false; } return current_user_can( 'edit_post', $post_id ); } /** * Check if user can read the post. * * @param WP_REST_Request $request Request object. * @return bool True if user can read. */ public static function check_read_permission( $request ) { $post_id = $request->get_param( 'post_id' ); if ( ! $post_id ) { return false; } return current_user_can( 'read_post', $post_id ); } /** * Analyze content endpoint - unified analysis. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error Response object. */ public static function analyze_content( $request ) { $post_id = $request->get_param( 'post_id' ); $content = $request->get_param( 'content' ); // Rate limiting check if ( ! self::check_rate_limit( $post_id ) ) { return new WP_Error( 'rate_limit_exceeded', __( 'Rate limit exceeded. Please wait before requesting another analysis.', 'ai-answer-toolkit' ), array( 'status' => 429 ) ); } // Get post $post = get_post( $post_id ); if ( ! $post ) { return new WP_Error( 'post_not_found', __( 'Post not found.', 'ai-answer-toolkit' ), array( 'status' => 404 ) ); } // Check cache $cache_key = self::get_cache_key( $post_id, $content ); $cached = get_transient( $cache_key ); if ( $cached !== false ) { return rest_ensure_response( array( 'cached' => true, 'data' => $cached, ) ); } // Temporarily update content if provided $original_content = $post->post_content; if ( $content ) { $post->post_content = $content; $GLOBALS['post'] = $post; } // Calculate all components $response = array( 'score' => self::calculate_score_data( $post_id ), 'tips' => self::calculate_tips_data( $post_id ), 'visibility' => self::calculate_visibility_data( $post_id ), 'guidance' => self::calculate_guidance_data( $post_id ), 'timestamp' => current_time( 'timestamp' ), ); // Apply subscription filter (for future gating) $response = apply_filters( 'aitk_rest_analyze_response', $response, $post_id ); // Restore original content if ( $content ) { $post->post_content = $original_content; $GLOBALS['post'] = $post; } // Cache response set_transient( $cache_key, $response, 5 ); // 5 seconds cache // Update rate limit self::update_rate_limit( $post_id ); return rest_ensure_response( array( 'cached' => false, 'data' => $response, ) ); } /** * Get score endpoint. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error Response object. */ public static function get_score( $request ) { $post_id = $request->get_param( 'post_id' ); $score_data = self::calculate_score_data( $post_id ); return rest_ensure_response( array( 'data' => $score_data, ) ); } /** * Get tips endpoint. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error Response object. */ public static function get_tips( $request ) { $post_id = $request->get_param( 'post_id' ); $tips_data = self::calculate_tips_data( $post_id ); return rest_ensure_response( array( 'data' => $tips_data, ) ); } /** * Get visibility endpoint. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error Response object. */ public static function get_visibility( $request ) { $post_id = $request->get_param( 'post_id' ); $visibility_data = self::calculate_visibility_data( $post_id ); return rest_ensure_response( array( 'data' => $visibility_data, ) ); } /** * Calculate score data. * * @param int $post_id Post ID. * @return array Score data. */ private static function calculate_score_data( $post_id ) { $score_data = AI_Answer_Toolkit_Score::calculate_score( $post_id ); return array( 'unified_score' => $score_data['score'], 'label' => $score_data['label'], 'color' => $score_data['color'], 'status' => $score_data['status'], 'breakdown' => $score_data['breakdown'], ); } /** * Calculate tips data. * * @param int $post_id Post ID. * @return array Tips data. */ private static function calculate_tips_data( $post_id ) { $tips = AI_Answer_Toolkit_AI_Suggestions::get_improvement_hints( $post_id ); // Apply subscription filter (for future gating) $tips = apply_filters( 'aitk_rest_tips_response', $tips, $post_id ); return $tips; } /** * Calculate visibility data. * * @param int $post_id Post ID. * @return array Visibility data. */ private static function calculate_visibility_data( $post_id ) { $visibility_data = AI_Answer_Toolkit_Visibility::analyze_visibility( $post_id ); return array( 'score' => $visibility_data['score'], 'checks' => $visibility_data['checks'], 'warnings' => $visibility_data['warnings'], ); } /** * Calculate guidance data. * * @param int $post_id Post ID. * @return array Guidance data. */ private static function calculate_guidance_data( $post_id ) { $guidance = AI_Answer_Toolkit_Content_Guidance::analyze_content( $post_id ); return $guidance; } /** * Get cache key for analysis. * * @param int $post_id Post ID. * @param string $content Content hash. * @return string Cache key. */ private static function get_cache_key( $post_id, $content = '' ) { $content_hash = md5( $content ); return self::CACHE_GROUP . '_' . $post_id . '_' . $content_hash; } /** * Check rate limit for post analysis. * * @param int $post_id Post ID. * @return bool True if within rate limit. */ private static function check_rate_limit( $post_id ) { $rate_key = 'aitk_rate_limit_' . $post_id . '_' . get_current_user_id(); $requests = get_transient( $rate_key ); // Allow 10 requests per minute $max_requests = apply_filters( 'aitk_rate_limit_max', 10 ); if ( $requests !== false && $requests >= $max_requests ) { return false; } return true; } /** * Update rate limit counter. * * @param int $post_id Post ID. */ private static function update_rate_limit( $post_id ) { $rate_key = 'aitk_rate_limit_' . $post_id . '_' . get_current_user_id(); $requests = get_transient( $rate_key ); if ( $requests === false ) { set_transient( $rate_key, 1, 60 ); // 1 minute } else { set_transient( $rate_key, $requests + 1, 60 ); } } /** * Clear cache for a post. * * @param int $post_id Post ID. */ public static function clear_cache( $post_id ) { global $wpdb; $pattern = '_transient_' . self::CACHE_GROUP . '_' . $post_id . '_%'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $pattern ) ); } }