Your IP : 216.73.216.196


Current Path : /home/icmq6107/clients/BUCODAC/wp-content/plugins/surflink/includes/
Upload File :
Current File : /home/icmq6107/clients/BUCODAC/wp-content/plugins/surflink/includes/class-surf-link-sr.php

<?php

/**
 * Core search and replace functionality for the Surf Link plugin.
 *
 * This class provides comprehensive search and replace functionality for WordPress databases,
 * with support for serialized data, batch processing, and detailed reporting.
 * It handles both regular search/replace operations and URL-specific replacements.
 *
 * @since 1.0.0
 */
if (!defined('ABSPATH')) {
    exit;
}
class SURFL_HyperDBReplace
{
    /**
     * WordPress database object.
     *
     * @since 1.0.0
     * @var wpdb
     */
    private $wpdb;

    /**
     * Number of database rows to process in a single batch.
     *
     * @since 1.0.0
     * @var int
     */
    private $batch_size = 500;

    /**
     * Collection of operation reports.
     *
     * @since 1.0.0
     * @var array
     */
    private $reports = [];

    /**
     * Detailed metrics and statistics about the operations.
     *
     * @since 1.0.0
     * @var array
     */
    private $report_details = [];

    /**
     * Whether the search should be case insensitive.
     *
     * @since 1.0.0
     * @var bool
     */
    private $case_insensitive = false;

    /**
     * Initialize the search and replace functionality.
     *
     * Sets up the class properties and retrieves the global wpdb object.
     *
     * @since 1.0.0
     */
    public function __construct()
    {
        global $wpdb;
        $this->wpdb = $wpdb;
    }

    /**
     * Process a search and replace operation across specified database tables.
     *
     * This is the main method that orchestrates the entire search and replace process.
     * It handles special treatment for the options table, processes tables in batches,
     * and collects detailed metrics about the operation.
     *
     * @since 1.0.0
     * @param string $search           The string to search for
     * @param string $replace          The replacement string
     * @param array  $tables           Array of database table names to search in
     * @param bool   $dry_run          If true, no actual changes are made
     * @param bool   $replace_guid     Whether to include GUID columns in the replacement
     * @param bool   $case_insensitive Whether the search should be case insensitive
     * @return array                   Detailed report of the operation
     */
    public function process_replace($search, $replace, $tables, $dry_run = false, $replace_guid = false, $case_insensitive = false)
    {
        $this->case_insensitive = $case_insensitive; // Save the flag for use in other methods
        // Reset/initialize report details.
        $this->report_details = [];
        // Separate out the options table (if selected) from the rest.
        $non_options_tables = [];
        $options_tables = [];

        foreach ($tables as $table) {
            if ($table === $this->wpdb->options) {
                $options_tables[] = $table;
            } else {
                $non_options_tables[] = $table;
            }
        }

        try {
            // Process all tables except the options table first.
            foreach ($non_options_tables as $table) {
                if (!$this->table_exists($table)) {
                    throw new Exception("Invalid table: $table");
                }

                $primary_key = $this->get_primary_key($table);
                $columns = $this->get_text_columns($table);

                // If the user did not check the replace guid option, filter out any column named 'guid'
                if (!$replace_guid) {
                    $columns = array_filter($columns, function ($col) {
                        return strtolower($col) !== 'guid';
                    });
                }

                // Initialize detailed metrics for this table.
                $this->report_details[$table]['rows_scanned'] = 0;
                $table_start = microtime(true);

                $offset = 0;
                while (true) {
                    $prepared_cols = implode(', ', $columns);
                    $prepared_query = $this->wpdb->prepare(
                        "SELECT $primary_key, $prepared_cols FROM $table LIMIT %d, %d",
                        $offset,
                        $this->batch_size
                    );


                    $rows = $this->wpdb->get_results(
                        $prepared_query,
                        ARRAY_A
                    );

                    if (empty($rows)) {
                        break;
                    }

                    $this->report_details[$table]['rows_scanned'] += count($rows);
                    $this->process_batch($table, $primary_key, $columns, $rows, $search, $replace, $dry_run);
                    $offset += $this->batch_size;
                    $this->free_memory();
                }

                $this->report_details[$table]['time'] = microtime(true) - $table_start;
            }

            // Process the options table(s) after all other tables.
            foreach ($options_tables as $table) {
                $table_start = microtime(true);
                // For the options table, count all rows.

                $rows = $this->wpdb->get_results("SELECT option_name, option_value FROM $table", ARRAY_A);
                $this->report_details[$table]['rows_scanned'] = count($rows);
                $this->process_options_table($table, $search, $replace, $dry_run);
                $this->report_details[$table]['time'] = microtime(true) - $table_start;
            }
        } catch (Exception $e) {
            error_log('HyperDBReplace Error: ' . $e->getMessage());
            $this->reports['errors'][] = $e->getMessage();
        }
        // Append detailed report info.
        $this->reports['details'] = $this->report_details;

        if (isset($this->reports['success'])) {
            $this->reports['success'] = array_filter($this->reports['success'], function ($msg) {
                return trim($msg) !== '';
            });
        }
        if (isset($this->reports['errors'])) {
            $this->reports['errors'] = array_filter($this->reports['errors'], function ($msg) {
                return trim($msg) !== '';
            });
        }
        $this->reports = $this->reports;
        return $this->reports;
    }

    /**
     * Replace search string with replacement in a given value.
     *
     * Handles both regular strings and serialized data. For serialized data,
     * it properly unserializes, performs the replacement deep in the structure,
     * and reserializes the data safely.
     *
     * @since 1.0.0
     * @param string $value    The value to perform replacement on
     * @param string $search   The string to search for
     * @param string $replace  The replacement string
     * @return string          The value with replacements made
     */
    private function replace_value($value, $search, $replace)
    {
        if ($this->is_serialized_data($value)) {
            try {
                $unserialized = unserialize($value, ['allowed_classes' => false]);
                $modified = $this->deep_replace($search, $replace, $unserialized);
                $reserialized = serialize($modified);

                // Validate the reserialized data
                if ($this->is_serialized_data($reserialized)) {
                    return $reserialized;
                }
                return $value; // Fallback if reserialization fails
            } catch (Exception $e) {
                error_log('Serialization error in replace_value: ' . $e->getMessage());
                return $value;
            }
        }

        return $this->case_insensitive
            ? str_ireplace($search, $replace, $value)
            : str_replace($search, $replace, $value);
    }

    /**
     * Process the WordPress options table with special handling.
     * 
     * Options tables require special handling due to their unique structure and importance.
     * This method processes the options table, with 'siteurl' option being processed last
     * to maintain site integrity.
     *
     * @since 1.0.0
     * @param string $table    The name of the options table
     * @param string $search   The string to search for
     * @param string $replace  The replacement string
     * @param bool   $dry_run  Whether this is a dry run (no actual changes)
     * @throws Exception       If database update fails
     */
    private function process_options_table($table, $search, $replace, $dry_run)
    {
        if (!$dry_run) {
            // Begin transaction only if updating the DB.
            $this->wpdb->query('START TRANSACTION');
        }
        try {


            // Retrieve all rows from the options table.
            $rows = $this->wpdb->get_results("SELECT option_name, option_value FROM $table", ARRAY_A);
            $siteurlRow = null;

            // Process all options except for 'siteurl'.
            foreach ($rows as $row) {
                if ($row['option_name'] === 'siteurl') {
                    $siteurlRow = $row;
                    continue;
                }

                $original = $row['option_value'];
                $modified = $this->replace_value($original, $search, $replace);

                // Update only if a change was made.
                if ($modified !== $original) {
                    // Count occurrences in the original value.
                    $occ = $this->count_occurrences($search, $original);
                    if (!isset($this->report_details[$table]['columns']['option_value'])) {
                        $this->report_details[$table]['columns']['option_value'] = ['occurrences' => 0];
                    }
                    $this->report_details[$table]['columns']['option_value']['occurrences'] += $occ;


                    if (!$dry_run) {
                        $result = $this->wpdb->update(
                            $table,
                            ['option_value' => $modified],
                            ['option_name' => $row['option_name']]
                        );
                        if ($result === false) {
                            throw new Exception("Failed to update option '" . $row['option_name'] . "' in table $table: " . $this->wpdb->last_error);
                        }
                    }
                    $this->reports[$table] = ($this->reports[$table] ?? 0) + 1;
                }
            }

            // Process the 'siteurl' option last.
            if ($siteurlRow !== null) {
                $original = $siteurlRow['option_value'];
                $modified = $this->replace_value($original, $search, $replace);

                if ($modified !== $original) {
                    $occ = $this->count_occurrences($search, $original);
                    if (!isset($this->report_details[$table]['columns']['option_value'])) {
                        $this->report_details[$table]['columns']['option_value'] = ['occurrences' => 0];
                    }
                    $this->report_details[$table]['columns']['option_value']['occurrences'] += $occ;


                    if (!$dry_run) {
                        $result = $this->wpdb->update(
                            $table,
                            ['option_value' => $modified],
                            ['option_name' => 'siteurl']
                        );
                        if ($result === false) {
                            throw new Exception("Failed to update option 'siteurl' in table $table: " . $this->wpdb->last_error);
                        }
                    }
                    $this->reports[$table] = ($this->reports[$table] ?? 0) + 1;
                }
            }

            if (!$dry_run) {
                $this->wpdb->query('COMMIT');
            }
        } catch (Exception $e) {
            if (!$dry_run) {
                $this->wpdb->query('ROLLBACK');
            }
            throw $e;
        }
    }

    /**
     * Process a batch of database rows for search and replace operation.
     *
     * Uses SQL CASE statements for efficient batch updates, optimizing
     * the database operations and minimizing the number of queries.
     *
     * @since 1.0.0
     * @param string $table       The database table name
     * @param string $primary_key The primary key column name
     * @param array  $columns     Array of column names to process
     * @param array  $rows        Array of database rows to process
     * @param string $search      The string to search for
     * @param string $replace     The replacement string
     * @param bool   $dry_run     Whether this is a dry run (no actual changes)
     * @throws Exception          If update fails or primary key is not found
     */
    private function process_batch($table, $primary_key, $columns, $rows, $search, $replace, $dry_run)
    {
        $case_statements = [];
        $ids = [];

        foreach ($rows as $row) {
            if (!isset($row[$primary_key])) {
                throw new Exception("Primary key $primary_key not found in table $table");
            }

            foreach ($columns as $col) {
                $original = $row[$col];
                $is_serialized = $this->is_serialized_data($original);

                if ($is_serialized) {
                    $unserialized = @unserialize($original);
                    if ($unserialized !== false) {
                        $modified_data = $this->deep_replace($search, $replace, $unserialized, $this->case_insensitive);
                        $modified = serialize($modified_data);
                    } else {
                        // On failure to unserialize, fall back gracefully.
                        $modified = $original;
                    }
                } else {
                    $modified = $this->case_insensitive
                        ? str_ireplace($search, $replace, $original)
                        : str_replace($search, $replace, $original);
                }

                // Only prepare a CASE update if the value has been modified.
                if ($original !== $modified) {
                    // Count occurrences in the original cell.
                    $occ = $this->count_occurrences($search, $original);
                    if (!isset($this->report_details[$table]['columns'][$col])) {
                        $this->report_details[$table]['columns'][$col] = ['occurrences' => 0];
                    }
                    $this->report_details[$table]['columns'][$col]['occurrences'] += $occ;


                    if (!$dry_run) {
                        $case_statements[$col][] = $this->wpdb->prepare(
                            "WHEN %d THEN %s",
                            $row[$primary_key],
                            $modified
                        );
                    }
                    $ids[] = $row[$primary_key];
                }
            }
        }

        if (!empty($case_statements)) {
            if (!$dry_run) {
                $this->wpdb->query('START TRANSACTION');

                foreach ($case_statements as $col => $cases) {
                    $_table = $this->sanitize_identifier($table);
                    $_primary_key = $this->sanitize_identifier($primary_key);
                    $_col = $this->sanitize_identifier($col);

                    $sql = "UPDATE $_table SET $_col = CASE $_primary_key 
                            " . implode(' ', $cases) . " 
                            END WHERE $_primary_key IN (" . implode(',', array_unique($ids)) . ")";

                    $result = $this->wpdb->query($sql);
                    if ($result === false) {
                        $this->wpdb->query('ROLLBACK');
                        throw new Exception("Update failed for column $col in table $table: " . $this->wpdb->last_error);
                    }
                }

                $this->wpdb->query('COMMIT');
            }
            $this->reports[$table] = ($this->reports[$table] ?? 0) + count($ids);
        }
    }

    /**
     * Sanitize a database identifier (table or column name).
     *
     * Ensures identifiers are safe to use in SQL queries by checking 
     * for valid characters and backtick-escaping them.
     *
     * @since 1.0.0
     * @param string $identifier The identifier to sanitize
     * @return string Sanitized and backtick-wrapped identifier
     * @throws Exception If identifier contains invalid characters
     */
    private function sanitize_identifier($identifier)
    {
        // Allow only alphanumeric characters and underscores
        if (!preg_match('/^[A-Za-z0-9_]+$/', $identifier)) {
            throw new Exception("Invalid identifier: " . $identifier);
        }
        return "`" . $identifier . "`";
    }

    /**
     * Get the primary key column name for a database table.
     *
     * Determines the primary key by querying the database schema information.
     * Falls back to reasonable defaults if no primary key is found.
     *
     * @since 1.0.0
     * @param string $table The database table name
     * @return string The primary key column name
     */
    private function get_primary_key($table)
    {
        // Remove prefix if using $wpdb->prefix
        $clean_table = str_replace($this->wpdb->prefix, '', $table);

        $result = $this->wpdb->get_row(
            $this->wpdb->prepare(
                "SELECT COLUMN_NAME
                FROM INFORMATION_SCHEMA.COLUMNS
                WHERE TABLE_SCHEMA = DATABASE()
                AND TABLE_NAME = %s
                AND COLUMN_KEY = 'PRI'
                LIMIT 1",
                $table
            )
        );

        if ($result && isset($result->COLUMN_NAME)) {
            return $result->COLUMN_NAME;
        }

        // Fallback for tables without explicit primary key
        $columns = $this->wpdb->get_col("DESCRIBE $table", 0);
        return $columns[0] ?? 'id'; // Final fallback
    }

    /**
     * Get all text-type columns from a database table.
     *
     * Identifies columns suitable for text search and replace operations
     * by querying the database schema information.
     *
     * @since 1.0.0
     * @param string $table The database table name
     * @return array Array of column names
     */
    private function get_text_columns($table)
    {
        $columns = $this->wpdb->get_results(
            $this->wpdb->prepare(
                "SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_SET_NAME
                FROM INFORMATION_SCHEMA.COLUMNS
                WHERE TABLE_SCHEMA = DATABASE()
                AND TABLE_NAME = %s
                AND DATA_TYPE IN ('varchar', 'text', 'tinytext', 'mediumtext', 'longtext', 'char', 'enum')
                AND CHARACTER_SET_NAME IS NOT NULL",
                $table
            ),
            ARRAY_A
        );

        $valid_columns = [];
        foreach ($columns as $col) {
            // Exclude serialized meta fields unless they are serialized
            if (strpos($col['COLUMN_NAME'], 'meta_value') === false) {
                $valid_columns[] = $col['COLUMN_NAME'];
            } else {
                if ($this->is_serialized_column($col)) {
                    $valid_columns[] = $col['COLUMN_NAME'];
                }
            }
        }

        return $valid_columns;
    }

    /**
     * Check if a column is likely to contain serialized data.
     *
     * @since 1.0.0
     * @param array $column_info Column information from database schema
     * @return bool Whether the column likely contains serialized data
     */
    private function is_serialized_column($column_info)
    {
        // Smart serialization detection
        return ($column_info['DATA_TYPE'] === 'varchar' && $column_info['CHARACTER_SET_NAME'] === null)
            || in_array($column_info['DATA_TYPE'], ['text', 'longtext']);
    }

    /**
     * Check if data is serialized PHP data.
     *
     * Uses multiple checks to reliably detect serialized data,
     * including pattern matching and unserialization verification.
     *
     * @since 1.0.0
     * @param mixed $data The data to check
     * @return bool Whether the data is serialized
     */
    private function is_serialized_data($data)
    {
        // First check if the data is a string
        if (!is_string($data)) {
            return false;
        }

        $data = trim($data);

        // Check for serialized data pattern
        if (preg_match('/^([adObis]:|N;)/', $data)) {
            // Verify serialization integrity with strict checking
            try {
                $unserialized = unserialize($data, ['allowed_classes' => false]);

                // Additional check to prevent false positives
                if ($unserialized === false && $data !== 'b:0;') {
                    return false;
                }
                return true;
            } catch (Exception $e) {
                error_log(esc_html__('Serialization error: ', 'surflink') . esc_html($e->getMessage()));
                return false;
            }
        }

        return false;
    }


    /**
     * Perform deep replacement in complex data structures.
     *
     * Recursively replaces search string with replacement in arrays and objects.
     *
     * @since 1.0.0
     * @param string $search     The string to search for
     * @param string $replace    The replacement string
     * @param mixed  $data       The data structure to perform replacement on
     * @param bool   $insensitive Whether the search should be case insensitive
     * @return mixed The data structure with replacements made
     */
    private function deep_replace($search, $replace, $data, $insensitive = true)
    {
        if (is_array($data)) {
            foreach ($data as $key => $value) {
                $data[$key] = $this->deep_replace($search, $replace, $value, $insensitive);
            }
        } elseif (is_object($data)) {
            if ($data instanceof \__PHP_Incomplete_Class) {
                // Skip incomplete objects
                return $data;
            }
            foreach ($data as $key => $value) {
                $data->$key = $this->deep_replace($search, $replace, $value, $insensitive);
            }
        } else {
            $data = $insensitive
                ? str_ireplace($search, $replace, (string) $data)
                : str_replace($search, $replace, (string) $data);
        }
        return $data;
    }

    /**
     * Count occurrences of a search string in data.
     *
     * Recursively counts occurrences in arrays, objects, and scalar values.
     * Handles case sensitivity based on the class property.
     *
     * @since 1.0.0
     * @param string $search The string to search for
     * @param mixed $data    The data to search in (can be array, object, or scalar)
     * @return int           Number of occurrences found
     */
    private function count_occurrences($search, $data)
    {
        if (is_array($data)) {
            $count = 0;
            foreach ($data as $item) {
                $count += $this->count_occurrences($search, $item);
            }
            return $count;
        } elseif (is_object($data)) {
            $count = 0;
            foreach (get_object_vars($data) as $item) {
                $count += $this->count_occurrences($search, $item);
            }
            return $count;
        }
        return $this->case_insensitive
            ? substr_count(strtolower((string) $data), strtolower($search))
            : substr_count((string) $data, $search);
    }

    /**
     * Check if a table exists in the database.
     *
     * @since 1.0.0
     * @param string $table The table name to check
     * @return bool Whether the table exists
     */
    private function table_exists($table)
    {
        return $this->wpdb->get_var(
            $this->wpdb->prepare(
                "SHOW TABLES LIKE %s",
                $table
            )
        ) === $table;
    }


    /**
     * Free memory during batch processing.
     *
     * Performs garbage collection and memory optimization during
     * long-running operations to prevent memory exhaustion.
     *
     * @since 1.0.0
     */
    private function free_memory()
    {
        // Aggressive memory cleanup
        if (function_exists('gc_mem_caches')) {
            gc_mem_caches();
        }
        gc_collect_cycles();

        // Reset WordPress query log
        if (defined('SAVEQUERIES') && SAVEQUERIES) {
            $this->wpdb->queries = [];
        }

        // Adjust batch size based on memory usage
        $this->batch_size = max(100, min($this->batch_size, 1000));
        if (memory_get_usage(true) > 64 * 1024 * 1024) {
            $this->batch_size = floor($this->batch_size * 0.8);
        }
    }

    /**
     * Render the main search and replace admin interface.
     *
     * Creates and displays the form for standard search and replace operations
     * across database tables, with options for dry run and case sensitivity.
     *
     * @since 1.0.0
     */
    public function render_ui()
    {
        if (!current_user_can('manage_options')) {
            wp_die('Access denied');
        }

        // Process the form submission
        if (isset($_POST['process']) && check_admin_referer('hyperdb_replace')) {
            $search = sanitize_text_field($_POST['search']);
            $replace = sanitize_text_field($_POST['replace']);
            $tables = isset($_POST['tables']) ? array_map('sanitize_text_field', $_POST['tables']) : [];
            $valid_tables = $this->get_valid_tables($tables);
            $dry_run = isset($_POST['dry_run']) && $_POST['dry_run'] == '1';
            $replace_guid = isset($_POST['replace_guid']) && $_POST['replace_guid'] == '1';
            $case_insensitive = isset($_POST['case_insensitive']) && $_POST['case_insensitive'] == '1';

            $start_time = microtime(true);
            $reports = $this->process_replace($search, $replace, $valid_tables, $dry_run, $replace_guid, $case_insensitive);
            $elapsed_time = round(microtime(true) - $start_time, 4);

            if (empty($reports['errors'])) {
                $msg = $dry_run
                    ? "Dry run completed in {$elapsed_time} seconds. No changes were made."
                    : "Operation completed in {$elapsed_time} seconds.";
                $reports['success'][] = $msg;
            }

            $reports['dry_run'] = $dry_run;
            $this->reports = $reports;
        }

        require_once SURFL_PATH . 'templates/surf-link-sr-report.php';
        ?>

        <form method="post" action="">
            <?php wp_nonce_field('hyperdb_replace'); ?>

            <!-- Grid container for Group 1 and Group 2 -->
            <div class="surfl-grid">
                <!-- Group 1: Search, Replace &amp; Tables -->
                <div class="postbox surfl-group surfl-search-group">
                    <h2 class="hndle"><span><?php esc_html_e('Search &amp; Replace Fields', 'surflink'); ?></span></h2>
                    <div class="surfl-sr-fields inside">


                        <label for="search"><?php esc_html_e('Search Query', 'surflink'); ?></label>

                        <input type="text" id="search" name="search" class="regular-text"
                            value="<?php echo isset($_POST['search']) ? esc_attr(sanitize_text_field($_POST['search'])) : ''; ?>"
                            required />
                        <p class="description">
                            <?php esc_html_e('Enter the string you want to search for.', 'surflink'); ?>
                        </p>


                        <label for="replace"><?php esc_html_e('Replace With', 'surflink'); ?></label>

                        <input type="text" id="replace" name="replace" class="regular-text"
                            value="<?php echo isset($_POST['replace']) ? esc_attr(sanitize_text_field($_POST['replace'])) : ''; ?>"
                            required />
                        <p class="description"><?php esc_html_e('Enter the replacement text.', 'surflink'); ?></p>


                        <label for="tables"><?php esc_html_e('Tables', 'surflink'); ?></label>

                        <?php
                        global $wpdb;
                        $all_tables = $wpdb->get_results("SHOW TABLES", ARRAY_N);
                        $all_table_statuses = $wpdb->get_results("SHOW TABLE STATUS", ARRAY_A);
                        $table_sizes = [];
                        if ($all_table_statuses) {
                            foreach ($all_table_statuses as $status) {
                                $table_name = $status['Name'];
                                $size_bytes = $status['Data_length'] + $status['Index_length'];
                                $size_mb = number_format($size_bytes / (1024 * 1024), 2);
                                $table_sizes[$table_name] = $size_mb;
                            }
                        }

                        if (SURFL_IS_MULTISITE) {
                            if (SURFL_IS_MAIN_SITE) {
                                $base_prefix = $wpdb->base_prefix;
                                $all_tables = array_filter($all_tables, function ($table_row) use ($base_prefix) {
                                    return (bool) preg_match('/^' . preg_quote($base_prefix, '/') . '(?!\d)/', $table_row[0]);
                                });
                            } else {
                                $prefix = $wpdb->prefix;
                                $all_tables = array_filter($all_tables, function ($table_row) use ($prefix) {
                                    return strpos($table_row[0], $prefix) === 0;
                                });
                            }
                        }
                        ?>
                        <select name="tables[]" id="tables" multiple="multiple" style="width:100%;" required>
                            <?php foreach ($all_tables as $table): ?>
                                <?php
                                $table_name = $table[0];
                                $selected = (isset($_POST['tables']) && in_array($table_name, $_POST['tables'])) ? 'selected="selected"' : '';
                                ?>
                                <option value="<?php echo esc_attr($table_name); ?>" <?php echo $selected; ?>>
                                    <?php echo esc_html($table_name); ?>
                                    (<?php echo isset($table_sizes[$table_name]) ? $table_sizes[$table_name] : '0.00'; ?>
                                    MB)
                                </option>
                            <?php endforeach; ?>
                        </select>
                        <p class="description">
                            <?php esc_html_e('Select ( <span class="surfl-key">Ctrl</span>+<span class="surfl-key">Click</span> ) one or more tables for the search &amp; replace operation.', 'surflink'); ?>
                        </p>

                    </div>
                    <div class="inside">
                        <?php submit_button(__('Run Search & Replace', 'surflink'), 'primary', 'process', true, 'style="background: #007cba;color: white;padding: 6px 16px;border-radius: 20px;font-size: 12px;font-weight: 600;position: relative;left: 64%;"'); ?>
                    </div>
                </div>

                <!-- Group 2: Options for Dry Run, Replace GUID, and Case Insensitive -->
                <div class="postbox surfl-group surfl-options-group">
                    <h2 class="hndle"><span><?php esc_html_e('Options', 'surflink'); ?></span></h2>
                    <div class="inside">
                        <table class="form-table surfl-table">
                            <tr>
                                <th scope="row">
                                    <label for="dry_run"><?php esc_html_e('Dry Run', 'surflink'); ?></label>
                                </th>
                                <td>
                                    <input type="checkbox" id="dry_run" name="dry_run" value="1" <?php checked(!isset($_POST['process']) || (isset($_POST['dry_run']) && $_POST['dry_run'] == '1')); ?> />
                                    <p class="description">
                                        <?php esc_html_e('Checked by default. Uncheck to perform actual updates.', 'surflink'); ?>
                                    </p>
                                </td>
                            </tr>
                            <tr>
                                <th scope="row">
                                    <label for="replace_guid"><?php esc_html_e('Replace GUID', 'surflink'); ?></label>
                                </th>
                                <td>
                                    <input type="checkbox" id="replace_guid" name="replace_guid" value="1" <?php checked(isset($_POST['replace_guid']) && $_POST['replace_guid'] == '1'); ?> />
                                    <p class="description">
                                        <?php esc_html_e('Replace GUID values (use with caution).', 'surflink'); ?>
                                    </p>
                                </td>
                            </tr>
                            <tr>
                                <th scope="row">
                                    <label for="case_insensitive"><?php esc_html_e('Case Insensitive', 'surflink'); ?></label>
                                </th>
                                <td>
                                    <input type="checkbox" id="case_insensitive" name="case_insensitive" value="1" <?php checked(isset($_POST['case_insensitive']) && $_POST['case_insensitive'] == '1'); ?> />
                                    <p class="description">
                                        <?php esc_html_e('Unchecked by default. Check to perform a case-insensitive search &amp; replace.', 'surflink'); ?>
                                    </p>
                                </td>
                            </tr>
                        </table>
                    </div>
                </div>
            </div>


        </form>
        </div>
        </div>

        <?php
    }

    /**
     * Get valid table names from user input.
     *
     * Validates and filters the table names to ensure they exist and are safe.
     *
     * @since 1.0.0
     * @param array $tables Array of table names to validate
     * @return array Filtered array of valid table names
     */
    private function get_valid_tables($tables)
    {
        $valid_tables = [];
        foreach ($tables as $table) {
            // Validate table name to allow only letters, numbers, and underscores
            if (!preg_match('/^[A-Za-z0-9_]+$/', $table)) {
                continue;
            }
            if ($this->table_exists($table)) {
                $valid_tables[] = $table;
            }
        }
        return $valid_tables;
    }

    /**
     * Process URL-specific search and replace operations.
     *
     * Handles targeted search and replace operations specifically for URLs
     * across different parts of the WordPress database based on predefined tasks.
     *
     * @since 1.0.0
     * @param string $search           The URL string to search for
     * @param string $replace          The replacement URL string
     * @param array  $tasks            Array of task definitions
     * @param bool   $dry_run          Whether this is a dry run (no actual changes)
     * @param bool   $case_insensitive Whether the search should be case insensitive
     * @return array                   Operation report
     */
    public function process_url_replace($search, $replace, $tasks, $dry_run = false, $case_insensitive = false)
    {
        $this->case_insensitive = $case_insensitive;
        $this->report_details = [];

        error_log('search: ' . $search);
        error_log('replace: ' . $replace);
        error_log('dry_run: ' . ($dry_run ? 'true' : 'false'));
        error_log('case_insensitive: ' . ($case_insensitive ? 'true' : 'false'));


        try {
            foreach ($tasks as $task) {
                $table = $task['table'];
                $column = $task['column'];
                $where = $task['where'];

                if (!$this->table_exists($table)) {
                    continue;
                }

                $primary_key = $this->get_primary_key($table);

                if (!$this->column_exists($table, $column)) {
                    continue;
                }

                if (!isset($this->report_details[$table])) {
                    $this->report_details[$table] = [
                        'rows_scanned' => 0,
                        'time' => 0,
                        'columns' => [],
                    ];
                }
                if (!isset($this->report_details[$table]['columns'][$column])) {
                    $this->report_details[$table]['columns'][$column] = [
                        'occurrences' => 0,

                    ];
                }

                $table_start = microtime(true);
                $offset = 0;

                while (true) {
                    $query = "SELECT $primary_key, $column FROM $table";
                    $args = array();

                    if (!empty($where)) {
                        $query .= " WHERE $where";
                        $args[] = 'attachment';
                    }
                    $query .= " LIMIT %d, %d";
                    $args[] = $offset;
                    $args[] = $this->batch_size;

                    error_log('query: ' . $query);
                    // Prepare and execute the query safely


                    $rows = $this->wpdb->get_results($this->wpdb->prepare($query, $args), ARRAY_A);

                    if (empty($rows)) {
                        break;
                    }

                    $this->report_details[$table]['rows_scanned'] += count($rows);
                    $this->process_task_batch($table, $primary_key, $column, $rows, $search, $replace, $dry_run);
                    $offset += $this->batch_size;
                    $this->free_memory();
                }

                $this->report_details[$table]['time'] += microtime(true) - $table_start;
            }
        } catch (Exception $e) {
            error_log('HyperDBReplace Error: ' . $e->getMessage());
            $this->reports['errors'][] = $e->getMessage();
        }

        $this->reports['details'] = $this->report_details;

        if (isset($this->reports['success'])) {
            $this->reports['success'] = array_filter($this->reports['success'], function ($msg) {
                return trim($msg) !== '';
            });
        }
        if (isset($this->reports['errors'])) {
            $this->reports['errors'] = array_filter($this->reports['errors'], function ($msg) {
                return trim($msg) !== '';
            });
        }

        return $this->reports;
    }

    /**
     * Generate task definitions based on user-selected categories.
     *
     * Creates an array of task definitions for the URL replacement operation
     * based on predefined categories like content, attachments, links, etc.
     *
     * @since 1.0.0
     * @param array $categories Selected categories for URL replacement
     * @return array Array of task definitions
     */
    private function generate_tasks($categories)
    {
        global $wpdb;
        $tasks = [];

        foreach ($categories as $category) {
            switch ($category) {
                case 'contents':
                    $tasks[] = [
                        'table' => $wpdb->posts,
                        'column' => 'post_content',
                        'where' => '',
                    ];
                    break;
                case 'attachments':
                    $tasks[] = [
                        'table' => $wpdb->posts,
                        'column' => 'guid',
                        'where' => "post_type = %s",
                    ];
                    break;
                case 'links':
                    $tasks[] = [
                        'table' => $wpdb->links,
                        'column' => 'link_url',
                        'where' => '',
                    ];
                    break;
                case 'custom': // New Custom category
                    $tasks[] = [
                        'table' => $wpdb->postmeta,
                        'column' => 'meta_value',
                        'where' => '',
                    ];
                    break;
                case 'guids': // New GUIDs category
                    $tasks[] = [
                        'table' => $wpdb->posts,
                        'column' => 'guid',
                        'where' => '',
                    ];
                    break;
            }
        }

        $valid_tasks = [];
        foreach ($tasks as $task) {
            if ($this->table_exists($task['table'])) {
                $valid_tasks[] = $task;
            }
        }

        return $valid_tasks;
    }

    /**
     * Check if a column exists in a given table.
     *
     * @since 1.0.0
     * @param string $table  The database table name
     * @param string $column The column name to check
     * @return bool Whether the column exists
     */
    private function column_exists($table, $column)
    {
        $result = $this->wpdb->get_row(
            $this->wpdb->prepare(
                "SELECT COLUMN_NAME
                FROM INFORMATION_SCHEMA.COLUMNS
                WHERE TABLE_SCHEMA = DATABASE()
                AND TABLE_NAME = %s
                AND COLUMN_NAME = %s",
                $table,
                $column
            )
        );
        return !is_null($result);
    }

    /**
     * Process a batch of database rows for a specific URL replace task.
     *
     * Similar to process_batch() but optimized for single-column operations
     * in URL replacement tasks.
     *
     * @since 1.0.0
     * @param string $table       The database table name
     * @param string $primary_key The primary key column name
     * @param string $column      The column to perform replacements on
     * @param array  $rows        The database rows to process
     * @param string $search      The URL string to search for
     * @param string $replace     The replacement URL string
     * @param bool   $dry_run     Whether this is a dry run (no actual changes)
     * @throws Exception If update fails
     */
    private function process_task_batch($table, $primary_key, $column, $rows, $search, $replace, $dry_run)
    {
        $case_statements = [];
        $ids = [];

        foreach ($rows as $row) {
            if (!isset($row[$primary_key])) {
                throw new Exception("Primary key $primary_key not found in table $table");
            }

            $original = $row[$column];
            $modified = $this->replace_value($original, $search, $replace);

            if ($original !== $modified) {
                $occ = $this->count_occurrences($search, $original);
                $this->report_details[$table]['columns'][$column]['occurrences'] += $occ;


                if (!$dry_run) {
                    $case_statements[] = $this->wpdb->prepare(
                        "WHEN %d THEN %s",
                        $row[$primary_key],
                        $modified
                    );
                }
                $ids[] = $row[$primary_key];
            }
        }

        if (!empty($case_statements)) {
            if (!$dry_run) {
                $this->wpdb->query('START TRANSACTION');

                $_table = $this->sanitize_identifier($table);
                $_primary_key = $this->sanitize_identifier($primary_key);
                $_column = $this->sanitize_identifier($column);

                $sql = "UPDATE $_table SET $_column = CASE $_primary_key 
                        " . implode(' ', $case_statements) . " 
                        END WHERE $_primary_key IN (" . implode(',', array_unique($ids)) . ")";

                $result = $this->wpdb->query($sql);
                if ($result === false) {
                    $this->wpdb->query('ROLLBACK');
                    throw new Exception("Update failed for column $column in table $table: " . $this->wpdb->last_error);
                }

                $this->wpdb->query('COMMIT');
            }
            $this->reports[$table] = ($this->reports[$table] ?? 0) + count($ids);
        }
    }

    /**
     * Render the URL-specific search and replace admin interface.
     *
     * Creates and displays the form for URL-specific search and replace operations
     * with predefined categories like content, attachments, links, etc.
     *
     * @since 1.0.0
     */
    public function render_url_replace_ui()
    {
        if (!current_user_can('manage_options')) {
            wp_die('Access denied');
        }

        if (isset($_POST['process']) && check_admin_referer('hyperdb_replace')) {
            $search = sanitize_text_field($_POST['search']);
            $replace = sanitize_text_field($_POST['replace']);
            $categories = isset($_POST['categories']) ? array_map('sanitize_text_field', $_POST['categories']) : [];
            $tasks = $this->generate_tasks($categories);
            $dry_run = isset($_POST['dry_run']) && $_POST['dry_run'] == '1';
            $case_insensitive = isset($_POST['case_insensitive']) && $_POST['case_insensitive'] == '1';

            $start_time = microtime(true);
            $reports = $this->process_url_replace($search, $replace, $tasks, $dry_run, $case_insensitive);
            $elapsed_time = round(microtime(true) - $start_time, 4);

            if (empty($reports['errors'])) {
                $msg = $dry_run
                    ? "Dry run completed in {$elapsed_time} seconds. No changes were made."
                    : "Operation completed in {$elapsed_time} seconds.";
                $reports['success'][] = $msg;
            }
            $reports['dry_run'] = $dry_run;
            $this->reports = $reports;
        }

        require_once SURFL_PATH . 'templates/surf-link-sr-report.php';
        ?>
        <form method="post" action="">
            <?php wp_nonce_field('hyperdb_replace'); ?>
            <div class="surfl-grid">
                <div class="postbox surfl-group surfl-search-group">
                    <h2 class="hndle"><span><?php esc_html_e('Easy Url Update', 'surflink'); ?></span></h2>
                    <div class="inside surfl-sr-fields">
                        <label for="search"><?php esc_html_e('OLD URL', 'surflink'); ?></label>

                        <input type="text" id="search" name="search" class="regular-text" required />
                        <label for="replace"><?php esc_html_e('NEW URL', 'surflink'); ?></label>

                        <input type="text" id="replace" name="replace" class="regular-text" required />

                        <label><?php esc_html_e('Categories', 'surflink'); ?></label>

                        <div class="surfl-checkbox-group">
                            <div class="surfl-checkbox-option">
                                <input type="checkbox" id="cat-contents" name="categories[]" value="contents" />
                                <div>
                                    <label for="cat-contents"><?php esc_html_e('Contents', 'surflink'); ?></label>
                                    <p class="description">
                                        <?php esc_html_e('Search in contents <br> (Posts, Pages, Custom Post Types, Revisions.)', 'surflink'); ?>
                                    </p>
                                </div>
                            </div>

                            <div class="surfl-checkbox-option">
                                <input type="checkbox" id="cat-attachments" name="categories[]" value="attachments" />
                                <div>
                                    <label for="cat-attachments"><?php esc_html_e('Attachments', 'surflink'); ?></label>
                                    <p class="description">
                                        <?php esc_html_e('Search in attachments (images, documents, general media)', 'surflink'); ?>
                                    </p>
                                </div>
                            </div>

                            <div class="surfl-checkbox-option">
                                <input type="checkbox" id="cat-links" name="categories[]" value="links" />
                                <div>
                                    <label for="cat-links"><?php esc_html_e('Links', 'surflink'); ?></label>
                                    <p class="description"><?php esc_html_e('Search in links URL', 'surflink'); ?></p>
                                </div>
                            </div>

                            <div class="surfl-checkbox-option">
                                <input type="checkbox" id="cat-custom" name="categories[]" value="custom" />
                                <div>
                                    <label
                                        for="cat-custom"><?php esc_html_e('Custom (post meta values)', 'surflink'); ?></label>
                                    <p class="description">
                                        <?php esc_html_e('Search in custom fields and meta boxes', 'surflink'); ?>
                                    </p>
                                </div>
                            </div>

                            <div class="surfl-checkbox-option">
                                <input type="checkbox" id="cat-guids" name="categories[]" value="guids" />
                                <div>
                                    <label for="cat-guids"><?php esc_html_e('GUIDs', 'surflink'); ?></label>
                                    <p class="description">
                                        <?php esc_html_e('Search in Post GUIDs (use with caution).', 'surflink'); ?>
                                    </p>
                                </div>
                            </div>
                        </div>

                        <?php submit_button(__('Run Search & Replace', 'surflink'), 'primary', 'process', true); ?>
                    </div>
                </div>

                <div class="postbox surfl-group surfl-options-group">
                    <h2 class="hndle"><span><?php esc_html_e('Options', 'surflink'); ?></span></h2>
                    <div class="inside">
                        <table class="form-table surfl-table">
                            <tr>
                                <th scope="row"><label for="dry_run"><?php esc_html_e('Dry Run', 'surflink'); ?></label></th>
                                <td>
                                    <input type="checkbox" id="dry_run" name="dry_run" value="1" checked />
                                </td>
                            </tr>
                            <tr>
                                <th scope="row"><label
                                        for="case_insensitive"><?php esc_html_e('Case Insensitive', 'surflink'); ?></label></th>
                                <td>
                                    <input type="checkbox" id="case_insensitive" name="case_insensitive" value="1" />
                                </td>
                            </tr>
                        </table>
                    </div>
                </div>
            </div>
        </form>
        </div>
        </div>
        <?php
    }
}

?>