plugin = $plugin; } /** * Registers all WordPress AJAX action hooks. */ public function register_ajax_hooks() { // Fetch actions add_action( 'wp_ajax_ainp_fetch_next_news_item', array( $this, 'handle_fetch_next_news_item_ajax' ) ); add_action( 'wp_ajax_ainp_start_batch_search', array( $this, 'handle_start_batch_search_ajax' ) ); add_action( 'wp_ajax_ainp_cancel_search', array( $this, 'handle_cancel_search_ajax' ) ); // Uses maintenance nonce // Status and Log polling add_action( 'wp_ajax_ainp_get_status', array( $this, 'handle_get_status_ajax' ) ); add_action( 'wp_ajax_ainp_get_logs', array( $this, 'handle_get_logs_ajax' ) ); // Diagnostics / Testing add_action( 'wp_ajax_ainp_test_api_connection', array( $this, 'handle_test_api_connection_ajax' ) ); add_action( 'wp_ajax_ainp_test_write_permission', array( $this, 'handle_test_write_permission_ajax' ) ); // Dashboard Toggles add_action( 'wp_ajax_ainp_toggle_auto_search', array( $this, 'handle_toggle_auto_search_ajax' ) ); add_action( 'wp_ajax_ainp_toggle_image_fallback', array( $this, 'handle_toggle_image_fallback_ajax' ) ); add_action( 'wp_ajax_ainp_toggle_force_image', array( $this, 'handle_toggle_force_image_ajax' ) ); // Maintenance Actions (use maintenance nonce) add_action( 'wp_ajax_ainp_reset_jobs', array( $this, 'handle_reset_jobs_ajax' ) ); add_action( 'wp_ajax_ainp_clear_logs', array( $this, 'handle_clear_logs_ajax' ) ); add_action( 'wp_ajax_ainp_reset_processing_status', array( $this, 'handle_reset_processing_status_ajax' ) ); // Dismiss Welcome Panel add_action( 'wp_ajax_ainp_dismiss_welcome_panel', array( $this, 'handle_dismiss_welcome_panel_ajax' ) ); } /** * Verifies AJAX request nonce and user capabilities. Terminates with JSON error on failure. * * @param string $nonce_action The expected nonce action name. Default 'ainp_nonce'. * @param string $nonce_key The key used for the nonce in the POST data. Default '_ajax_nonce'. */ private function verify_ajax_request( $nonce_action = 'ainp_nonce', $nonce_key = '_ajax_nonce' ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- This is the verification function itself. if ( '_ajax_nonce' !== $nonce_key && isset( $_POST[ $nonce_key ] ) ) { // A specific key was passed, use it. // phpcs:ignore WordPress.Security.NonceVerification.Missing -- This is the verification function itself. } elseif ( isset( $_POST['_ajax_nonce'] ) ) { $nonce_key = '_ajax_nonce'; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- This is the verification function itself. } elseif ( isset( $_POST['nonce'] ) ) { $nonce_key = 'nonce'; } else { $nonce_key = '_ajax_nonce'; } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- We are verifying it right here. $nonce = isset( $_POST[ $nonce_key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $nonce_key ] ) ) : ''; if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, $nonce_action ) ) { wp_send_json_error( array( 'message' => __( 'Security check (Nonce) failed.', 'ainp-ai-native-publisher' ) ), 403 ); wp_die(); } if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', 'ainp-ai-native-publisher' ) ), 403 ); wp_die(); } } /** * Handles AJAX request to start a batch search process. */ public function handle_start_batch_search_ajax() { $this->verify_ajax_request( 'ainp_nonce' ); set_transient( 'ainp_manual_context', true, HOUR_IN_SECONDS ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified in verify_ajax_request. $choice = isset( $_POST['search_depth_choice'] ) ? sanitize_key( wp_unslash( $_POST['search_depth_choice'] ) ) : '1'; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified in verify_ajax_request. $custom_val = isset( $_POST['search_depth_custom'] ) ? absint( wp_unslash( $_POST['search_depth_custom'] ) ) : 5; $this->plugin->load_options(); $this->plugin->options['manual_search_depth_choice'] = $choice; $this->plugin->options['manual_search_depth_custom'] = $custom_val; update_option( 'ainp_settings', $this->plugin->options ); $search_depth = ( 'custom' === $choice ) ? max( 1, $custom_val ) : $choice; $this->plugin->logger->add_log_entry( 'info', __( 'Received AJAX request for manual batch search.', 'ainp-ai-native-publisher' ), 'Search depth setting: ' . esc_html( $search_depth ) ); delete_option( AINP_Status_Logger::CANCEL_FLAG_OPTION ); update_option( AINP_EXECUTION_START_TIME_OPTION, time() ); update_option( AINP_PENDING_IMAGE_TASKS_OPTION, 0 ); $this->plugin->processor->set_posts_awaiting_image_current_run( array() ); $sources = $this->plugin->options['sources_config'] ?? array(); if ( empty( $sources ) ) { wp_send_json_error( array( 'message' => __( 'Cannot start search: No valid sources configured.', 'ainp-ai-native-publisher' ) ) ); wp_die(); } $queue_data = array( 'sources' => $sources, 'search_depth' => $search_depth, ); set_transient( AINP_FEED_QUEUE_TRANSIENT, $queue_data, HOUR_IN_SECONDS * 2 ); $total_sources = count( $sources ); $this->plugin->logger->update_processing_status( 'processing', 0, $total_sources, __( 'Starting manual batch search...', 'ainp-ai-native-publisher' ) ); /* translators: %d: Number of sources */ $this->plugin->logger->add_log_entry( 'info', sprintf( __( 'Manual batch search initiated for %d sources.', 'ainp-ai-native-publisher' ), $total_sources ) ); wp_schedule_single_event( time(), AINP_PROCESS_BATCH_HOOK ); wp_send_json_success( array( 'message' => __( 'Background batch search initiated successfully!', 'ainp-ai-native-publisher' ), 'total_feeds' => $total_sources, ) ); wp_die(); } /** * Handles AJAX request to fetch and process only the next single news item. */ public function handle_fetch_next_news_item_ajax() { $this->verify_ajax_request( 'ainp_nonce' ); $this->plugin->logger->add_log_entry( 'info', __( 'Received AJAX request to fetch the next single news item.', 'ainp-ai-native-publisher' ) ); $this->plugin->load_options(); delete_option( AINP_Status_Logger::CANCEL_FLAG_OPTION ); $sources = $this->plugin->options['sources_config'] ?? array(); if ( empty( $sources ) ) { wp_send_json_error( array( 'message' => __( 'Cannot fetch: No sources configured.', 'ainp-ai-native-publisher' ) ) ); wp_die(); } $result_summary = $this->plugin->processor->process_single_source( $sources[0], 1, true ); $message = sprintf( /* translators: 1: created count, 2: skipped count, 3: error count */ __( 'Processing of the first source complete. %1$d news item(s) created, %2$d skipped (duplicates/filtered), %3$d encountered errors.', 'ainp-ai-native-publisher' ), $result_summary['created'], $result_summary['skipped'], $result_summary['errors'] ); $this->plugin->logger->add_log_entry( 'success', __( 'Single item fetch complete.', 'ainp-ai-native-publisher' ), $message ); $final_status_msg = ( $result_summary['errors'] > 0 ) ? __( 'Completed with errors.', 'ainp-ai-native-publisher' ) : __( 'Completed successfully.', 'ainp-ai-native-publisher' ); $this->plugin->logger->update_processing_status( 'completed', $result_summary['created'], 1, $final_status_msg ); wp_send_json_success( array( 'message' => $message ) ); wp_die(); } /** * Handles AJAX request to cancel the currently running batch search. */ public function handle_cancel_search_ajax() { $this->verify_ajax_request( 'ainp_maintenance_nonce' ); update_option( AINP_Status_Logger::CANCEL_FLAG_OPTION, true ); delete_transient( AINP_FEED_QUEUE_TRANSIENT ); delete_option( AINP_PENDING_IMAGE_TASKS_OPTION ); $this->plugin->logger->add_log_entry( 'warning', __( 'Search cancellation requested by user.', 'ainp-ai-native-publisher' ) ); $status = $this->plugin->logger->get_processing_status(); $this->plugin->logger->update_processing_status( 'idle', $status['current'], $status['total'], __( 'Search cancelled by user.', 'ainp-ai-native-publisher' ) ); wp_send_json_success( array( 'message' => __( 'Search cancellation initiated. The process will stop shortly.', 'ainp-ai-native-publisher' ) ) ); wp_die(); } /** * Handles AJAX polling request to get the current processing status. */ public function handle_get_status_ajax() { $this->verify_ajax_request( 'ainp_nonce' ); $status_data = $this->plugin->logger->get_processing_status(); $pending_images = (int) get_option( AINP_PENDING_IMAGE_TASKS_OPTION, 0 ); if ( 'completed' === $status_data['status'] && $pending_images > 0 ) { $status_data['status'] = 'processing_images'; /* translators: %d: Number of images remaining */ $status_data['message'] = sprintf( _n( 'Fetch complete. Generating %d image...', 'Fetch complete. Generating %d images...', $pending_images, 'ainp-ai-native-publisher' ), $pending_images ); } $status_display_map = array( 'idle' => __( 'Idle', 'ainp-ai-native-publisher' ), 'processing' => __( 'Processing Sources...', 'ainp-ai-native-publisher' ), 'processing_images' => __( 'Generating Images...', 'ainp-ai-native-publisher' ), 'completed' => __( 'Completed', 'ainp-ai-native-publisher' ), ); $status_data['status_display'] = $status_display_map[ $status_data['status'] ] ?? ucfirst( $status_data['status'] ); wp_send_json_success( array( 'status' => $status_data ) ); wp_die(); } /** * Handles AJAX polling request to get the latest log entries HTML. */ public function handle_get_logs_ajax() { $this->verify_ajax_request( 'ainp_nonce' ); wp_send_json_success( array( 'logs' => $this->plugin->logger->get_log_entries_html() ) ); wp_die(); } /** * Handles AJAX request to test connections to configured AI APIs. */ public function handle_test_api_connection_ajax() { $this->verify_ajax_request( 'ainp_nonce' ); $this->plugin->logger->add_log_entry( 'info', __( 'Starting API connection tests via AJAX.', 'ainp-ai-native-publisher' ) ); $results = array(); // --- Test Groq (Active) --- $groq_results = $this->plugin->api_handler->test_groq_connection(); if ( isset( $groq_results['error'] ) ) { $results['groq'] = array( 'provider' => 'Groq (Text)', 'status' => 'error', 'message' => $groq_results['error'], 'details' => array(), ); $this->plugin->logger->add_log_entry( 'warning', 'Groq API test skipped: ' . $groq_results['error'] ); } else { $results['groq'] = array( 'provider' => 'Groq (Text)', 'status' => $groq_results['status'], 'message' => $groq_results['message'], 'details' => array(), ); $log_level_groq = ( 'error' === $groq_results['status'] ) ? 'error' : 'info'; $this->plugin->logger->add_log_entry( $log_level_groq, 'Groq API Test Result: ' . wp_strip_all_tags( $groq_results['message'] ) ); } // --- Test Unsplash (Active) --- $unsplash_results = $this->plugin->api_handler->test_unsplash_connection(); if ( isset( $unsplash_results['error'] ) ) { $results['unsplash'] = array( 'provider' => 'Unsplash (Images)', 'status' => 'error', 'message' => $unsplash_results['error'], 'details' => array(), ); $this->plugin->logger->add_log_entry( 'warning', 'Unsplash API test skipped: ' . $unsplash_results['error'] ); } else { $results['unsplash'] = array( 'provider' => 'Unsplash (Images)', 'status' => $unsplash_results['status'], 'message' => $unsplash_results['message'], 'details' => array(), ); $log_level_unsplash = ( 'error' === $unsplash_results['status'] ) ? 'error' : 'info'; $this->plugin->logger->add_log_entry( $log_level_unsplash, 'Unsplash API Test Result: ' . wp_strip_all_tags( $unsplash_results['message'] ) ); } wp_send_json_success( array( 'results' => $results ) ); wp_die(); } /** * Generic handler for dashboard toggle switches. */ private function handle_toggle_ajax( $option_key, $log_msg_enabled, $log_msg_disabled ) { $this->verify_ajax_request( 'ainp_nonce' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified in verify_ajax_request. if ( ! isset( $_POST['is_enabled'] ) ) { wp_send_json_error( array( 'message' => __( 'Missing required parameter.', 'ainp-ai-native-publisher' ) ) ); wp_die(); } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified in verify_ajax_request. $is_enabled_raw = sanitize_text_field( wp_unslash( $_POST['is_enabled'] ) ); $is_enabled = filter_var( $is_enabled_raw, FILTER_VALIDATE_BOOLEAN ); $this->plugin->load_options(); $this->plugin->options[ $option_key ] = $is_enabled; update_option( 'ainp_settings', $this->plugin->options ); if ( 'enable_auto_search' === $option_key ) { $this->plugin->cron_handler->schedule_or_clear_auto_search(); } $message = $is_enabled ? $log_msg_enabled : $log_msg_disabled; $this->plugin->logger->add_log_entry( 'info', $message ); wp_send_json_success( array( 'message' => $message ) ); wp_die(); } public function handle_toggle_auto_search_ajax() { $this->handle_toggle_ajax( 'enable_auto_search', __( 'Automatic search enabled via dashboard toggle.', 'ainp-ai-native-publisher' ), __( 'Automatic search disabled via dashboard toggle.', 'ainp-ai-native-publisher' ) ); } public function handle_toggle_image_fallback_ajax() { $this->handle_toggle_ajax( 'module_generate_images', __( 'AI image generation fallback ENABLED via dashboard toggle.', 'ainp-ai-native-publisher' ), __( 'AI image generation fallback DISABLED via dashboard toggle.', 'ainp-ai-native-publisher' ) ); } public function handle_toggle_force_image_ajax() { $this->handle_toggle_ajax( 'module_force_ia_image', __( 'Force AI image generation ENABLED via dashboard toggle.', 'ainp-ai-native-publisher' ), __( 'Force AI image generation DISABLED via dashboard toggle.', 'ainp-ai-native-publisher' ) ); } /** * Handles AJAX request to reset currently running jobs. */ public function handle_reset_jobs_ajax() { $this->verify_ajax_request( 'ainp_maintenance_nonce' ); $this->plugin->logger->add_log_entry( 'warning', __( '"Reset Jobs" action triggered by user.', 'ainp-ai-native-publisher' ) ); update_option( AINP_Status_Logger::CANCEL_FLAG_OPTION, true ); delete_transient( AINP_FEED_QUEUE_TRANSIENT ); delete_option( AINP_PENDING_IMAGE_TASKS_OPTION ); delete_option( AINP_DOMAIN_TIMESTAMPS_OPTION ); $this->plugin->logger->add_log_entry( 'info', __( 'Domain cooldown timestamps have been reset.', 'ainp-ai-native-publisher' ) ); $this->plugin->logger->update_processing_status( 'idle', 0, 0, __( 'Running jobs reset by user.', 'ainp-ai-native-publisher' ) ); wp_send_json_success( array( 'message' => __( 'Running jobs have been flagged to stop. Status and domain cooldowns reset.', 'ainp-ai-native-publisher' ) ) ); wp_die(); } /** * Handles AJAX request to clear all plugin log entries. */ public function handle_clear_logs_ajax() { $this->verify_ajax_request( 'ainp_maintenance_nonce' ); delete_option( AINP_Status_Logger::LOG_OPTION_KEY ); $this->plugin->logger->add_log_entry( 'warning', __( 'Log history cleared by user.', 'ainp-ai-native-publisher' ) ); wp_send_json_success( array( 'message' => __( 'Log history has been cleared.', 'ainp-ai-native-publisher' ) ) ); wp_die(); } /** * Handles AJAX request to force reset the processing status to 'idle'. */ public function handle_reset_processing_status_ajax() { $this->verify_ajax_request( 'ainp_maintenance_nonce' ); $this->plugin->logger->add_log_entry( 'info', __( 'Processing status and image queue manually reset by user.', 'ainp-ai-native-publisher' ) ); delete_transient( AINP_FEED_QUEUE_TRANSIENT ); delete_option( AINP_PENDING_IMAGE_TASKS_OPTION ); delete_option( AINP_Status_Logger::CANCEL_FLAG_OPTION ); delete_option( AINP_DOMAIN_TIMESTAMPS_OPTION ); $this->plugin->logger->add_log_entry( 'info', __( 'Domain cooldown timestamps have been reset.', 'ainp-ai-native-publisher' ) ); $this->plugin->logger->update_processing_status( 'idle', 0, 0, __( 'Processing status manually reset.', 'ainp-ai-native-publisher' ) ); wp_send_json_success( array( 'message' => __( 'Processing status, related queues, and domain cooldowns have been reset to Idle.', 'ainp-ai-native-publisher' ) ) ); wp_die(); } /** * Handles AJAX request to test write permissions in the WordPress uploads directory. */ public function handle_test_write_permission_ajax() { $this->verify_ajax_request( 'ainp_nonce' ); $upload_dir = wp_upload_dir(); $test_file = trailingslashit( $upload_dir['path'] ) . 'ainp-write-test.tmp'; if ( file_put_contents( $test_file, 'test_write' ) !== false ) { if ( file_exists( $test_file ) ) { wp_delete_file( $test_file ); } wp_send_json_success( array( 'message' => __( 'SUCCESS! The plugin has write permission in the uploads folder.', 'ainp-ai-native-publisher' ) ) ); } else { $error = error_get_last(); $message = sprintf( /* translators: 1: Uploads folder path, 2: System error message (or N/A) */ __( 'FAIL: The plugin could not write to the uploads folder (%1$s). Please check the folder permissions on your server. System error: %2$s', 'ainp-ai-native-publisher' ), esc_html( $upload_dir['path'] ), esc_html( $error['message'] ?? __( 'N/A', 'ainp-ai-native-publisher' ) ) ); $this->plugin->logger->add_log_entry( 'error', __( 'Uploads directory write permission test failed.', 'ainp-ai-native-publisher' ), esc_html( $upload_dir['path'] ) ); wp_send_json_error( array( 'message' => $message ) ); } wp_die(); } /** * Handles AJAX request to save the user's preference for dismissing the welcome panel. */ public function handle_dismiss_welcome_panel_ajax() { $this->verify_ajax_request( 'ainp_nonce' ); $user_id = get_current_user_id(); if ( $user_id ) { $result = update_user_meta( $user_id, 'ainp_hide_welcome_panel', true ); if ( false === $result ) { $this->plugin->logger->add_log_entry( 'error', 'Failed to update user meta for dismissing welcome panel.', 'User ID: ' . $user_id ); wp_send_json_error( array( 'message' => 'Failed to save preference.' ) ); } elseif ( is_numeric( $result ) ) { $this->plugin->logger->add_log_entry( 'info', 'Successfully updated user meta for dismissing welcome panel.', 'User ID: ' . $user_id ); wp_send_json_success(); } else { $this->plugin->logger->add_log_entry( 'info', 'User meta for dismissing welcome panel was already set.', 'User ID: ' . $user_id ); wp_send_json_success(); } } else { $this->plugin->logger->add_log_entry( 'error', 'Could not get user ID in dismiss welcome panel AJAX handler.' ); wp_send_json_error( array( 'message' => 'Could not identify user.' ) ); } wp_die(); } } // End Class AINP_Ajax