# metasync/2.7.2/includes/class-metasync-redirections-admin.php

Search Atlas SEO – OTTO AI SEO Automation for WordPress, version 2.7.2. 796 lines.

- Page: https://pluginprobe.com/plugins/metasync/2.7.2/code/includes/class-metasync-redirections-admin.php
- Raw: https://pluginprobe.com/plugins/metasync/2.7.2/raw/includes/class-metasync-redirections-admin.php
- Modified: 2026-10-07T12:05:24+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/metasync/2.7.2/code/includes/class-metasync-redirections-admin.php#L10-L20`.

```php
<?php
if (!defined('ABSPATH')) {
	exit;
}

/**
 * Redirections admin page logic extracted from Metasync_Admin.
 *
 * Handles the tabbed redirections / 404-monitor UI, form processing,
 * validation, and related database checks.
 *
 * @package    Metasync
 * @subpackage Metasync/includes
 */
class Metasync_Redirections_Admin
{
    private static $instance = null;

    /**
     * Which tab of the combined Redirections screen is being viewed.
     *
     * Set ONLY by render_tab_content(), i.e. only when both the redirections
     * and 404-monitor tables are rendered onto one page. Stays null on the
     * standalone 404 Monitor page (page=...-404-monitor), where that table is
     * the only one present. The list tables read it to decide whether to offer
     * bulk actions - see get_bulk_actions() in either table for why.
     *
     * @var string|null 'redirections', '404-monitor', or null when not on the
     *                  combined screen.
     */
    public static $combined_screen_active_tab = null;

    /** @var object Redirection database helper */
    private $db_redirection;

    /** @var Metasync_Admin Back-reference used for shared UI helpers */
    private $admin;

    private function __construct($db_redirection, $admin)
    {
        $this->db_redirection = $db_redirection;
        $this->admin          = $admin;
    }

    /**
     * @param object|null       $db_redirection  Required on first call.
     * @param Metasync_Admin|null $admin          Required on first call.
     */
    public static function get_instance($db_redirection = null, $admin = null)
    {
        if (self::$instance === null) {
            self::$instance = new self($db_redirection, $admin);
        }
        return self::$instance;
    }

    /* ------------------------------------------------------------------
     *  Public entry points (called from Metasync_Admin delegation stubs)
     * ------------------------------------------------------------------ */

    public function create_admin_redirections_page()
    {
        $this->handle_redirection_form_processing();

        $this->check_database_structure();

        $this->ensure_404_monitor_table();

        $current_tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : 'redirections'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab selection

        $this->add_tabbed_interface_assets();
        $this->admin->render_layout_open('Redirections', 'redirections', 'Manage URL redirects and monitor 404 errors on your site.');
        $this->render_tab_navigation($current_tab);
        $this->render_tab_content($current_tab);
        $this->admin->render_layout_close();
    }

    public function display_redirection_messages()
    {
        $uid = get_current_user_id();

        if ($error = get_transient('metasync_redirection_error_' . $uid)) {
            echo '<div class="notice notice-error is-dismissible"><p>' . esc_html($error) . '</p></div>';
            delete_transient('metasync_redirection_error_' . $uid);
        }

        if ($success = get_transient('metasync_redirection_success_' . $uid)) {
            echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($success) . '</p></div>';
            delete_transient('metasync_redirection_success_' . $uid);
        }

        if ($warning = get_transient('metasync_redirection_warning_' . $uid)) {
            echo '<div class="notice notice-warning is-dismissible"><p>' . esc_html($warning) . '</p></div>';
            delete_transient('metasync_redirection_warning_' . $uid);
        }
    }

    /* ------------------------------------------------------------------
     *  Private helpers
     * ------------------------------------------------------------------ */

    private function safe_redirect($url)
    {
        if (!headers_sent()) {
            wp_safe_redirect($url);
            exit;
        } else {
            echo '<script type="text/javascript">window.location.href = "' . esc_url($url) . '";</script>';
            echo '<noscript><meta http-equiv="refresh" content="0;url=' . esc_url($url) . '"></noscript>';
            exit;
        }
    }

    private function handle_redirection_form_processing()
    {
        if (!isset($_POST['submit'])) {
            return;
        }

        $uid = get_current_user_id();

        $nonce_valid = false;
        
        if (isset($_POST['metasync_redirection_nonce']) && wp_verify_nonce($_POST['metasync_redirection_nonce'], 'metasync_redirection_form')) {
            $nonce_valid = true;
        }
        elseif (isset($_POST['_wpnonce']) && wp_verify_nonce($_POST['_wpnonce'], 'metasync_redirection_form')) {
            $nonce_valid = true;
        }
        
        if (!$nonce_valid) {
            wp_die('Security check failed. Please refresh and try again.');
        }
        
        if (!Metasync::current_user_has_plugin_access()) {
            wp_die('Insufficient permissions.');
        }

        // Unslash before sanitizing: source URLs with apostrophes/backslashes
        // must be stored in their literal form so the front-end matcher (which
        // compares against the unslashed request URI) can hit them.
        $source_urls = isset($_POST['source_url']) ? array_map('sanitize_text_field', wp_unslash($_POST['source_url'])) : [];
        $search_types = isset($_POST['search_type']) ? array_map('sanitize_text_field', wp_unslash($_POST['search_type'])) : [];
        $destination_url = isset($_POST['destination_url']) ? sanitize_text_field(wp_unslash($_POST['destination_url'])) : '';
        $redirect_type = isset($_POST['redirect_type']) ? intval($_POST['redirect_type']) : 301;
        $status = isset($_POST['status']) ? sanitize_text_field(wp_unslash($_POST['status'])) : 'active';
        $regex_pattern = isset($_POST['regex_pattern']) ? wp_unslash(trim($_POST['regex_pattern'])) : '';
        $description = isset($_POST['description']) ? sanitize_text_field(wp_unslash($_POST['description'])) : '';
        $redirect_id = isset($_POST['redirect_id']) ? intval($_POST['redirect_id']) : 0;

        $validation_errors = [];

        if (empty($source_urls)) {
            $validation_errors[] = 'Please enter at least one source URL.';
        } else {
            $processed_sources = [];
            $empty_count = 0;

            foreach ($source_urls as $source_url) {
                $trimmed_url = trim($source_url);

                if (!empty($trimmed_url)) {
                    if (!$this->is_valid_url($trimmed_url)) {
                        $validation_errors[] = 'Invalid source URL format: "' . esc_html($trimmed_url) . '". URLs should start with / for relative paths or be complete URLs.';
                    }

                    if (in_array($trimmed_url, $processed_sources)) {
                        $validation_errors[] = 'Duplicate source URL detected: "' . esc_html($trimmed_url) . '".';
                    } else {
                        $processed_sources[] = $trimmed_url;
                    }
                } else {
                    $empty_count++;
                }
            }

            if ($empty_count === count($source_urls)) {
                $validation_errors[] = 'All source URL fields are empty. Please enter at least one source URL.';
            }
        }

        $allowed_redirect_types = [301, 302, 307, 410, 451];
        if (!in_array($redirect_type, $allowed_redirect_types)) {
            $validation_errors[] = 'Invalid redirection type selected.';
        }

        if (!in_array($redirect_type, [410, 451])) {
            $trimmed_dest = trim($destination_url);
            if (empty($trimmed_dest)) {
                $validation_errors[] = 'Destination URL is required for this redirect type.';
            } elseif (!$this->is_valid_url($trimmed_dest)) {
                $validation_errors[] = 'Invalid destination URL format. URLs should start with / for relative paths or be complete URLs.';
            } elseif (!Metasync_Redirection_Validator::is_safe_destination_syntax($trimmed_dest)) {
                // Browsers treat backslashes as path separators, so '/\evil.com'
                // passes wp_validate_redirect yet navigates off-site; a leading
                // '//' is a protocol-relative host. Neither is a valid internal
                // destination regardless of the external-redirects setting.
                $validation_errors[] = 'Destination URL contains invalid characters.';
            } elseif (!get_option('metasync_allow_external_redirects', 0) && wp_validate_redirect($trimmed_dest, '') !== $trimmed_dest) {
                $validation_errors[] = 'Destination URL must point to this site. External redirect destinations are not allowed. Enable "Allow External Redirects" in the Redirections settings to permit off-site redirects.';
            }
        }

        $allowed_statuses = ['active', 'inactive'];
        if (!in_array($status, $allowed_statuses)) {
            $validation_errors[] = 'Invalid status selected.';
        }

        if (!empty($validation_errors)) {
            $error_message = implode(' ', $validation_errors);
            set_transient('metasync_redirection_error_' . $uid, $error_message, 45);
            $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
        }

        $sources_from = [];
        foreach ($source_urls as $index => $source_url) {
            $trimmed_url = trim($source_url);
            if (!empty($trimmed_url)) {
                $search_type = isset($search_types[$index]) ? $search_types[$index] : 'exact';
                $sources_from[$trimmed_url] = $search_type;
            }
        }

        $pattern_type = 'exact';
        // Derive the global type only from populated rows. A blank placeholder
        // with a changed select must not trigger regex validation or alter the
        // behavior of a redirect the user actually entered.
        foreach ($sources_from as $search_type) {
            if (!empty($search_type)) {
                $pattern_type = $search_type;
                break;
            }
        }

        if ($pattern_type === 'regex') {
            if (empty($regex_pattern)) {
                set_transient('metasync_redirection_error_' . $uid, 'Please enter a regex pattern when using "Regex Pattern" as the pattern type.', 45);
                $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
                return;
            }

            // Limit pattern length to prevent ReDoS via catastrophic backtracking
            if (strlen($regex_pattern) > 500) {
                set_transient('metasync_redirection_error_' . $uid, 'Regex pattern is too long (max 500 characters).', 45);
                $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
                return;
            }

            // Reject patterns with nested quantification that could cause ReDoS,
            // including alternation blowup ('((a|a)*)*') the older guard missed.
            $raw_check = preg_replace('/^\S(.*)\S[a-zA-Z]*$/', '$1', $regex_pattern);
            if (!Metasync_Redirection_Validator::is_regex_safe($raw_check)) {
                set_transient('metasync_redirection_error_' . $uid, 'Regex pattern is too long or contains potentially unsafe nested quantifiers (including nested alternation).', 45);
                $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
                return;
            }

            $test_pattern = $regex_pattern;
            $delimiter_chars = ['/', '#', '~', '%', '@'];
            $has_valid_delimiters = false;

            if (strlen($test_pattern) >= 2) {
                $first_char = $test_pattern[0];
                if (in_array($first_char, $delimiter_chars)) {
                    $last_pos = strrpos($test_pattern, $first_char);
                    if ($last_pos > 0) {
                        $has_valid_delimiters = true;
                    }
                }
            }

            if (!$has_valid_delimiters) {
                $test_pattern = '/' . $test_pattern . '/';
            }
            $is_valid = Metasync_Regex_Validator::is_valid($test_pattern);
            if ($is_valid === false) {
                $error_message = error_get_last();
                $error_text = isset($error_message['message']) ? $error_message['message'] : 'Unknown regex error';
                set_transient('metasync_redirection_error_' . $uid, 'Invalid Regex Pattern: ' . $error_text . ' Please fix the regex pattern and try again.', 45);
                $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
                return;
            }
        }

        // Loop detection: refuse to persist a chain that would resolve back to any source
        if (!in_array($redirect_type, [410, 451])) {
            require_once dirname(__FILE__, 2) . '/redirections/class-metasync-redirection.php';
            require_once dirname(__FILE__, 2) . '/redirections/class-metasync-redirection-validator.php';
            $redirection_helper = new Metasync_Redirection($this->db_redirection);
            foreach (array_keys($sources_from) as $source_url) {
                $loop_error = $redirection_helper->validate_no_loop($source_url, $destination_url);
                if ($loop_error !== null) {
                    set_transient('metasync_redirection_error_' . $uid, $loop_error, 45);
                    $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
                    return;
                }
            }

            // Non-blocking destination reachability check — surface a warning, never block the save.
            $dest_warning = $redirection_helper->destination_resolves_warning($destination_url, $redirect_type, $pattern_type);
            if ($dest_warning !== null) {
                set_transient('metasync_redirection_warning_' . $uid, $dest_warning, 45);
            }
        }

        // Non-blocking duplicate-source check: warn (but never block) when a
        // submitted source duplicates an existing ACTIVE rule's exact source.
        // Two active rows with the same normalized exact source path cannot
        // both fire — the engine's index only keeps one — so saving a second
        // active rule for a source that already has one creates a dead row.
        // Exclude the row being edited so an edit-and-re-save does not warn
        // against itself. An inactive submission cannot shadow (the engine's
        // index only holds active rows), so skip the check in that case.
        if ($status === 'active') {
            $duplicate_warnings = $this->check_duplicate_active_sources($sources_from, $pattern_type, $redirect_id);
            if ($duplicate_warnings !== '') {
                set_transient('metasync_redirection_warning_' . $uid, $duplicate_warnings, 45);
            }
        }

        $data = [
            'sources_from' => serialize($sources_from),
            'url_redirect_to' => $destination_url,
            'http_code' => $redirect_type,
            'status' => $status,
            'pattern_type' => $pattern_type,
            'regex_pattern' => $regex_pattern,
            'description' => $description,
        ];

        try {
            if ($redirect_id > 0) {
                $result = $this->db_redirection->update($data, $redirect_id);
                if ($result === false) {
                    throw new Exception('Failed to update redirection');
                }
                $message = 'Redirection updated successfully.';
            } else {
                $result = $this->db_redirection->add($data);
                if ($result === false) {
                    throw new Exception('Failed to add redirection');
                }
                $message = 'Redirection added successfully.';
            }
            
        } catch (Exception $e) {
            error_log('MetaSync error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
            set_transient('metasync_redirection_error_' . $uid, 'An error occurred while saving the redirection. Please try again.', 45);
            $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
        }

        set_transient('metasync_redirection_success_' . $uid, $message, 45);

        $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
    }

    private function is_valid_url($url)
    {
        if (strpos($url, '/') === 0) {
            return true;
        }

        return filter_var($url, FILTER_VALIDATE_URL) !== false;
    }

    /**
     * Non-blocking check for duplicate active exact sources.
     *
     * When a new (or edited) redirect saves an active exact-match source that
     * another active row already uses, only one of the two will ever fire —
     * the engine's exact-index keeps a single winner per source path. This
     * returns a human-readable warning string listing the colliding sources
     * (and the existing rule's destination) so the user knows the save
     * succeeded but created a shadowed rule. It never blocks the save.
     *
     * Normalization mirrors Metasync_Redirection::ensure_redirect_index()
     * exactly: extract the path from full URLs, force a leading slash, strip
     * a trailing slash, and treat '/' as the root. Only exact-pattern sources
     * are checked — wildcard/regex/contain/start/end sources do not collapse
     * onto a single index key.
     *
     * @param array  $sources_from  Submitted sources (url => pattern_type).
     * @param string $pattern_type  Global pattern type for the submission.
     * @param int    $redirect_id    ID of the row being edited (excluded from the check), 0 for new.
     * @return string  Warning message, or '' when no duplicate was found.
     */
    private function check_duplicate_active_sources(array $sources_from, $pattern_type, $redirect_id)
    {
        // Only exact-pattern submissions can shadow. If the global pattern type
        // is not exact (regex/wildcard/contain/start/end) the sources do not go
        // into the engine's exact index, so there is nothing to warn about.
        if ($pattern_type !== 'exact') {
            return '';
        }

        // Normalize the submitted sources into the same keys the engine uses.
        $submitted_norms = array();
        foreach (array_keys($sources_from) as $source_url) {
            $source_url = trim((string) $source_url);
            if ($source_url === '') {
                continue;
            }
            $norm = $source_url;
            if (strpos($norm, 'http') === 0) {
                $parsed = wp_parse_url($norm);
                $norm = isset($parsed['path']) ? $parsed['path'] : '/';
            }
            if ($norm === '' || $norm[0] !== '/') {
                $norm = '/' . $norm;
            }
            $norm = rtrim($norm, '/') ?: '/';
            $submitted_norms[$norm] = $source_url;
        }

        if (empty($submitted_norms)) {
            return '';
        }

        // Build an index of existing active rows' normalized exact sources,
        // excluding the row being edited so an edit does not warn against itself.
        $existing = array(); // norm => [ 'destination' => string ]
        $active_rows = $this->db_redirection->getAllActiveRecords();
        if (is_array($active_rows)) {
            foreach ($active_rows as $row) {
                if ($redirect_id > 0 && (int) $row->id === (int) $redirect_id) {
                    continue;
                }
                $sources = !empty($row->sources_from)
                    ? unserialize($row->sources_from, ['allowed_classes' => false])
                    : array();
                $source_urls = is_array($sources) ? $sources : array();
                $global_pattern_type = isset($row->pattern_type) ? $row->pattern_type : null;

                foreach ($source_urls as $source_key => $source_value) {
                    // Legacy list-format rows store the URL as the VALUE under a
                    // numeric key; remap so the legacy source is normalized.
                    if ((is_int($source_key) || ctype_digit((string) $source_key)) && is_string($source_value) && $source_value !== '') {
                        $source_key = $source_value;
                    }
                    $row_pattern_type = in_array($source_value, ['exact', 'contain', 'start', 'end', 'wildcard', 'regex'])
                        ? $source_value
                        : ($global_pattern_type ? $global_pattern_type : 'exact');

                    if ($row_pattern_type !== 'exact' || strpos((string) $source_key, '*') !== false) {
                        continue;
                    }

                    $norm = (string) $source_key;
                    if (strpos($norm, 'http') === 0) {
                        $parsed = wp_parse_url($norm);
                        $norm = isset($parsed['path']) ? $parsed['path'] : '/';
                    }
                    if ($norm === '' || $norm[0] !== '/') {
                        $norm = '/' . $norm;
                    }
                    $norm = rtrim($norm, '/') ?: '/';

                    $existing[$norm] = isset($row->url_redirect_to) ? (string) $row->url_redirect_to : '';
                }
            }
        }

        $collisions = array();
        foreach ($submitted_norms as $norm => $original_source) {
            if (isset($existing[$norm])) {
                $collisions[] = sprintf(
                    'Source "%s" is already used by an active redirect (destination: %s). Only one of the two rules will fire.',
                    $original_source,
                    $existing[$norm]
                );
            }
        }

        if (empty($collisions)) {
            return '';
        }

        return implode(' ', $collisions);
    }

    private function add_tabbed_interface_assets()
    {
        ?>
        <style>
        /* Root Variables - Dashboard Color Scheme */
        :root {
            --dashboard-bg: #0f1419;
            --dashboard-card-bg: #1a1f26;
            --dashboard-card-hover: #222831;
            --dashboard-text-primary: #ffffff;
            --dashboard-text-secondary: #9ca3af;
            --dashboard-accent: #3b82f6;
            --dashboard-accent-hover: #2563eb;
            --dashboard-success: #10b981;
            --dashboard-warning: #f59e0b;
            --dashboard-error: #ef4444;
            --dashboard-border: #374151;
            --dashboard-gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            --dashboard-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.1);
            --dashboard-shadow-hover: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -2px rgba(0, 0, 0, 0.2);
        }

        .metasync-tabs {
            margin: 20px 0;
            background: var(--dashboard-card-bg);
            border: 1px solid var(--dashboard-border);
            border-radius: 12px;
            padding: 6px;
            box-shadow: var(--dashboard-shadow);
        }
        
        .metasync-tab-nav {
            border-bottom: none;
            margin: 0;
            padding: 0;
            display: flex;
            gap: 4px;
            background: transparent;
        }
        
        .metasync-tab-nav li {
            display: inline-block;
            margin: 0;
            list-style: none;
        }
        
        .metasync-tab-nav a {
            display: block;
            padding: 12px 20px;
            text-decoration: none;
            color: var(--dashboard-text-secondary);
            border-bottom: none;
            border-radius: 8px;
            transition: all 0.3s ease;
            font-weight: 500;
            font-size: 14px;
            background: transparent;
            position: relative;
            overflow: hidden;
        }
        
        .metasync-tab-nav a:hover {
            color: var(--dashboard-text-primary);
            background: rgba(255, 255, 255, 0.05);
            transform: translateY(-1px);
        }
        
        .metasync-tab-nav a.active {
            color: var(--dashboard-text-primary);
            background: var(--dashboard-card-hover);
            border-bottom: none;
            box-shadow: var(--dashboard-shadow);
            transform: translateY(-1px);
            font-weight: 600;
        }
        
        .metasync-tab-nav a.active::after {
            content: '';
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            height: 2px;
            background: var(--dashboard-accent);
            border-radius: 1px;
        }
        
        .metasync-tab-content {
            display: none;
            padding: 20px 0;
        }
        
        .metasync-tab-content.active {
            display: block;
        }
        </style>
        
        <script>
        jQuery(document).ready(function($) {
            function switchToTab(targetTab) {
                $('.metasync-tab-nav a').removeClass('active');
                $('.metasync-tab-nav a[data-tab="' + targetTab + '"]').addClass('active');
                
                $('.metasync-tab-content').removeClass('active');
                $('#' + targetTab + '-content').addClass('active');
            }
            
            function initializeTabs() {
                var urlParams = new URLSearchParams(window.location.search);
                var currentTab = urlParams.get('tab');

                if (currentTab && (currentTab === 'redirections' || currentTab === '404-monitor')) {
                    switchToTab(currentTab);
                } else {
                    switchToTab('redirections');
                    currentTab = 'redirections';
                }

                var needsCleanup = false;
                if (currentTab === '404-monitor') {
                    if (urlParams.has('paged') || urlParams.has('paged_redir')) {
                        urlParams.delete('paged');
                        urlParams.delete('paged_redir');
                        needsCleanup = true;
                    }
                } else if (currentTab === 'redirections') {
                    if (urlParams.has('paged') || urlParams.has('paged_404')) {
                        urlParams.delete('paged');
                        urlParams.delete('paged_404');
                        needsCleanup = true;
                    }
                }

                if (needsCleanup) {
                    var newUrl = window.location.pathname + '?' + urlParams.toString();
                    window.history.replaceState({}, '', newUrl);
                }
            }

            initializeTabs();

            setTimeout(initializeTabs, 100);
            
            $('.metasync-tab-nav a').on('click', function(e) {
                e.preventDefault();

                var targetTab = $(this).data('tab');
                switchToTab(targetTab);

                var url = new URL(window.location);
                url.searchParams.set('tab', targetTab);

                url.searchParams.delete('paged');
                url.searchParams.delete('paged_404');
                url.searchParams.delete('paged_redir');

                window.history.pushState({}, '', url);
            });
        });
        </script>
        <?php
    }

    private function render_tab_navigation($current_tab)
    {
        $base_url = admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections');
        
        ?>
        <div class="metasync-tabs">
            <ul class="metasync-tab-nav">
                <li>
                    <a href="<?php echo esc_url($base_url . '&tab=redirections'); ?>" 
                       data-tab="redirections" 
                       class="<?php echo $current_tab === 'redirections' ? 'active' : ''; ?>">
                        Redirections
                    </a>
                </li>
                <li>
                    <a href="<?php echo esc_url($base_url . '&tab=404-monitor'); ?>" 
                       data-tab="404-monitor" 
                       class="<?php echo $current_tab === '404-monitor' ? 'active' : ''; ?>">
                        404 Monitor
                    </a>
                </li>
            </ul>
        </div>
        <?php
    }

    private function render_tab_content($current_tab)
    {
        // Both tables are rendered onto this one page (CSS hides the inactive
        // tab), so record which tab is actually being viewed. The list tables
        // use this to keep the hidden tab from emitting a duplicate set of
        // bulk-action controls - see get_bulk_actions() in either table.
        self::$combined_screen_active_tab = ($current_tab === '404-monitor')
            ? '404-monitor'
            : 'redirections';

        $this->render_redirections_tab($current_tab);
        $this->render_404_monitor_tab($current_tab);
    }

    private function render_redirections_tab($current_tab = null)
    {
        if ($current_tab === null) {
            $current_tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : 'redirections'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab selection
        }
        $active_class = $current_tab === 'redirections' ? 'active' : '';
        
        ?>
        <div id="redirections-content" class="metasync-tab-content <?php echo esc_attr($active_class); ?>">
            <?php
            $redirection = new Metasync_Redirection($this->db_redirection);
            $redirection->create_admin_redirection_interface();
            ?>
        </div>
        <?php
    }

    private function render_404_monitor_tab($current_tab = null)
    {
        if ($current_tab === null) {
            $current_tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : 'redirections'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab selection
        }
        $active_class = $current_tab === '404-monitor' ? 'active' : '';
        
        ?>
        <div id="404-monitor-content" class="metasync-tab-content <?php echo esc_attr($active_class); ?>">
            <?php
            try {
                require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor-database.php';
                require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor.php';
                
                $db_404 = new Metasync_Error_Monitor_Database();
                $ErrorMonitor = new Metasync_Error_Monitor($db_404);
                
                $ErrorMonitor->create_admin_plugin_interface();
                
            } catch (Exception $e) {
                error_log('MetaSync 404 Monitor Error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
                echo '<div class="notice notice-error"><p>An error occurred while loading the 404 monitor. Please check the server logs for details.</p></div>';
            } catch (Error $e) {
                error_log('MetaSync 404 Monitor Fatal Error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
                echo '<div class="notice notice-error"><p>A fatal error occurred while loading the 404 monitor. Please check the server logs for details.</p></div>';
            }
            ?>
        </div>
        <?php
    }

    private function ensure_404_monitor_table()
    {
        global $wpdb;
        require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor-database.php';
        
        $table_name = $wpdb->prefix . Metasync_Error_Monitor_Database::$table_name;
        
        if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) != $table_name) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- admin repair action — re-runs the migration check on demand from the UI
            require_once plugin_dir_path(dirname(__FILE__)) . 'database/class-db-migrations.php';
            MetaSync_DBMigration::run_migrations();
        }
    }

    private function check_database_structure()
    {
        global $wpdb;
        $table_name = $wpdb->prefix . 'metasync_redirections';
        
        if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) != $table_name) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- admin repair action — re-runs the migration check on demand from the UI
            return;
        }
        
        $columns = $wpdb->get_col("DESCRIBE {$wpdb->prefix}metasync_redirections"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- admin repair action — re-runs the migration check on demand from the UI
        $required_columns = ['pattern_type', 'regex_pattern', 'description', 'created_at', 'updated_at', 'last_accessed_at'];
        
        $missing_columns = array_diff($required_columns, $columns);
        
        if (!empty($missing_columns)) {
            add_action('admin_notices', function() use ($missing_columns) {
                echo '<div class="notice notice-warning is-dismissible">';
                echo '<p><strong>MetaSync:</strong> Database structure needs updating. Missing columns: ' . esc_html(implode(', ', $missing_columns)) . '</p>';
                echo '<p><button type="button" class="button button-secondary" onclick="updateDatabaseStructure()">Update Database Structure</button></p>';
                echo '</div>';
                
                echo '<script>
                function updateDatabaseStructure() {
                    if (confirm("This will update your database structure. Continue?")) {
                        const formData = new FormData();
                        formData.append("action", "metasync_update_db_structure");
                        formData.append("nonce", "' . esc_attr(wp_create_nonce('metasync_update_db_nonce')) . '");
                        
                        fetch(ajaxurl, {
                            method: "POST",
                            body: formData
                        })
                        .then(response => response.json())
                        .then(data => {
                            if (data.success) {
                                alert("Database structure updated successfully!");
                                location.reload();
                            } else {
                                alert("Error updating database: " + (data.data || "Unknown error"));
                            }
                        })
                        .catch(error => {
                            alert("Error updating database: " + error.message);
                        });
                    }
                }
                </script>';
            });
        }
    }
}

```
