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($_GET['tab']) : 'redirections'; $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() { if ($error = get_transient('metasync_redirection_error')) { echo '

' . esc_html($error) . '

'; delete_transient('metasync_redirection_error'); } if ($success = get_transient('metasync_redirection_success')) { echo '

' . esc_html($success) . '

'; delete_transient('metasync_redirection_success'); } } /* ------------------------------------------------------------------ * Private helpers * ------------------------------------------------------------------ */ private function safe_redirect($url) { if (!headers_sent()) { wp_redirect($url); exit; } else { echo ''; echo ''; exit; } } private function handle_redirection_form_processing() { if (!isset($_POST['submit'])) { return; } $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.'); } $source_urls = isset($_POST['source_url']) ? array_map('sanitize_text_field', $_POST['source_url']) : []; $search_types = isset($_POST['search_type']) ? array_map('sanitize_text_field', $_POST['search_type']) : []; $destination_url = isset($_POST['destination_url']) ? sanitize_text_field($_POST['destination_url']) : ''; $redirect_type = isset($_POST['redirect_type']) ? intval($_POST['redirect_type']) : 301; $status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'active'; $regex_pattern = isset($_POST['regex_pattern']) ? wp_unslash(trim($_POST['regex_pattern'])) : ''; $description = isset($_POST['description']) ? sanitize_text_field($_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)) { $empty_count++; continue; } 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; } } 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.'; } } $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', $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'; foreach ($search_types 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', '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; } $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 = @preg_match($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', '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]) && $redirect_id <= 0) { require_once dirname(__FILE__, 2) . '/redirections/class-metasync-redirection.php'; $redirection_helper = new Metasync_Redirection($this->db_redirection); foreach (array_keys($sources_from) as $source_url) { $loop_chain = []; if ($redirection_helper->would_create_loop($source_url, $destination_url, $loop_chain)) { set_transient( 'metasync_redirection_error', 'Redirect would create a loop: ' . implode(' → ', $loop_chain), 45 ); $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections')); return; } } } $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) { set_transient('metasync_redirection_error', 'Error saving redirection: ' . $e->getMessage(), 45); $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections')); } set_transient('metasync_redirection_success', $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; } private function add_tabbed_interface_assets() { ?>
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($_GET['tab']) : 'redirections'; } $active_class = $current_tab === 'redirections' ? 'active' : ''; ?>
db_redirection); $redirection->create_admin_redirection_interface(); ?>
create_admin_plugin_interface(); } catch (Exception $e) { echo '

Error loading 404 monitor: ' . esc_html($e->getMessage()) . '

'; error_log('MetaSync 404 Monitor Error: ' . $e->getMessage()); } catch (Error $e) { echo '

Fatal error loading 404 monitor: ' . esc_html($e->getMessage()) . '

'; error_log('MetaSync 404 Monitor Fatal Error: ' . $e->getMessage()); } ?>
prefix . Metasync_Error_Monitor_Database::$table_name; if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) != $table_name) { 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) { return; } $columns = $wpdb->get_col("DESCRIBE {$table_name}"); $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 '
'; echo '

MetaSync: Database structure needs updating. Missing columns: ' . implode(', ', $missing_columns) . '

'; echo '

'; echo '
'; echo ''; }); } } }