post_content; $home_url = untrailingslashit(home_url()); // Media file pattern — skip HTTP checks for known media URLs. $media_pattern = '/\.(jpg|jpeg|png|gif|webp|svg|pdf|zip|rar|mp4|webm)$/i'; // Content length for relative position calculation. $content_length = strlen($content); if ($content_length < 1) { $content_length = 1; } // Pre-load active rules for this donor to match rule_id. $rules = array(); $rules_map = array(); if (class_exists('AILM_Link_DB')) { $rules_table = AILM_Link_DB::get_rules_table(); // 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_url, anchor, source FROM {$rules_table} WHERE donor_id = %d AND status = 'active'", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $post_id ) ); // Build a lookup map: normalized_url|lowercase_anchor => [rule_id, source] foreach ($rules as $rule) { $key = untrailingslashit(strtok($rule->target_url, '?')) . '|' . mb_strtolower($rule->anchor, 'UTF-8'); $rules_map[$key] = array( 'rule_id' => (int) $rule->id, 'source' => $rule->source, ); } } // ===================================================================== // PARSE ALL LINKS FROM HTML // ===================================================================== $links_new = array(); // For wp_ailm_links (v2) $links_legacy = array(); // For wp_ailm_link_monitor (v1 backward compat) if (preg_match_all('/]*)href=[\'\"]([^\'\"\s]+)[\'\"]([^>]*)>(.*?)<\/a>/is', $content, $matches, PREG_OFFSET_CAPTURE)) { foreach ($matches[2] as $index => $match_data) { $url = trim(html_entity_decode($match_data[0])); $byte_pos = (int) $match_data[1]; // Byte offset of the href URL in content. if (empty($url) || strpos($url, 'http') !== 0) { continue; } $attr_before = $matches[1][$index][0]; $attr_after = $matches[3][$index][0]; $anchor = wp_strip_all_tags($matches[4][$index][0]); // Skip image links and other non-text anchors (e.g. ). if ('' === trim($anchor)) { continue; } // 1. Determine link type: internal / external / sponsored. $is_sponsored = (stripos($attr_before, 'sponsored') !== false || stripos($attr_after, 'sponsored') !== false); $is_internal = (strpos($url, $home_url) === 0); if ($is_sponsored) { $link_type = 'sponsored'; } elseif ($is_internal) { $link_type = 'internal'; } else { $link_type = 'external'; } // 2. Determine origin: 'plugin' or 'manual'. // Any link with a data-ailm attribute was created by the plugin. $has_ailm_marker = (stripos($attr_before, 'data-ailm') !== false || stripos($attr_after, 'data-ailm') !== false); $origin = $has_ailm_marker ? 'plugin' : 'manual'; // Legacy origin mapping for old table compatibility. $legacy_origin = $has_ailm_marker ? 'ailm' : 'manual'; // 3. Resolve target post ID for internal links. $target_id = 0; if ('internal' === $link_type) { $target_id = url_to_postid($url); } // 4. Calculate relative position in content (0.0 to 1.0). $position = $byte_pos / $content_length; $position = max(0.0, min(1.0, $position)); // 5. Match against active rules to set rule_id. $rule_id = null; $rule_source = ''; $clean_url = untrailingslashit(strtok($url, '?')); $rule_key = $clean_url . '|' . mb_strtolower(trim($anchor), 'UTF-8'); if (isset($rules_map[$rule_key])) { $rule_id = $rules_map[$rule_key]['rule_id']; $rule_source = $rules_map[$rule_key]['source']; // If the link matches a rule, determine origin from the // rule's source. Rules with source 'pre_existing' are // manual links registered by the sync tool — they should // keep origin 'manual'. All other sources ('ai', // 'orphan_rescue', 'ki_manual', etc.) are plugin-injected. if ($rule_source === 'pre_existing') { $origin = 'manual'; $legacy_origin = 'manual'; } else { $origin = 'plugin'; $legacy_origin = 'ailm'; } } // 6. Media file detection — auto-set status to 200. $is_media = preg_match($media_pattern, $url); $status_code = $is_media ? 200 : 0; $last_checked = $is_media ? current_time('mysql') : '0000-00-00 00:00:00'; // Deduplicate by URL + anchor hash. $hash = md5($url . $anchor); // New table row (wp_ailm_links). $links_new[$hash] = array( 'donor_id' => $post_id, 'target_id' => $target_id, 'url' => esc_url_raw($url), 'anchor' => sanitize_text_field($anchor), 'origin' => $origin, 'link_type' => $link_type, 'position' => round($position, 4), 'status_code' => $status_code, 'last_checked' => $last_checked, 'rule_id' => $rule_id, ); // Legacy table row (wp_ailm_link_monitor). $links_legacy[$hash] = array( 'post_id' => $post_id, 'url' => esc_url_raw($url), 'anchor' => sanitize_text_field($anchor), 'link_origin' => $legacy_origin, 'link_type' => $link_type, 'status_code' => $status_code, 'last_checked' => $last_checked, ); } } // ===================================================================== // ADD FROZEN MAP LINKS (plugin-injected links not yet in HTML) // DEPRECATED v2.0: _ailm_frozen_map meta is replaced by wp_ailm_link_rules. // This block is kept commented for audit purposes and will be removed // once full migration is confirmed stable. // ===================================================================== /* $frozen_map = get_post_meta( $post_id, '_ailm_frozen_map', true ); if ( is_array( $frozen_map ) && ! empty( $frozen_map ) ) { $link_index = 0; foreach ( $frozen_map as $item ) { if ( empty( $item['url'] ) ) { continue; } $link_index++; $hash = md5( $item['url'] . $item['word'] . 'ailm' . $link_index ); // Only add to legacy if not already found in HTML (avoid duplicates). if ( ! isset( $links_legacy[ $hash ] ) ) { $links_legacy[ $hash ] = array( 'post_id' => $post_id, 'url' => esc_url_raw( $item['url'] ), 'anchor' => sanitize_text_field( $item['word'] ), 'link_origin' => 'ailm', 'link_type' => 'internal', 'status_code' => 0, 'last_checked' => '0000-00-00 00:00:00', ); } // For new table, check if it wasn't already captured from HTML. $clean_url = untrailingslashit( strtok( $item['url'], '?' ) ); $anchor_lc = mb_strtolower( sanitize_text_field( $item['word'] ), 'UTF-8' ); $already_captured = false; foreach ( $links_new as $existing ) { $ex_clean = untrailingslashit( strtok( $existing['url'], '?' ) ); if ( $ex_clean === $clean_url ) { $already_captured = true; break; } } if ( ! $already_captured ) { $target_id = url_to_postid( $item['url'] ); $rule_key = $clean_url . '|' . $anchor_lc; $rule_id = isset( $rules_map[ $rule_key ] ) ? $rules_map[ $rule_key ] : null; $links_new[ $hash ] = array( 'donor_id' => $post_id, 'target_id' => $target_id ? $target_id : 0, 'url' => esc_url_raw( $item['url'] ), 'anchor' => sanitize_text_field( $item['word'] ), 'origin' => 'plugin', 'link_type' => 'internal', 'position' => 0.5, 'status_code' => 0, 'last_checked' => '0000-00-00 00:00:00', 'rule_id' => $rule_id, ); } } } */ // ===================================================================== // WRITE TO DATABASES // ===================================================================== // 1. Legacy table (wp_ailm_link_monitor) — backward compatibility. $legacy_table = AILM_Scout_DB::get_table_name(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->delete($legacy_table, array('post_id' => $post_id), array('%d')); if (!empty($links_legacy)) { foreach ($links_legacy as $data) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->insert( $legacy_table, $data, array('%d', '%s', '%s', '%s', '%s', '%d', '%s') ); } } // 2. New unified table (wp_ailm_links) — v2 architecture. if (class_exists('AILM_Link_DB')) { $new_table = AILM_Link_DB::get_links_table(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->delete($new_table, array('donor_id' => $post_id), array('%d')); if (!empty($links_new)) { foreach ($links_new as $data) { // Build format array dynamically to handle NULL rule_id. $formats = array('%d', '%d', '%s', '%s', '%s', '%s', '%f', '%d', '%s'); if (null === $data['rule_id']) { unset($data['rule_id']); } else { $formats[] = '%d'; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->insert($new_table, $data, $formats); } } } // Invalidate dashboard caches after scan. if (class_exists('AILM_Link_Repository')) { AILM_Link_Repository::invalidate_cache(); } } }