0, 'confirmed' => 0, 'failed' => 0, 'deleted' => 0, ); // Recursion guard. if (self::$running) { return $result; } self::$running = true; try { $result = self::do_reconcile($post_id); } finally { self::$running = false; } return $result; } /** * Internal reconciliation logic. * * @param int $post_id Donor post ID. * @return array Result counts. */ private static function do_reconcile($post_id) { global $wpdb; if (!class_exists('AILM_Link_DB') || !class_exists('AILM_Link_Repository')) { return array('created' => 0, 'confirmed' => 0, 'failed' => 0, 'deleted' => 0); } $rules_table = AILM_Link_DB::get_rules_table(); $links_table = AILM_Link_DB::get_links_table(); // ----------------------------------------------------------------- // 1. Load reality: internal links found by the scanner in this post. // ----------------------------------------------------------------- // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $real_links = $wpdb->get_results( $wpdb->prepare( "SELECT target_id, url, anchor, rule_id FROM {$links_table} WHERE donor_id = %d AND link_type = 'internal'", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $post_id ) ); // Build a lookup: normalized_url|lowercase_anchor => real link data. $real_map = array(); $touched_targets = array(); foreach ($real_links as $rl) { $clean_url = untrailingslashit(strtok($rl->url, '?')); $key = $clean_url . '|' . mb_strtolower(trim($rl->anchor), 'UTF-8'); $real_map[$key] = $rl; if ((int) $rl->target_id > 0) { $touched_targets[(int) $rl->target_id] = true; } } // ----------------------------------------------------------------- // 2. Load intent: active rules for this donor. // ----------------------------------------------------------------- // We query ALL active rules (not just in_html=0) because we need to // verify that in_html=1 rules still have their link in HTML. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $rules = $wpdb->get_results( $wpdb->prepare( "SELECT id, target_id, target_url, anchor, source, status, in_html FROM {$rules_table} WHERE donor_id = %d AND status = 'active'", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $post_id ) ); $created = 0; $confirmed = 0; $failed = 0; $deleted = 0; // Track which real links were matched to an existing rule. $matched_real_keys = array(); // ----------------------------------------------------------------- // 3. For each active rule, check if its link is in HTML. // ----------------------------------------------------------------- foreach ($rules as $rule) { $clean_url = untrailingslashit(strtok($rule->target_url, '?')); $key = $clean_url . '|' . mb_strtolower(trim($rule->anchor), 'UTF-8'); $in_html_now = isset($real_map[$key]); if ($in_html_now) { $matched_real_keys[$key] = true; // The link IS in HTML. Ensure in_html=1 so the injector skips it. if (!isset($rule->in_html) || (int) $rule->in_html !== 1) { AILM_Link_Repository::mark_in_html((int) $rule->id, true); $confirmed++; } } else { // The link is NOT in HTML anymore. if (isset($rule->in_html) && (int) $rule->in_html === 1) { // Was in HTML, now gone. Someone removed it. $source = $rule->source; if (in_array($source, array('manual', 'pre_existing', 'editor_manual'), true)) { // Manual / imported link removed by user → delete the rule. AILM_Link_Repository::delete_rule((int) $rule->id); $deleted++; } else { // Plugin-managed link removed (ai, ki_manual, orphan_rescue, etc.) // → mark failed so the injector can retry on next pass. AILM_Link_Repository::update_rule_status((int) $rule->id, 'failed'); AILM_Link_Repository::mark_in_html((int) $rule->id, false); $failed++; } } // If in_html was already 0 (pending injection) and the link is // not in HTML, that's expected — the injector hasn't run yet. // Leave it alone. } } // ----------------------------------------------------------------- // 4. For each real link NOT matched to a rule → register as manual. // ----------------------------------------------------------------- foreach ($real_map as $key => $rl) { if (isset($matched_real_keys[$key])) { continue; // Already has a rule. } $target_id = (int) $rl->target_id; if ($target_id <= 0) { continue; // Couldn't resolve to a post (external or unresolved). } // Register as a manual link already present in HTML. $target_url = get_permalink($target_id); if (!$target_url) { continue; } AILM_Link_Repository::register_link( $post_id, $target_url, $rl->anchor, 'manual', 1, true // in_html = true ); $created++; $touched_targets[$target_id] = true; } // ----------------------------------------------------------------- // 5. Recalculate _ailm_inbound_count for all touched targets. // ----------------------------------------------------------------- foreach (array_keys($touched_targets) as $target_id) { $count = AILM_Link_Repository::get_active_inbound_rule_count($target_id); update_post_meta($target_id, '_ailm_inbound_count', $count); } // Invalidate caches. if (method_exists('AILM_Link_Repository', 'invalidate_cache')) { AILM_Link_Repository::invalidate_cache(); } return array( 'created' => $created, 'confirmed' => $confirmed, 'failed' => $failed, 'deleted' => $deleted, ); } }