PDO::ERRMODE_EXCEPTION, ]); echo "=== Fix Joomla Internal Links ===\n"; echo $dry_run ? "[DRY RUN - no changes will be saved]\n\n" : "[LIVE RUN - changes will be saved]\n\n"; // ------------------------------------------------------------------------- // Step 1: Build lookup maps from wp_postmeta // ------------------------------------------------------------------------- echo "Building lookup maps from wp_postmeta...\n"; // Map: K2 item ID → WP post_name $k2_map = []; $stmt = $pdo->query(" SELECT pm.meta_value AS k2_id, p.post_name FROM wp_postmeta pm JOIN wp_posts p ON pm.post_id = p.ID WHERE pm.meta_key = '_fgj2wp_old_k2_id' AND p.post_status IN ('publish', 'draft') AND p.post_type = 'post' AND p.post_name != '' "); foreach ($stmt as $row) { $k2_map[(int)$row['k2_id']] = $row['post_name']; } echo " K2 map: " . count($k2_map) . " entries\n"; // Map: jos_content ID → WP post_name $joomla_map = []; $stmt = $pdo->query(" SELECT pm.meta_value AS joomla_id, p.post_name FROM wp_postmeta pm JOIN wp_posts p ON pm.post_id = p.ID WHERE pm.meta_key = '_fgj2wp_old_id' AND p.post_status IN ('publish', 'draft') AND p.post_type = 'post' AND p.post_name != '' "); foreach ($stmt as $row) { $joomla_map[(int)$row['joomla_id']] = $row['post_name']; } echo " jos_content map: " . count($joomla_map) . " entries\n\n"; // ------------------------------------------------------------------------- // Step 2: Fetch posts with Joomla links // ------------------------------------------------------------------------- $stmt = $pdo->query(" SELECT ID, post_title, post_content FROM wp_posts WHERE post_type = 'post' AND post_status IN ('publish', 'draft') AND ( post_content LIKE '%index.php?option=%' OR post_content LIKE '%\"es/%' OR post_content LIKE '%/es/%' OR post_content LIKE '%feadulta.com%' OR post_content LIKE '%farmer.taild3aaf6%' ) "); $posts = $stmt->fetchAll(PDO::FETCH_ASSOC); echo "Posts to process: " . count($posts) . "\n\n"; // ------------------------------------------------------------------------- // Step 3: Process each post // ------------------------------------------------------------------------- $stats = [ 'posts_changed' => 0, 'posts_skipped' => 0, 'links_replaced' => 0, 'links_not_found'=> 0, ]; $not_found_log = []; // Regex patterns (note: href values may be HTML-entity encoded: & → &) $patterns = [ // Pattern A: index.php?option=com_content&[amp;]view=article&[amp;]id=NNN[;alias] 'joomla_content' => '/href="index\.php\?option=com_content(?:&(?:amp;)?)[^"]*?(?:&(?:amp;)?)id=(\d+)[^"]*"/i', // Pattern B: K2 item links — only on known Joomla-origin domains/paths // Matches: // href="es/[path/]NNN-slug.html" (relative) // href="http://feadulta.com/[path/]NNN-slug.html" (old domain) // href="https://farmer.taild3aaf6.ts.net/fea/[path/]NNN-slug.html" (staging domain) 'k2_item' => '/href="(?:(?:https?:\/\/feadulta\.com|https?:\/\/farmer\.taild3aaf6\.ts\.net\/fea)\/)?es\/[^"]*?\/(\d+)-[^"\/]+\.html[^"]*"/i', ]; $update_stmt = $pdo->prepare("UPDATE wp_posts SET post_content = ? WHERE ID = ?"); foreach ($posts as $post) { $original = $post['post_content']; $content = $original; $changed = false; // --- Pattern A: jos_content links --- $content = preg_replace_callback( $patterns['joomla_content'], function ($m) use ($joomla_map, $wp_site_url, &$stats, &$not_found_log, $post) { $id = (int)$m[1]; if (isset($joomla_map[$id])) { $stats['links_replaced']++; $new_url = $wp_site_url . '/' . $joomla_map[$id] . '/'; return 'href="' . $new_url . '"'; } $stats['links_not_found']++; $not_found_log[] = "jos_content ID=$id not found (post {$post['ID']}: {$post['post_title']})"; return $m[0]; // keep original }, $content ); // --- Pattern B: K2 item links --- $content = preg_replace_callback( $patterns['k2_item'], function ($m) use ($k2_map, $wp_site_url, &$stats, &$not_found_log, $post) { $id = (int)$m[1]; // Skip if ID 0, or if this looks like a year (4 digits in 1900-2100 range) in a date URL if ($id === 0) return $m[0]; // Skip pure numbers that are years in date-based URLs (e.g. /2024/01/post.html) // We check: if the full match contains /YYYY/ before the filename, skip if ($id >= 1990 && $id <= 2100 && preg_match('/\/\d{4}\//', $m[0])) { return $m[0]; } if (isset($k2_map[$id])) { $stats['links_replaced']++; $new_url = $wp_site_url . '/' . $k2_map[$id] . '/'; return 'href="' . $new_url . '"'; } $stats['links_not_found']++; $not_found_log[] = "K2 ID=$id not found in map (post {$post['ID']}: {$post['post_title']}) | original: " . substr($m[0], 0, 100); return $m[0]; // keep original }, $content ); if ($content !== $original) { $changed = true; $stats['posts_changed']++; if (!$dry_run) { $update_stmt->execute([$content, $post['ID']]); } else { echo " [DRY] Would update post {$post['ID']}: {$post['post_title']}\n"; } } else { $stats['posts_skipped']++; } } // ------------------------------------------------------------------------- // Step 4: Summary // ------------------------------------------------------------------------- echo "\n=== Results ===\n"; echo "Posts changed: {$stats['posts_changed']}\n"; echo "Posts unchanged: {$stats['posts_skipped']}\n"; echo "Links replaced: {$stats['links_replaced']}\n"; echo "Links not resolved: {$stats['links_not_found']}\n"; if (!empty($not_found_log)) { $log_path = '/tmp/fix_joomla_links_unresolved.log'; file_put_contents($log_path, implode("\n", $not_found_log) . "\n"); echo "\nUnresolved links logged to: $log_path\n"; echo "First 10 unresolved:\n"; foreach (array_slice($not_found_log, 0, 10) as $line) { echo " $line\n"; } } echo "\nDone.\n";