2. September 2025

Eine ganze WordPress-Installation durchsuchen

Eine ganze WordPress-Installation zu durchsuchen ist nicht ganz unkompliziert. Mit dem MU-Plugin Search Everywhere geht das aber relativ einfach. Dateitypen und Datenbank-Tabellen können konfiguriert werden. Für den Aufruf muss man als Administrator in WordPress angemeldet sein.

<?php

/**
 * Plugin Name: Search Everywhere
 * Version: 1.0
 * Description: Finds files and database entries that contain a specific string or regex
 * 
 * INSTALLATION:
 * Place this file in: /wp-content/mu-plugins/search-everywhere-mu-plugin.php
 * (Create the mu-plugins directory if it doesn't exist)
 * 
 * USAGE:
 * 1. Configure search patterns in the $patterns array below
 * 2. Access via: yoursite.com/?search_everywhere_scan=1
 * 
 * CONFIGURATION:
 * Edit the $patterns array to define what strings/regex to search for.
 * Edit the $file_extensions array to define what file types to search.
 * Edit the $database_config array to define what database tables/columns to search.
 * 
 * SEARCH PATTERNS:
 * SIMPLE STRINGS (case-insensitive):
 * $patterns = ["debug_code", "test_mode", "dev_flag"];
 * 
 * CLEAN REGEX FORMAT (recommended for regex):
 * $patterns = [
 *     "mn_dev",                                              // Simple string
 *     ['regex' => '\$_GET\s*\[\s*[\'"]debug[\'"]\s*\]'],     // $_GET['debug'] - no escaping needed!
 *     ['regex' => 'isset\s*\(\s*\$_GET\s*\[\s*[\'"]test'],   // isset($_GET['test']
 *     ['regex' => 'define\s*\(\s*[\'"]DEBUG[\'"]'],          // define('DEBUG'
 * ];
 * 
 * HEREDOC FORMAT (for very complex patterns):
 * $complex_pattern = <<<'REGEX'
 * \$_GET\s*\[\s*['"]debug['"]\s*\]
 * REGEX;
 * $patterns = ["mn_dev", ['regex' => $complex_pattern]];
 * 
 * LEGACY FORMAT (still supported but harder to read):
 * $patterns = ["/\\$_GET.*debug/"];  // Requires double escaping
 * 
 * FILE EXTENSIONS:
 * $file_extensions = ['php'];                           // PHP only (default)
 * $file_extensions = ['php', 'js', 'css'];             // PHP, JavaScript, CSS
 * $file_extensions = ['php', 'js', 'css', 'html', 'htm', 'txt', 'json', 'xml'];  // All common web files
 * 
 * DATABASE CONFIGURATION:
 * $database_config = [
 *     'posts' => ['post_content'],                       // Standard posts/pages
 *     'postmeta' => ['meta_value'],                      // Post metadata only
 *     'options' => ['option_value'],                     // WordPress options only
 * ];
 * 
 * $database_config = [
 *     'posts' => ['post_content', 'post_title'],         // Search content AND titles
 *     'postmeta' => ['meta_value', 'meta_key'],          // Search values AND keys
 *     'options' => ['option_value', 'option_name'],      // Search values AND names
 *     'users' => ['user_email', 'display_name'],         // Custom: search users
 *     'usermeta' => ['meta_value'],                      // Custom: user metadata
 * ];
 */

if (!defined('ABSPATH')) { exit; }

// ===== CONFIGURATION =====
$patterns = [
    // Simple strings (exact match, case-insensitive)
    'mn_dev',
    
    // Regex patterns using array format (much cleaner!)
    ['regex' => '\$_GET\s*\[\s*[\'"]mn_dev[\'"]\s*\]'],     // $_GET['mn_dev']
];

// File extensions to search (without the dot)
$file_extensions = ['php', 'js', 'css', 'html', 'htm', 'txt', 'json', 'xml'];

// Database tables and columns to search
$database_config = [
    'posts' => ['post_content'],                    // Posts, pages, custom post types
    'postmeta' => ['meta_value'],                   // Post metadata
    'options' => ['option_value'],                  // WordPress options
    'comments' => ['comment_content'],              // User comments
    'commentmeta' => ['meta_value'],                // Comment metadata
    // 'usermeta' => ['meta_value'],                   // User settings
];

add_action('init', function () {
    global $patterns, $file_extensions, $database_config;
    
    // Handle AJAX request for unserializing data
    if (isset($_GET['search_everywhere_unserialize'])) {
        handle_unserialize_request();
        return;
    }
    
    // Handle AJAX request for file context
    if (isset($_GET['search_everywhere_context'])) {
        handle_context_request();
        return;
    }
    
    // Only run scanner when specifically requested and user is admin
    if (!isset($_GET['search_everywhere_scan']) || !current_user_can('manage_options')) return;

    // Prevent timeout for large scans
    set_time_limit(300);
    
    // Add dark mode styling
    echo "<!DOCTYPE html>\n";
    echo "<html>\n";
    echo "<head>\n";
    echo "<meta charset='UTF-8'>\n";
    echo "<meta name='viewport' content='width=device-width, initial-scale=1.0'>\n";
    echo "<title>Search Everywhere Scanner</title>\n";
    echo "<style>\n";
    echo "body { background: #1a1a1a; color: #e0e0e0; font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', 'Courier New', monospace; margin: 20px; line-height: 1.6; }\n";
    echo "h1, h2, h3 { color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; }\n";
    echo "h1 { border-bottom: 2px solid #333; padding-bottom: 10px; }\n";
    echo "h2 { color: #4fc3f7; margin-top: 30px; }\n";
    echo "h3 { color: #81c784; }\n";
    echo "pre { background: #2d2d2d; border: 1px solid #444; border-radius: 6px; padding: 15px; overflow-x: auto; font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', 'Courier New', monospace; }\n";
    echo "strong { color: #ffb74d; }\n";
    echo "a { color: #64b5f6; text-decoration: none; }\n";
    echo "a:hover { color: #90caf9; text-decoration: underline; }\n";
    echo ".button { background: #0073aa; color: white; padding: 8px 16px; text-decoration: none; border-radius: 3px; display: inline-block; margin: 5px 0; }\n";
    echo ".button:hover { background: #005a87; color: white; }\n";
    echo "ul { margin: 10px 0; }\n";
    echo "li { margin: 5px 0; }\n";
    echo ".context { background: #2d2d2d; padding: 15px; border: 1px solid #444; border-radius: 6px; margin: 10px 0; }\n";
    echo ".unserialize-btn { background: #28a745; color: white; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer; font-size: 12px; margin: 5px 0; }\n";
    echo ".unserialize-btn:hover { background: #218838; }\n";
    echo ".context-btn { background: #6c757d; color: white; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer; font-size: 12px; margin: 5px 0; }\n";
    echo ".context-btn:hover { background: #5a6268; }\n";
    echo ".unserialized-data, .context-data { background: #2d2d2d; border: 1px solid #444; border-radius: 6px; padding: 15px; margin: 10px 0; white-space: pre-wrap; font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', 'Courier New', monospace; }\n";
    echo "</style>\n";
    echo "<script>\n";
    echo "function unserializeData(table, column, id, metaKey = '') {\n";
    echo "    const btn = event.target;\n";
    echo "    const container = btn.nextElementSibling;\n";
    echo "    if (container && container.classList.contains('unserialized-data') && container.style.display === 'block') {\n";
    echo "        container.style.display = 'none';\n";
    echo "        btn.textContent = 'Decode';\n";
    echo "        return;\n";
    echo "    }\n";
    echo "    btn.textContent = 'Loading...';\n";
    echo "    const url = window.location.origin + window.location.pathname + '?search_everywhere_unserialize=1&table=' + encodeURIComponent(table) + '&column=' + encodeURIComponent(column) + '&id=' + encodeURIComponent(id) + '&meta_key=' + encodeURIComponent(metaKey);\n";
    echo "    fetch(url)\n";
    echo "        .then(response => response.text())\n";
    echo "        .then(data => {\n";
    echo "            let targetContainer = container;\n";
    echo "            if (!targetContainer || !targetContainer.classList.contains('unserialized-data')) {\n";
    echo "                targetContainer = document.createElement('div');\n";
    echo "                targetContainer.className = 'unserialized-data';\n";
    echo "                btn.parentNode.insertBefore(targetContainer, btn.nextSibling);\n";
    echo "            }\n";
    echo "            targetContainer.innerHTML = data;\n";
    echo "            targetContainer.style.display = 'block';\n";
    echo "            btn.textContent = 'Hide';\n";
    echo "        })\n";
    echo "        .catch(error => {\n";
    echo "            btn.textContent = 'Error';\n";
    echo "            console.error('Error:', error);\n";
    echo "        });\n";
    echo "}\n";
    echo "function showFileContext(filePath, lineNumber) {\n";
    echo "    const btn = event.target;\n";
    echo "    const container = btn.nextElementSibling;\n";
    echo "    if (container && container.classList.contains('context-data') && container.style.display === 'block') {\n";
    echo "        container.style.display = 'none';\n";
    echo "        btn.textContent = 'Show Context';\n";
    echo "        return;\n";
    echo "    }\n";
    echo "    btn.textContent = 'Loading...';\n";
    echo "    const url = window.location.origin + window.location.pathname + '?search_everywhere_context=1&file=' + encodeURIComponent(filePath) + '&line=' + encodeURIComponent(lineNumber);\n";
    echo "    fetch(url)\n";
    echo "        .then(response => response.text())\n";
    echo "        .then(data => {\n";
    echo "            let targetContainer = container;\n";
    echo "            if (!targetContainer || !targetContainer.classList.contains('context-data')) {\n";
    echo "                targetContainer = document.createElement('div');\n";
    echo "                targetContainer.className = 'context-data';\n";
    echo "                btn.parentNode.insertBefore(targetContainer, btn.nextSibling);\n";
    echo "            }\n";
    echo "            targetContainer.innerHTML = data;\n";
    echo "            targetContainer.style.display = 'block';\n";
    echo "            btn.textContent = 'Hide Context';\n";
    echo "        })\n";
    echo "        .catch(error => {\n";
    echo "            btn.textContent = 'Error';\n";
    echo "            console.error('Error:', error);\n";
    echo "        });\n";
    echo "}\n";
    echo "</script>\n";
    echo "</head>\n";
    echo "<body>\n";
    
    echo "<h1>Search Everywhere Scanner</h1>\n";
    $pattern_displays = array_map('get_pattern_display', $patterns);
    echo "<p>Scanning for patterns: <strong>" . esc_html(implode(', ', $pattern_displays)) . "</strong></p>\n";
    echo "<p>File types: <strong>" . esc_html(implode(', ', array_map(function($ext) { return '.' . $ext; }, $file_extensions))) . "</strong></p>\n";
    
    // Debug: Show pattern types
    echo "<p><small>Pattern types: ";
    foreach ($patterns as $p) {
        echo esc_html(get_pattern_display($p)) . " (" . (is_regex_pattern($p) ? "regex" : "string") . "), ";
    }
    echo "</small></p>\n";
    
    $found_files = [];
    $found_db_entries = [];
    
    // 1. Scan files for the search patterns
    echo "<h2>Scanning Files...</h2>\n";
    $php_files = scan_php_files_for_patterns($patterns, $file_extensions);
    if (!empty($php_files)) {
        echo "<h3>Found in Files:</h3>\n";
        foreach ($php_files as $file_info) {
            echo "<strong>File:</strong> " . esc_html($file_info['file']) . "<br>\n";
            echo "<strong>Line:</strong> " . esc_html($file_info['line']) . "<br>\n";
            echo "<strong>Pattern:</strong> " . esc_html($file_info['matched_pattern']) . "<br>\n";
            echo "<strong>Code:</strong> <pre>" . esc_html($file_info['code']) . "</pre>\n";
            
            // Add context button for all files
            echo "<button class='context-btn' onclick='showFileContext(\"" . esc_attr($file_info['file']) . "\", \"" . esc_attr($file_info['line']) . "\")'>Show Context</button>\n";
            
            echo "<br><br>\n";
        }
        $found_files = $php_files;
    } else {
        echo "<p>No files found with configured patterns.</p>\n";
    }
    
    // 2. Scan database for the search patterns
    echo "<h2>Scanning Database...</h2>\n";
    $db_entries = scan_database_for_patterns($patterns, $database_config);
    if (!empty($db_entries)) {
        echo "<h3>Found in Database:</h3>\n";
        foreach ($db_entries as $entry) {
            echo "<strong>Table:</strong> " . esc_html($entry['table']) . "<br>\n";
            echo "<strong>Column:</strong> " . esc_html($entry['column']) . "<br>\n";
            echo "<strong>ID:</strong> " . esc_html($entry['id']) . "<br>\n";
            echo "<strong>Pattern:</strong> " . esc_html($entry['matched_pattern']) . "<br>\n";
            
            // Show revision info if available
            if (isset($entry['is_revision'])) {
                echo "<strong>Post Type:</strong> " . esc_html($entry['post_type'] ?? 'unknown') . "<br>\n";
                echo "<strong>Post Status:</strong> " . esc_html($entry['post_status'] ?? 'unknown') . "<br>\n";
                if ($entry['is_revision']) {
                    echo "<strong>🔄 REVISION:</strong> Yes" . esc_html($entry['parent_info'] ?? '') . "<br>\n";
                } else {
                    echo "<strong>🔄 REVISION:</strong> No<br>\n";
                }
                if (isset($entry['post_title'])) {
                    echo "<strong>Post Title:</strong> " . esc_html($entry['post_title']) . "<br>\n";
                }
            }
            
            if (isset($entry['meta_key'])) {
                echo "<strong>Meta Key:</strong> " . esc_html($entry['meta_key']) . "<br>\n";
            }
            
            echo "<strong>Preview:</strong> <pre>" . esc_html(substr($entry['content'], 0, 300)) . "...</pre>\n";
            
            // Add decode button for all database entries
            $meta_key = isset($entry['meta_key']) ? $entry['meta_key'] : '';
            echo "<button class='unserialize-btn' onclick='unserializeData(\"" . esc_attr($entry['table']) . "\", \"" . esc_attr($entry['column']) . "\", \"" . esc_attr($entry['id']) . "\", \"" . esc_attr($meta_key) . "\")'>Decode</button>\n";
            
            echo "<br><br>\n";
        }
        $found_db_entries = $db_entries;
    } else {
        echo "<p>No database entries found with configured patterns.</p>\n";
    }
    
    // Count revisions
    $revision_count = 0;
    foreach ($found_db_entries as $entry) {
        if (isset($entry['is_revision']) && $entry['is_revision']) {
            $revision_count++;
        }
    }
    
    echo "<h2>Summary</h2>\n";
    echo "<p>Total files with patterns: " . count($found_files) . "</p>\n";
    echo "<p>Total database entries with patterns: " . count($found_db_entries) . "</p>\n";
    echo "<p>└── Revisions: {$revision_count}</p>\n";
    echo "<p>└── Active content: " . (count($found_db_entries) - $revision_count) . "</p>\n";
    
    if (empty($found_files) && empty($found_db_entries)) {
        echo "<p><strong>No configured patterns found!</strong> The code might be:</p>\n";
        echo "<ul>\n";
        echo "<li>In a theme file outside wp-content</li>\n";
        echo "<li>In a plugin that's not currently active</li>\n";
        echo "<li>In a cached file</li>\n";
        echo "<li>Using a different parameter name</li>\n";
        echo "</ul>\n";
    }
    
    echo "</body>\n";
    echo "</html>\n";
    exit();
});

function scan_php_files_for_patterns($search_patterns, $file_extensions = ['php']) {
    $found = [];
    $found_unique = []; // Track unique file:line combinations to prevent duplicates
    
    // Scan ENTIRE WordPress installation recursively
    $root_directories = [
        ABSPATH // This will scan everything: wp-content, wp-includes, wp-admin, root files, etc.
    ];
    
    foreach ($root_directories as $root_dir) {
        if (!is_dir($root_dir)) continue;
        
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($root_dir, RecursiveDirectoryIterator::SKIP_DOTS),
            RecursiveIteratorIterator::SELF_FIRST
        );
        
        foreach ($iterator as $file) {
            if (!$file->isFile() || !in_array(strtolower($file->getExtension()), $file_extensions)) continue;
            
            $file_path = $file->getPathname();
            
            // Skip our own scanner file to avoid self-detection
            if (strpos($file_path, 'search-everywhere-mu-plugin.php') !== false) continue;
            
            // Skip some heavy/unnecessary directories for performance
            if (preg_match('/\/(cache|tmp|temp|logs|backups)\//', $file_path)) continue;
            
            $content = @file_get_contents($file_path);
            if ($content === false) continue;
            
            $lines = explode("\n", $content);
            
            foreach ($search_patterns as $pattern) {
                foreach ($lines as $line_num => $line) {
                    $matches = false;
                    
                    // Check if pattern is regex
                    if (is_regex_pattern($pattern)) {
                        $regex = get_regex_from_pattern($pattern);
                        $matches = @preg_match($regex, $line);
                        // Handle regex errors
                        if ($matches === false) {
                            continue; // Skip invalid regex
                        }
                        $matches = $matches > 0; // Convert to boolean
                    } else {
                        // Simple string search (case-insensitive)
                        $matches = stripos($line, $pattern) !== false;
                    }
                    
                    if ($matches) {
                        $unique_key = $file_path . ':' . ($line_num + 1);
                        
                        // Prevent duplicates
                        if (!isset($found_unique[$unique_key])) {
                            $found[] = [
                                'file' => $file_path,
                                'line' => $line_num + 1,
                                'code' => trim($line),
                                'matched_pattern' => get_pattern_display($pattern)
                            ];
                            $found_unique[$unique_key] = true;
                        }
                    }
                }
            }
        }
    }
    
    return $found;
}

function scan_database_for_patterns($search_patterns, $database_config) {
    global $wpdb;
    $found = [];
    
    foreach ($search_patterns as $pattern) {
        foreach ($database_config as $table_name => $columns) {
            foreach ($columns as $column) {
                // Handle special table configurations
                if ($table_name === 'posts') {
                    $results = search_in_posts_table($pattern, $column, $wpdb);
                } elseif ($table_name === 'postmeta') {
                    $results = search_in_postmeta_table($pattern, $column, $wpdb);
                } elseif ($table_name === 'options') {
                    $results = search_in_options_table($pattern, $column, $wpdb);
                } elseif ($table_name === 'breakdance_template') {
                    $results = search_in_breakdance_table($pattern, $column, $wpdb);
                } else {
                    // Generic table search for custom tables
                    $results = search_in_generic_table($pattern, $table_name, $column, $wpdb);
                }
                
                $found = array_merge($found, $results);
            }
        }
    }
    
    return $found;
}

function search_in_posts_table($pattern, $column, $wpdb) {
    $found = [];
    
    if (is_regex_pattern($pattern)) {
        $posts = $wpdb->get_results("
            SELECT ID, post_title, {$column}, post_type, post_status, post_parent
            FROM {$wpdb->posts} 
            WHERE {$column} != ''
        ");
        
        $regex = get_regex_from_pattern($pattern);
        foreach ($posts as $post) {
            if (@preg_match($regex, $post->{$column})) {
                $found[] = create_post_result($post, $column, $pattern);
            }
        }
    } else {
        $search_pattern = '%' . $pattern . '%';
        $posts = $wpdb->get_results($wpdb->prepare("
            SELECT ID, post_title, {$column}, post_type, post_status, post_parent
            FROM {$wpdb->posts} 
            WHERE {$column} LIKE %s
        ", $search_pattern));
        
        foreach ($posts as $post) {
            $found[] = create_post_result($post, $column, $pattern);
        }
    }
    
    return $found;
}

function search_in_postmeta_table($pattern, $column, $wpdb) {
    $found = [];
    
    if (is_regex_pattern($pattern)) {
        $meta = $wpdb->get_results("
            SELECT pm.post_id, pm.meta_key, pm.{$column}, p.post_type, p.post_status, p.post_title, p.post_parent
            FROM {$wpdb->postmeta} pm
            LEFT JOIN {$wpdb->posts} p ON pm.post_id = p.ID
            WHERE pm.{$column} != ''
        ");
        
        $regex = get_regex_from_pattern($pattern);
        foreach ($meta as $meta_entry) {
            if (@preg_match($regex, $meta_entry->{$column})) {
                $found[] = create_postmeta_result($meta_entry, $column, $pattern);
            }
        }
    } else {
        $search_pattern = '%' . $pattern . '%';
        $meta = $wpdb->get_results($wpdb->prepare("
            SELECT pm.post_id, pm.meta_key, pm.{$column}, p.post_type, p.post_status, p.post_title, p.post_parent
            FROM {$wpdb->postmeta} pm
            LEFT JOIN {$wpdb->posts} p ON pm.post_id = p.ID
            WHERE pm.{$column} LIKE %s
        ", $search_pattern));
        
        foreach ($meta as $meta_entry) {
            $found[] = create_postmeta_result($meta_entry, $column, $pattern);
        }
    }
    
    return $found;
}

function search_in_options_table($pattern, $column, $wpdb) {
    $found = [];
    
    if (is_regex_pattern($pattern)) {
        $options = $wpdb->get_results("
            SELECT option_name, {$column} 
            FROM {$wpdb->options} 
            WHERE {$column} != ''
        ");
        
        $regex = get_regex_from_pattern($pattern);
        foreach ($options as $option) {
            if (@preg_match($regex, $option->{$column})) {
                $found[] = [
                    'table' => 'options',
                    'column' => $column,
                    'id' => $option->option_name,
                    'matched_pattern' => get_pattern_display($pattern),
                    'content' => $option->{$column}
                ];
            }
        }
    } else {
        $search_pattern = '%' . $pattern . '%';
        $options = $wpdb->get_results($wpdb->prepare("
            SELECT option_name, {$column} 
            FROM {$wpdb->options} 
            WHERE {$column} LIKE %s
        ", $search_pattern));
        
        foreach ($options as $option) {
            $found[] = [
                'table' => 'options',
                'column' => $column,
                'id' => $option->option_name,
                'matched_pattern' => get_pattern_display($pattern),
                'content' => $option->{$column}
            ];
        }
    }
    
    return $found;
}

function search_in_breakdance_table($pattern, $column, $wpdb) {
    $found = [];
    
    if (is_regex_pattern($pattern)) {
        $posts = $wpdb->get_results("
            SELECT ID, post_title, {$column}, post_type, post_status, post_parent
            FROM {$wpdb->posts} 
            WHERE post_type = 'breakdance_template'
            AND {$column} != ''
        ");
        
        $regex = get_regex_from_pattern($pattern);
        foreach ($posts as $post) {
            if (@preg_match($regex, $post->{$column})) {
                $result = create_post_result($post, $column, $pattern);
                $result['table'] = 'posts (Breakdance)';
                $found[] = $result;
            }
        }
    } else {
        $search_pattern = '%' . $pattern . '%';
        $posts = $wpdb->get_results($wpdb->prepare("
            SELECT ID, post_title, {$column}, post_type, post_status, post_parent
            FROM {$wpdb->posts} 
            WHERE post_type = 'breakdance_template'
            AND {$column} LIKE %s
        ", $search_pattern));
        
        foreach ($posts as $post) {
            $result = create_post_result($post, $column, $pattern);
            $result['table'] = 'posts (Breakdance)';
            $found[] = $result;
        }
    }
    
    return $found;
}

function search_in_generic_table($pattern, $table_name, $column, $wpdb) {
    $found = [];
    
    if (is_regex_pattern($pattern)) {
        $results = $wpdb->get_results("
            SELECT * FROM {$table_name} 
            WHERE {$column} != ''
        ");
        
        $regex = get_regex_from_pattern($pattern);
        foreach ($results as $result) {
            if (@preg_match($regex, $result->{$column})) {
                $found[] = [
                    'table' => $table_name,
                    'column' => $column,
                    'id' => $result->ID ?? $result->id ?? 'unknown',
                    'matched_pattern' => get_pattern_display($pattern),
                    'content' => $result->{$column}
                ];
            }
        }
    } else {
        $search_pattern = '%' . $pattern . '%';
        $results = $wpdb->get_results($wpdb->prepare("
            SELECT * FROM {$table_name} 
            WHERE {$column} LIKE %s
        ", $search_pattern));
        
        foreach ($results as $result) {
            $found[] = [
                'table' => $table_name,
                'column' => $column,
                'id' => $result->ID ?? $result->id ?? 'unknown',
                'matched_pattern' => get_pattern_display($pattern),
                'content' => $result->{$column}
            ];
        }
    }
    
    return $found;
}

function create_post_result($post, $column, $pattern) {
    $is_revision = $post->post_type === 'revision';
    $parent_info = '';
    if ($is_revision && $post->post_parent > 0) {
        $parent_post = get_post($post->post_parent);
        $parent_info = $parent_post ? " (Parent: {$parent_post->post_title} - ID: {$parent_post->ID})" : " (Parent ID: {$post->post_parent})";
    }
    
    return [
        'table' => 'posts',
        'column' => $column,
        'id' => $post->ID,
        'title' => $post->post_title,
        'post_type' => $post->post_type,
        'post_status' => $post->post_status,
        'is_revision' => $is_revision,
        'parent_info' => $parent_info,
        'matched_pattern' => get_pattern_display($pattern),
        'content' => $post->{$column}
    ];
}

function create_postmeta_result($meta_entry, $column, $pattern) {
    $is_revision = $meta_entry->post_type === 'revision';
    $parent_info = '';
    if ($is_revision && $meta_entry->post_parent > 0) {
        $parent_post = get_post($meta_entry->post_parent);
        $parent_info = $parent_post ? " (Parent: {$parent_post->post_title} - ID: {$parent_post->ID})" : " (Parent ID: {$meta_entry->post_parent})";
    }
    
    return [
        'table' => 'postmeta',
        'column' => $column,
        'id' => $meta_entry->post_id,
        'meta_key' => $meta_entry->meta_key,
        'post_type' => $meta_entry->post_type,
        'post_status' => $meta_entry->post_status,
        'post_title' => $meta_entry->post_title,
        'is_revision' => $is_revision,
        'parent_info' => $parent_info,
        'matched_pattern' => get_pattern_display($pattern),
        'content' => $meta_entry->{$column}
    ];
}

function is_json($string) {
    if (!is_string($string)) return false;
    
    // Trim whitespace
    $string = trim($string);
    
    // Check if it's empty
    if (empty($string)) return false;
    
    // Check if it starts with { or [ (JSON object/array) or " (JSON string)
    if (!in_array($string[0], ['{', '[', '"'])) return false;
    
    // Try to decode
    json_decode($string);
    return json_last_error() === JSON_ERROR_NONE;
}

function format_json_recursively($data) {
    // Recursively check for nested JSON strings and format them
    if (is_array($data)) {
        foreach ($data as $key => $value) {
            if (is_string($value) && is_json($value)) {
                // Found nested JSON string, decode and format it
                $nested_data = json_decode($value, true);
                if ($nested_data !== null) {
                    $data[$key] = $nested_data;
                }
            } elseif (is_array($value)) {
                $data[$key] = format_json_recursively($value);
            }
        }
    }
    
    // Return formatted JSON
    return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}

function get_file_context($file_path, $target_line, $context_lines = 5) {
    $content = @file_get_contents($file_path);
    if ($content === false) return "Could not read file.";
    
    $lines = explode("\n", $content);
    $start = max(0, $target_line - $context_lines - 1);
    $end = min(count($lines) - 1, $target_line + $context_lines - 1);
    
    $context = "";
    for ($i = $start; $i <= $end; $i++) {
        $line_num = $i + 1;
        $marker = ($line_num == $target_line) ? ">>> " : "    ";
        $context .= sprintf("%s%3d: %s\n", $marker, $line_num, htmlspecialchars($lines[$i]));
    }
    
    return $context;
}

function handle_unserialize_request() {
    global $wpdb;
    
    // Set content type for AJAX response
    header('Content-Type: text/html; charset=utf-8');
    
    // Validate required parameters
    if (!isset($_GET['table']) || !isset($_GET['column']) || !isset($_GET['id'])) {
        echo "Missing required parameters.";
        exit;
    }
    
    $table = sanitize_text_field($_GET['table']);
    $column = sanitize_text_field($_GET['column']);
    $id = sanitize_text_field($_GET['id']);
    $meta_key = isset($_GET['meta_key']) ? sanitize_text_field($_GET['meta_key']) : '';
    
    // Fetch the data based on table type
    $content = '';
    if ($table === 'posts' || $table === 'posts (Breakdance)') {
        $result = $wpdb->get_var($wpdb->prepare("SELECT {$column} FROM {$wpdb->posts} WHERE ID = %d", $id));
        $content = $result;
    } elseif ($table === 'postmeta') {
        $result = $wpdb->get_var($wpdb->prepare("SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s", $id, $meta_key));
        $content = $result;
    } elseif ($table === 'options') {
        $result = $wpdb->get_var($wpdb->prepare("SELECT option_value FROM {$wpdb->options} WHERE option_name = %s", $id));
        $content = $result;
    }
    
    if (empty($content)) {
        echo "No content found.";
        exit;
    }
    
    // Try to unserialize or decode
    echo "<strong>Unserialized/Decoded Data:</strong><br><br>";
    
    if (is_serialized($content)) {
        $unserialized = @unserialize($content);
        if ($unserialized !== false) {
            echo "<strong>Type:</strong> PHP Serialized Data<br><br>";
            echo "<pre>" . esc_html(print_r($unserialized, true)) . "</pre>";
        } else {
            echo "Failed to unserialize data.";
        }
    } elseif (is_json($content)) {
        $json_data = json_decode($content, true);
        if ($json_data !== null) {
            echo "<strong>Type:</strong> JSON Data<br><br>";
            
            // Handle JSON strings (quoted JSON) vs JSON objects/arrays
            if (is_string($json_data)) {
                // This is a JSON string, try to decode it further
                if (is_json($json_data)) {
                    $nested_json = json_decode($json_data, true);
                    if ($nested_json !== null) {
                        echo "<strong>Note:</strong> JSON string containing nested JSON<br><br>";
                        $formatted_json = format_json_recursively($nested_json);
                        echo "<pre>" . esc_html($formatted_json) . "</pre>";
                    } else {
                        echo "<pre>" . esc_html($json_data) . "</pre>";
                    }
                } else {
                    echo "<pre>" . esc_html($json_data) . "</pre>";
                }
            } else {
                // Regular JSON object/array
                $formatted_json = format_json_recursively($json_data);
                echo "<pre>" . esc_html($formatted_json) . "</pre>";
            }
        } else {
            echo "Failed to decode JSON data.";
        }
    } else {
        echo "Content is not serialized or JSON. Raw content:<br>";
        echo "<pre>" . esc_html($content) . "</pre>";
    }
    
    exit;
}

function handle_context_request() {
    // Set content type for AJAX response
    header('Content-Type: text/html; charset=utf-8');
    
    // Validate required parameters
    if (!isset($_GET['file']) || !isset($_GET['line'])) {
        echo "Missing required parameters.";
        exit;
    }
    
    $file_path = sanitize_text_field($_GET['file']);
    $line_number = intval($_GET['line']);
    
    // Security check - ensure file is within WordPress directory
    if (strpos(realpath($file_path), realpath(ABSPATH)) !== 0) {
        echo "File access denied for security reasons.";
        exit;
    }
    
    if (!file_exists($file_path)) {
        echo "File not found.";
        exit;
    }
    
    echo "<strong>📋 Context (±10 lines):</strong><br><br>";
    $context = get_file_context($file_path, $line_number, 10);
    echo "<pre>" . $context . "</pre>";
    
    exit;
}

function is_regex_pattern($pattern) {
    // Check if pattern is array with 'regex' key OR starts and ends with forward slashes
    if (is_array($pattern) && isset($pattern['regex'])) {
        return true;
    }
    return is_string($pattern) && strlen($pattern) >= 3 && $pattern[0] === '/' && substr($pattern, -1) === '/';
}

function get_regex_from_pattern($pattern) {
    // Extract regex from pattern
    if (is_array($pattern) && isset($pattern['regex'])) {
        return '/' . $pattern['regex'] . '/';
    }
    return $pattern; // Already a string regex
}

function get_pattern_display($pattern) {
    // Get display string for pattern
    if (is_array($pattern) && isset($pattern['regex'])) {
        return $pattern['regex'];
    }
    return $pattern;
}