verify_ajax_nonce() ) { wp_send_json_error( 'Invalid security token' ); return; } // Get parameters - nonce already verified above // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce already verified in verify_ajax_nonce() $post_type = isset( $_GET['post_type'] ) ? sanitize_key( $_GET['post_type'] ) : 'post'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce already verified in verify_ajax_nonce() $current_post_id = isset( $_GET['current_id'] ) ? absint( $_GET['current_id'] ) : 0; // Verify post type if ( ! $this->is_allowed_post_type( $post_type ) ) { wp_send_json_error( 'Invalid post type' ); return; } if ( ! $current_post_id ) { wp_send_json_error( 'No current post ID provided' ); return; } // Get sort parameters $sort_params = $this->get_sort_parameters(); // Get context posts $context_data = $this->get_context_posts( $current_post_id, $post_type, $sort_params ); wp_send_json_success( $context_data ); } /** * AJAX handler for searching posts */ public function ajax_search_posts() { // Verify nonce if ( ! $this->verify_ajax_nonce() ) { wp_send_json_error( 'Invalid security token' ); return; } // Get parameters - nonce already verified above // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce already verified in verify_ajax_nonce() $post_type = isset( $_GET['post_type'] ) ? sanitize_key( $_GET['post_type'] ) : 'post'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce already verified in verify_ajax_nonce() $search_term = isset( $_GET['search'] ) ? sanitize_text_field( wp_unslash( $_GET['search'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce already verified in verify_ajax_nonce() $current_post_id = isset( $_GET['current_id'] ) ? absint( $_GET['current_id'] ) : 0; // Verify post type if ( ! $this->is_allowed_post_type( $post_type ) ) { wp_send_json_error( 'Invalid post type' ); return; } if ( strlen( $search_term ) < 2 ) { wp_send_json_error( 'Search term too short' ); return; } // Get sort parameters $sort_params = $this->get_sort_parameters(); // Search posts $search_results = $this->search_posts( $search_term, $post_type, $sort_params, $current_post_id ); wp_send_json_success( $search_results ); } /** * Get context posts (first, previous 1, current, next 1, last) */ private function get_context_posts( $current_post_id, $post_type, $sort_params ) { $result = array( 'posts' => array(), 'current_position' => 0, 'total_posts' => 0, 'status_counts' => array(), ); // Get total counts for each status from database FIRST $status_counts = $this->get_total_status_counts( $post_type ); $result['status_counts'] = $status_counts; // Build base query args $base_args = array( 'post_type' => $post_type, 'post_status' => $this->get_post_statuses( $sort_params['post_status'] ), 'fields' => 'ids', 'posts_per_page' => -1, 'orderby' => $this->build_orderby_array( $sort_params['orderby'], $sort_params['order'] ), ); // Get all post IDs in order $all_posts_query = new WP_Query( $base_args ); $all_post_ids = $all_posts_query->posts; $total_posts = count( $all_post_ids ); // Find current post position $current_position = array_search( $current_post_id, $all_post_ids ); if ( false === $current_position ) { return $result; } $result['current_position'] = $current_position + 1; // 1-based for display $result['total_posts'] = $total_posts; // Collect posts to fetch $posts_to_fetch = array(); // First post if ( $total_posts > 0 && $all_post_ids[0] != $current_post_id ) { $posts_to_fetch['first'] = $all_post_ids[0]; } // Previous 1 post only $prev_index = $current_position - 1; if ( $prev_index >= 0 && $prev_index < $total_posts ) { $posts_to_fetch['prev_1'] = $all_post_ids[$prev_index]; } // Current post $posts_to_fetch['current'] = $current_post_id; // Next 1 post only $next_index = $current_position + 1; if ( $next_index < $total_posts ) { $posts_to_fetch['next_1'] = $all_post_ids[$next_index]; } // Last post if ( $total_posts > 0 && $all_post_ids[$total_posts - 1] != $current_post_id ) { $posts_to_fetch['last'] = $all_post_ids[$total_posts - 1]; } // Fetch post details if ( ! empty( $posts_to_fetch ) ) { $posts_data = $this->get_posts_by_ids( array_values( $posts_to_fetch ), $sort_params ); // Build result maintaining order foreach ( $posts_to_fetch as $key => $post_id ) { foreach ( $posts_data as $post_data ) { if ( $post_data['id'] == $post_id ) { $post_data['context_type'] = $key; $result['posts'][] = $post_data; break; } } } } return $result; } /** * Search posts */ private function search_posts( $search_term, $post_type, $sort_params, $current_post_id ) { $args = array( 'post_type' => $post_type, 'post_status' => $this->get_post_statuses( $sort_params['post_status'] ), 'posts_per_page' => 50, // Reasonable limit for search results 'orderby' => $this->build_orderby_array( $sort_params['orderby'], $sort_params['order'] ), 's' => $search_term, ); $query = new WP_Query( $args ); $posts = array(); foreach ( $query->posts as $post ) { $post_data = $this->format_post_data( $post, $sort_params ); $post_data['is_current'] = ( $post->ID == $current_post_id ); $posts[] = $post_data; } // Get total status counts from database $status_counts = $this->get_total_status_counts( $post_type ); return array( 'posts' => $posts, 'total_found' => $query->found_posts, 'search_term' => $search_term, 'status_counts' => $status_counts, ); } /** * Get posts by IDs */ private function get_posts_by_ids( $post_ids, $sort_params ) { if ( empty( $post_ids ) ) { return array(); } $args = array( 'post__in' => $post_ids, 'post_type' => 'any', 'post_status' => 'any', 'posts_per_page' => -1, 'orderby' => 'post__in', ); $query = new WP_Query( $args ); $posts = array(); foreach ( $query->posts as $post ) { $posts[] = $this->format_post_data( $post, $sort_params ); } return $posts; } /** * Format post data for response */ private function format_post_data( $post, $sort_params ) { $edit_url = get_edit_post_link( $post->ID, 'raw' ); // Build URL parameters $url_params = array(); if ( ! empty( $sort_params['orderby'] ) && 'menu_order' !== $sort_params['orderby'] ) { $url_params['orderby'] = $sort_params['orderby']; } if ( ! empty( $sort_params['order'] ) && 'ASC' !== $sort_params['order'] ) { $url_params['order'] = $sort_params['order']; } if ( ! empty( $sort_params['post_status'] ) && 'all' !== $sort_params['post_status'] ) { $url_params['post_status'] = $sort_params['post_status']; } // Add URL parameters and nonce if ( ! empty( $url_params ) ) { $edit_url = add_query_arg( $url_params, $edit_url ); } // Always add nonce $edit_url = add_query_arg( AI_Editor_Nonce_Manager::NAVIGATION_NONCE, AI_Editor_Nonce_Manager::create_nonce( AI_Editor_Nonce_Manager::NAVIGATION_ACTION ), $edit_url ); // Check if post is password protected $is_password_protected = ! empty( $post->post_password ); return array( 'id' => $post->ID, 'title' => get_the_title( $post ), 'edit_url' => $edit_url, 'status' => $post->post_status, 'date' => get_the_date( 'Y-m-d', $post ), 'password_protected' => $is_password_protected, ); } /** * Verify AJAX nonce */ private function verify_ajax_nonce() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This IS the nonce verification if ( ! isset( $_GET[ AI_Editor_Nonce_Manager::AJAX_NONCE ] ) ) { return false; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Getting nonce value to verify it $nonce = sanitize_text_field( wp_unslash( $_GET[ AI_Editor_Nonce_Manager::AJAX_NONCE ] ) ); return AI_Editor_Nonce_Manager::verify_nonce( $nonce, AI_Editor_Nonce_Manager::AJAX_ACTION ); } /** * Check if post type is allowed */ private function is_allowed_post_type( $post_type ) { $allowed_post_types = $this->get_allowed_post_types(); return in_array( $post_type, $allowed_post_types, true ); } /** * AJAX handler for getting posts (legacy - kept for compatibility) */ public function ajax_get_posts() { // Check nonce // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checking if nonce exists before verification if ( ! isset( $_GET[ AI_Editor_Nonce_Manager::AJAX_NONCE ] ) ) { wp_send_json_error( 'Invalid security token' ); return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is verified in the next line $nonce = sanitize_text_field( wp_unslash( $_GET[ AI_Editor_Nonce_Manager::AJAX_NONCE ] ) ); if ( ! AI_Editor_Nonce_Manager::verify_nonce( $nonce, AI_Editor_Nonce_Manager::AJAX_ACTION ) ) { wp_send_json_error( 'Invalid security token' ); return; } // Get post type - nonce already verified // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce already verified above $post_type = isset( $_GET['post_type'] ) ? sanitize_key( $_GET['post_type'] ) : 'post'; // Verify post type is allowed $allowed_post_types = $this->get_allowed_post_types(); if ( ! in_array( $post_type, $allowed_post_types, true ) ) { wp_send_json_error( 'Invalid post type' ); return; } // Get sort parameters $sort_params = $this->get_sort_parameters(); // Build orderby array with ID as secondary sort $orderby_array = $this->build_orderby_array( $sort_params['orderby'], $sort_params['order'] ); // Setup query args $args = array( 'post_type' => $post_type, 'post_status' => $this->get_post_statuses( $sort_params['post_status'] ), 'posts_per_page' => 100, // Limit for performance 'orderby' => $orderby_array, ); // Get posts $query = new WP_Query( $args ); $posts = array(); foreach ( $query->posts as $post ) { $posts[] = $this->format_post_data( $post, $sort_params ); } wp_send_json_success( $posts ); } /** * Get sort parameters from AJAX request * * @return array Sort parameters */ private function get_sort_parameters() { // Default values based on post type // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is checked in ajax_get_posts $post_type = isset( $_GET['post_type'] ) ? sanitize_key( $_GET['post_type'] ) : 'post'; // Set defaults based on post type if ( 'page' === $post_type ) { $default_orderby = 'menu_order'; $default_order = 'ASC'; } else { $default_orderby = 'date'; $default_order = 'DESC'; } $orderby = $default_orderby; $order = $default_order; $post_status = 'all'; $allowed_fields = array( 'menu_order', 'title', 'date', 'modified', 'author', 'ID' ); // Get parameters from request - nonce already verified in ajax_get_posts // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce already verified in ajax_get_posts if ( ! empty( $_GET['orderby'] ) && in_array( $_GET['orderby'], $allowed_fields, true ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce already verified in ajax_get_posts $orderby = sanitize_text_field( wp_unslash( $_GET['orderby'] ) ); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce already verified in ajax_get_posts if ( ! empty( $_GET['order'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce already verified in ajax_get_posts $order = strtoupper( sanitize_text_field( wp_unslash( $_GET['order'] ) ) ); // Validate order parameter if ( ! in_array( $order, array( 'ASC', 'DESC' ), true ) ) { $order = $default_order; } } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce already verified in ajax_get_posts if ( isset( $_GET['post_status'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce already verified in ajax_get_posts $post_status = sanitize_text_field( wp_unslash( $_GET['post_status'] ) ); } return array( 'orderby' => $orderby, 'order' => $order, 'post_status' => $post_status, ); } /** * Build orderby array with ID as secondary sort * * @param string $orderby Primary orderby field * @param string $order Sort order (ASC/DESC) * @return array Orderby array for WP_Query */ private function build_orderby_array( $orderby, $order ) { // Special handling for menu_order (Pages) if ( 'menu_order' === $orderby ) { return array( 'menu_order' => $order, 'title' => $order, 'ID' => $order ); } // For other fields, always add ID as secondary sort return array( $orderby => $order, 'ID' => $order ); } /** * Get post statuses array * * @param string $status Status parameter * @return array Post statuses */ private function get_post_statuses( $status ) { if ( 'all' === $status || empty( $status ) ) { return array( 'publish', 'draft', 'pending', 'private', 'future' ); } return array( $status ); } /** * Get allowed post types from options * * @return array Array of allowed post types. */ private function get_allowed_post_types() { $cache_key = 'ai_editor_allowed_post_types'; $allowed_post_types = get_transient( $cache_key ); if ( false !== $allowed_post_types ) { return $allowed_post_types; } $allowed_post_types = get_option( 'ai_editor_post_types', array( 'post', 'page' ) ); if ( ! is_array( $allowed_post_types ) ) { $allowed_post_types = array(); } set_transient( $cache_key, $allowed_post_types, HOUR_IN_SECONDS ); return $allowed_post_types; } /** * Get total status counts for a post type * * @param string $post_type The post type. * @return array Status counts including total. */ private function get_total_status_counts( $post_type ) { // Ensure we have a valid post type if ( empty( $post_type ) ) { $post_type = 'post'; } // Get the post type object to ensure it exists $post_type_obj = get_post_type_object( $post_type ); if ( ! $post_type_obj ) { return array( 'total' => 0, 'publish' => 0, 'draft' => 0, 'pending' => 0, 'private' => 0, 'future' => 0, ); } // Use WordPress built-in function to get post counts $count_posts = wp_count_posts( $post_type ); // Initialize counts array $counts = array( 'total' => 0, 'publish' => 0, 'draft' => 0, 'pending' => 0, 'private' => 0, 'future' => 0, ); // Map WordPress counts to our array if ( ! empty( $count_posts ) && is_object( $count_posts ) ) { // Convert object properties to array for easier handling $count_array = get_object_vars( $count_posts ); foreach ( array( 'publish', 'draft', 'pending', 'private', 'future' ) as $status ) { if ( isset( $count_array[ $status ] ) ) { $counts[ $status ] = intval( $count_array[ $status ] ); $counts['total'] += intval( $count_array[ $status ] ); } } } // If still no counts, try direct database query as fallback if ( $counts['total'] === 0 ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Fallback when wp_count_posts() fails $results = $wpdb->get_results( $wpdb->prepare( "SELECT post_status, COUNT(*) as count FROM {$wpdb->posts} WHERE post_type = %s AND post_status IN ('publish', 'draft', 'pending', 'private', 'future') GROUP BY post_status", $post_type ), ARRAY_A ); if ( ! empty( $results ) ) { foreach ( $results as $row ) { if ( isset( $counts[ $row['post_status'] ] ) ) { $counts[ $row['post_status'] ] = intval( $row['count'] ); $counts['total'] += intval( $row['count'] ); } } } } return $counts; } } // Initialize the class new AI_Editor_Posts_Search();