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();
}
}
}