plugin_name = $plugin_name; add_action('admin_post_ailm_handle_actions', [$this, 'route_csv_actions']); } public function route_csv_actions() { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'ailinkmaster-internal-auto-manager')); } check_admin_referer('ailm_form_nonce'); $submit_action = isset($_POST['submit_action']) ? sanitize_text_field(wp_unslash($_POST['submit_action'])) : ''; if ($submit_action === 'ailm_export_csv') { $this->export_csv(); } elseif ($submit_action === 'ailm_import_csv') { $this->import_csv(); } else { // Action is not related to CSV (e.g., saving settings). // Return control so class-ailm-settings-tab.php can handle it. return; } } private function export_csv() { global $wpdb; // 1. Get allowed post types from settings $post_types = get_option('ailm_allowed_post_types', ['post', 'page']); // 2. Get ALL published posts of these types $all_posts = get_posts([ 'post_type' => $post_types, 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids', 'orderby' => 'ID', 'order' => 'DESC' ]); header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename=ailm_universal_export_' . wp_date('Y-m-d') . '.csv'); $output = fopen('php://output', 'w'); fprintf($output, chr(0xEF) . chr(0xBB) . chr(0xBF)); // BOM for Excel // Add link_origin column to indicate the source of the link fputcsv($output, ['donor_id', 'donor_url', 'keyword', 'target_url', 'target_id', 'link_origin', 'action']); if (!empty($all_posts)) { $scout_table = $wpdb->prefix . 'ailm_link_monitor'; // Adjust table name if needed foreach ($all_posts as $donor_id) { $donor_url = get_permalink($donor_id); $has_links = false; // --- SOURCE 1: AILM Links (from meta fields) --- // DEPRECATED v2.0: _ailm_frozen_map meta is replaced by wp_ailm_link_rules. /* $frozen_map = get_post_meta( $donor_id, '_ailm_frozen_map', true ); if ( is_array( $frozen_map ) && ! empty( $frozen_map ) ) { foreach ( $frozen_map as $link ) { fputcsv( $output, [ $donor_id, $donor_url, $link['word'] ?? '', $link['url'] ?? '', $link['target_id'] ?? '', 'ailm', '' ] ); $has_links = true; } } */ // v2.0: Read from unified rules table. if (class_exists('AILM_Link_Repository')) { $rules = AILM_Link_Repository::get_rules_for_donor($donor_id); foreach ($rules as $rule) { fputcsv($output, [ $donor_id, $donor_url, $rule->anchor, $rule->target_url, $rule->target_id, 'ailm', '' ]); $has_links = true; } } // --- SOURCE 2: Manual links (from scanner table) --- // Check if the monitor table exists // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $scout_table))) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter $manual_links = $wpdb->get_results($wpdb->prepare( "SELECT url, anchor FROM {$scout_table} WHERE post_id = %d AND link_origin = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name built from $wpdb->prefix. $donor_id, 'manual' )); if (!empty($manual_links)) { foreach ($manual_links as $m_link) { fputcsv($output, [ $donor_id, $donor_url, $m_link->anchor, $m_link->url, url_to_postid($m_link->url), // Try to find target ID 'manual', // Origin '' ]); $has_links = true; } } } // --- SOURCE 3: Articles WITHOUT links --- // If nothing was found for the article, output one empty row so it appears in the list if (!$has_links) { fputcsv($output, [ $donor_id, $donor_url, '', // No keyword '', // No URL '', // No target ID 'none', '' ]); } } } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose fclose($output); exit; } private function import_csv() { $notice = 'csv_error'; // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in route_csv_actions() via check_admin_referer(). if (!empty($_FILES['ailm_csv_file']['tmp_name'])) { $file_path = sanitize_text_field($_FILES['ailm_csv_file']['tmp_name']); // phpcs:enable // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen if (($handle = fopen($file_path, "r")) !== FALSE) { fgetcsv($handle, 1000, ","); // Skip headers $updates = []; // Read CSV while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) { if (count($data) < 5) continue; $donor_id = intval($data[0]); $keyword = sanitize_text_field($data[2]); $target_url = esc_url_raw($data[3]); $target_id = intval($data[4]); $origin = isset($data[5]) ? sanitize_text_field($data[5]) : 'ailm'; $action = isset($data[6]) ? strtolower(sanitize_text_field($data[6])) : ''; // CRITICAL CHANGE: // Skip manual links and empty rows (none). // The plugin must not touch what was placed manually in the text. if (empty($donor_id) || empty($keyword) || empty($target_url) || $origin === 'manual' || $origin === 'none') { continue; } if (!isset($updates[$donor_id])) { $updates[$donor_id] = ['keep' => []]; } if ($action !== 'delete') { // Build array for AILM links only $updates[$donor_id]['keep'][] = [ 'word' => $keyword, 'url' => $target_url, 'target_id' => $target_id, 'occurrence' => 1, 'creation_type' => $origin ]; } } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose fclose($handle); // Apply changes to the database // DEPRECATED v2.0: _ailm_frozen_map meta is replaced by wp_ailm_link_rules. foreach ($updates as $donor_id => $instructions) { // Overwrite rules in the unified table foreach ($instructions['keep'] as $new_link) { if (class_exists('AILM_Link_Repository')) { // in_html=0: CSV import defines rules to be injected; // the injector will insert the and mark in_html=1. AILM_Link_Repository::register_link( $donor_id, esc_url_raw($new_link['url']), sanitize_text_field($new_link['word']), sanitize_text_field($new_link['creation_type']), 1, false ); } } /* $final_map = []; foreach ( $instructions['keep'] as $new_link ) { $final_map[] = $new_link; } if ( empty( $final_map ) ) { delete_post_meta( $donor_id, '_ailm_frozen_map' ); } else { update_post_meta( $donor_id, '_ailm_frozen_map', $final_map ); } */ // Re-scan with 404 Scout if (class_exists('AILM_Scout_Scanner')) { AILM_Scout_Scanner::process_post($donor_id); } } $notice = 'csv_sync_success'; } } wp_safe_redirect(admin_url('admin.php?page=' . $this->plugin_name . '&tab=settings&ailm_notice=' . $notice)); exit; } }