PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.1
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.1
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / includes / class-metasync-redirections-admin.php

class-metasync-redirections-admin.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.1, at includes/class-metasync-redirections-admin.php

664 lines 28.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 exit;
4 }
5
6 /**
7 * Redirections admin page logic extracted from Metasync_Admin.
8 *
9 * Handles the tabbed redirections / 404-monitor UI, form processing,
10 * validation, and related database checks.
11 *
12 * @package Metasync
13 * @subpackage Metasync/includes
14 */
15 class Metasync_Redirections_Admin
16 {
17 private static $instance = null;
18
19 /**
20 * Which tab of the combined Redirections screen is being viewed.
21 *
22 * Set ONLY by render_tab_content(), i.e. only when both the redirections
23 * and 404-monitor tables are rendered onto one page. Stays null on the
24 * standalone 404 Monitor page (page=...-404-monitor), where that table is
25 * the only one present. The list tables read it to decide whether to offer
26 * bulk actions - see get_bulk_actions() in either table for why.
27 *
28 * @var string|null 'redirections', '404-monitor', or null when not on the
29 * combined screen.
30 */
31 public static $combined_screen_active_tab = null;
32
33 /** @var object Redirection database helper */
34 private $db_redirection;
35
36 /** @var Metasync_Admin Back-reference used for shared UI helpers */
37 private $admin;
38
39 private function __construct($db_redirection, $admin)
40 {
41 $this->db_redirection = $db_redirection;
42 $this->admin = $admin;
43 }
44
45 /**
46 * @param object|null $db_redirection Required on first call.
47 * @param Metasync_Admin|null $admin Required on first call.
48 */
49 public static function get_instance($db_redirection = null, $admin = null)
50 {
51 if (self::$instance === null) {
52 self::$instance = new self($db_redirection, $admin);
53 }
54 return self::$instance;
55 }
56
57 /* ------------------------------------------------------------------
58 * Public entry points (called from Metasync_Admin delegation stubs)
59 * ------------------------------------------------------------------ */
60
61 public function create_admin_redirections_page()
62 {
63 $this->handle_redirection_form_processing();
64
65 $this->check_database_structure();
66
67 $this->ensure_404_monitor_table();
68
69 $current_tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : 'redirections'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab selection
70
71 $this->add_tabbed_interface_assets();
72 $this->admin->render_layout_open('Redirections', 'redirections', 'Manage URL redirects and monitor 404 errors on your site.');
73 $this->render_tab_navigation($current_tab);
74 $this->render_tab_content($current_tab);
75 $this->admin->render_layout_close();
76 }
77
78 public function display_redirection_messages()
79 {
80 $uid = get_current_user_id();
81
82 if ($error = get_transient('metasync_redirection_error_' . $uid)) {
83 echo '<div class="notice notice-error is-dismissible"><p>' . esc_html($error) . '</p></div>';
84 delete_transient('metasync_redirection_error_' . $uid);
85 }
86
87 if ($success = get_transient('metasync_redirection_success_' . $uid)) {
88 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($success) . '</p></div>';
89 delete_transient('metasync_redirection_success_' . $uid);
90 }
91
92 if ($warning = get_transient('metasync_redirection_warning_' . $uid)) {
93 echo '<div class="notice notice-warning is-dismissible"><p>' . esc_html($warning) . '</p></div>';
94 delete_transient('metasync_redirection_warning_' . $uid);
95 }
96 }
97
98 /* ------------------------------------------------------------------
99 * Private helpers
100 * ------------------------------------------------------------------ */
101
102 private function safe_redirect($url)
103 {
104 if (!headers_sent()) {
105 wp_safe_redirect($url);
106 exit;
107 } else {
108 echo '<script type="text/javascript">window.location.href = "' . esc_url($url) . '";</script>';
109 echo '<noscript><meta http-equiv="refresh" content="0;url=' . esc_url($url) . '"></noscript>';
110 exit;
111 }
112 }
113
114 private function handle_redirection_form_processing()
115 {
116 if (!isset($_POST['submit'])) {
117 return;
118 }
119
120 $uid = get_current_user_id();
121
122 $nonce_valid = false;
123
124 if (isset($_POST['metasync_redirection_nonce']) && wp_verify_nonce($_POST['metasync_redirection_nonce'], 'metasync_redirection_form')) {
125 $nonce_valid = true;
126 }
127 elseif (isset($_POST['_wpnonce']) && wp_verify_nonce($_POST['_wpnonce'], 'metasync_redirection_form')) {
128 $nonce_valid = true;
129 }
130
131 if (!$nonce_valid) {
132 wp_die('Security check failed. Please refresh and try again.');
133 }
134
135 if (!Metasync::current_user_has_plugin_access()) {
136 wp_die('Insufficient permissions.');
137 }
138
139 // Unslash before sanitizing: source URLs with apostrophes/backslashes
140 // must be stored in their literal form so the front-end matcher (which
141 // compares against the unslashed request URI) can hit them.
142 $source_urls = isset($_POST['source_url']) ? array_map('sanitize_text_field', wp_unslash($_POST['source_url'])) : [];
143 $search_types = isset($_POST['search_type']) ? array_map('sanitize_text_field', wp_unslash($_POST['search_type'])) : [];
144 $destination_url = isset($_POST['destination_url']) ? sanitize_text_field(wp_unslash($_POST['destination_url'])) : '';
145 $redirect_type = isset($_POST['redirect_type']) ? intval($_POST['redirect_type']) : 301;
146 $status = isset($_POST['status']) ? sanitize_text_field(wp_unslash($_POST['status'])) : 'active';
147 $regex_pattern = isset($_POST['regex_pattern']) ? wp_unslash(trim($_POST['regex_pattern'])) : '';
148 $description = isset($_POST['description']) ? sanitize_text_field(wp_unslash($_POST['description'])) : '';
149 $redirect_id = isset($_POST['redirect_id']) ? intval($_POST['redirect_id']) : 0;
150
151 $validation_errors = [];
152
153 if (empty($source_urls)) {
154 $validation_errors[] = 'Please enter at least one source URL.';
155 } else {
156 $processed_sources = [];
157 $empty_count = 0;
158
159 foreach ($source_urls as $source_url) {
160 $trimmed_url = trim($source_url);
161
162 if (empty($trimmed_url)) {
163 $empty_count++;
164 continue;
165 }
166
167 if (!$this->is_valid_url($trimmed_url)) {
168 $validation_errors[] = 'Invalid source URL format: "' . esc_html($trimmed_url) . '". URLs should start with / for relative paths or be complete URLs.';
169 }
170
171 if (in_array($trimmed_url, $processed_sources)) {
172 $validation_errors[] = 'Duplicate source URL detected: "' . esc_html($trimmed_url) . '".';
173 } else {
174 $processed_sources[] = $trimmed_url;
175 }
176 }
177
178 if ($empty_count === count($source_urls)) {
179 $validation_errors[] = 'All source URL fields are empty. Please enter at least one source URL.';
180 }
181 }
182
183 $allowed_redirect_types = [301, 302, 307, 410, 451];
184 if (!in_array($redirect_type, $allowed_redirect_types)) {
185 $validation_errors[] = 'Invalid redirection type selected.';
186 }
187
188 if (!in_array($redirect_type, [410, 451])) {
189 $trimmed_dest = trim($destination_url);
190 if (empty($trimmed_dest)) {
191 $validation_errors[] = 'Destination URL is required for this redirect type.';
192 } elseif (!$this->is_valid_url($trimmed_dest)) {
193 $validation_errors[] = 'Invalid destination URL format. URLs should start with / for relative paths or be complete URLs.';
194 } elseif (!Metasync_Redirection_Validator::is_safe_destination_syntax($trimmed_dest)) {
195 // Browsers treat backslashes as path separators, so '/\evil.com'
196 // passes wp_validate_redirect yet navigates off-site; a leading
197 // '//' is a protocol-relative host. Neither is a valid internal
198 // destination regardless of the external-redirects setting.
199 $validation_errors[] = 'Destination URL contains invalid characters.';
200 } elseif (!get_option('metasync_allow_external_redirects', 0) && wp_validate_redirect($trimmed_dest, '') !== $trimmed_dest) {
201 $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.';
202 }
203 }
204
205 $allowed_statuses = ['active', 'inactive'];
206 if (!in_array($status, $allowed_statuses)) {
207 $validation_errors[] = 'Invalid status selected.';
208 }
209
210 if (!empty($validation_errors)) {
211 $error_message = implode(' ', $validation_errors);
212 set_transient('metasync_redirection_error_' . $uid, $error_message, 45);
213 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
214 }
215
216 $sources_from = [];
217 foreach ($source_urls as $index => $source_url) {
218 $trimmed_url = trim($source_url);
219 if (!empty($trimmed_url)) {
220 $search_type = isset($search_types[$index]) ? $search_types[$index] : 'exact';
221 $sources_from[$trimmed_url] = $search_type;
222 }
223 }
224
225 $pattern_type = 'exact';
226 foreach ($search_types as $search_type) {
227 if (!empty($search_type)) {
228 $pattern_type = $search_type;
229 break;
230 }
231 }
232
233 if ($pattern_type === 'regex') {
234 if (empty($regex_pattern)) {
235 set_transient('metasync_redirection_error_' . $uid, 'Please enter a regex pattern when using "Regex Pattern" as the pattern type.', 45);
236 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
237 return;
238 }
239
240 // Limit pattern length to prevent ReDoS via catastrophic backtracking
241 if (strlen($regex_pattern) > 500) {
242 set_transient('metasync_redirection_error_' . $uid, 'Regex pattern is too long (max 500 characters).', 45);
243 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
244 return;
245 }
246
247 // Reject patterns with nested quantification that could cause ReDoS,
248 // including alternation blowup ('((a|a)*)*') the older guard missed.
249 $raw_check = preg_replace('/^\S(.*)\S[a-zA-Z]*$/', '$1', $regex_pattern);
250 if (!Metasync_Redirection_Validator::is_regex_safe($raw_check)) {
251 set_transient('metasync_redirection_error_' . $uid, 'Regex pattern is too long or contains potentially unsafe nested quantifiers (including nested alternation).', 45);
252 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
253 return;
254 }
255
256 $test_pattern = $regex_pattern;
257 $delimiter_chars = ['/', '#', '~', '%', '@'];
258 $has_valid_delimiters = false;
259
260 if (strlen($test_pattern) >= 2) {
261 $first_char = $test_pattern[0];
262 if (in_array($first_char, $delimiter_chars)) {
263 $last_pos = strrpos($test_pattern, $first_char);
264 if ($last_pos > 0) {
265 $has_valid_delimiters = true;
266 }
267 }
268 }
269
270 if (!$has_valid_delimiters) {
271 $test_pattern = '/' . $test_pattern . '/';
272 }
273 $is_valid = Metasync_Regex_Validator::is_valid($test_pattern);
274 if ($is_valid === false) {
275 $error_message = error_get_last();
276 $error_text = isset($error_message['message']) ? $error_message['message'] : 'Unknown regex error';
277 set_transient('metasync_redirection_error_' . $uid, 'Invalid Regex Pattern: ' . $error_text . ' Please fix the regex pattern and try again.', 45);
278 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
279 return;
280 }
281 }
282
283 // Loop detection: refuse to persist a chain that would resolve back to any source
284 if (!in_array($redirect_type, [410, 451])) {
285 require_once dirname(__FILE__, 2) . '/redirections/class-metasync-redirection.php';
286 require_once dirname(__FILE__, 2) . '/redirections/class-metasync-redirection-validator.php';
287 $redirection_helper = new Metasync_Redirection($this->db_redirection);
288 foreach (array_keys($sources_from) as $source_url) {
289 $loop_error = $redirection_helper->validate_no_loop($source_url, $destination_url);
290 if ($loop_error !== null) {
291 set_transient('metasync_redirection_error_' . $uid, $loop_error, 45);
292 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
293 return;
294 }
295 }
296
297 // Non-blocking destination reachability check — surface a warning, never block the save.
298 $dest_warning = $redirection_helper->destination_resolves_warning($destination_url, $redirect_type, $pattern_type);
299 if ($dest_warning !== null) {
300 set_transient('metasync_redirection_warning_' . $uid, $dest_warning, 45);
301 }
302 }
303
304 $data = [
305 'sources_from' => serialize($sources_from),
306 'url_redirect_to' => $destination_url,
307 'http_code' => $redirect_type,
308 'status' => $status,
309 'pattern_type' => $pattern_type,
310 'regex_pattern' => $regex_pattern,
311 'description' => $description,
312 ];
313
314 try {
315 if ($redirect_id > 0) {
316 $result = $this->db_redirection->update($data, $redirect_id);
317 if ($result === false) {
318 throw new Exception('Failed to update redirection');
319 }
320 $message = 'Redirection updated successfully.';
321 } else {
322 $result = $this->db_redirection->add($data);
323 if ($result === false) {
324 throw new Exception('Failed to add redirection');
325 }
326 $message = 'Redirection added successfully.';
327 }
328
329 } catch (Exception $e) {
330 error_log('MetaSync error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
331 set_transient('metasync_redirection_error_' . $uid, 'An error occurred while saving the redirection. Please try again.', 45);
332 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
333 }
334
335 set_transient('metasync_redirection_success_' . $uid, $message, 45);
336
337 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
338 }
339
340 private function is_valid_url($url)
341 {
342 if (strpos($url, '/') === 0) {
343 return true;
344 }
345
346 return filter_var($url, FILTER_VALIDATE_URL) !== false;
347 }
348
349 private function add_tabbed_interface_assets()
350 {
351 ?>
352 <style>
353 /* Root Variables - Dashboard Color Scheme */
354 :root {
355 --dashboard-bg: #0f1419;
356 --dashboard-card-bg: #1a1f26;
357 --dashboard-card-hover: #222831;
358 --dashboard-text-primary: #ffffff;
359 --dashboard-text-secondary: #9ca3af;
360 --dashboard-accent: #3b82f6;
361 --dashboard-accent-hover: #2563eb;
362 --dashboard-success: #10b981;
363 --dashboard-warning: #f59e0b;
364 --dashboard-error: #ef4444;
365 --dashboard-border: #374151;
366 --dashboard-gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
367 --dashboard-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.1);
368 --dashboard-shadow-hover: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -2px rgba(0, 0, 0, 0.2);
369 }
370
371 .metasync-tabs {
372 margin: 20px 0;
373 background: var(--dashboard-card-bg);
374 border: 1px solid var(--dashboard-border);
375 border-radius: 12px;
376 padding: 6px;
377 box-shadow: var(--dashboard-shadow);
378 }
379
380 .metasync-tab-nav {
381 border-bottom: none;
382 margin: 0;
383 padding: 0;
384 display: flex;
385 gap: 4px;
386 background: transparent;
387 }
388
389 .metasync-tab-nav li {
390 display: inline-block;
391 margin: 0;
392 list-style: none;
393 }
394
395 .metasync-tab-nav a {
396 display: block;
397 padding: 12px 20px;
398 text-decoration: none;
399 color: var(--dashboard-text-secondary);
400 border-bottom: none;
401 border-radius: 8px;
402 transition: all 0.3s ease;
403 font-weight: 500;
404 font-size: 14px;
405 background: transparent;
406 position: relative;
407 overflow: hidden;
408 }
409
410 .metasync-tab-nav a:hover {
411 color: var(--dashboard-text-primary);
412 background: rgba(255, 255, 255, 0.05);
413 transform: translateY(-1px);
414 }
415
416 .metasync-tab-nav a.active {
417 color: var(--dashboard-text-primary);
418 background: var(--dashboard-card-hover);
419 border-bottom: none;
420 box-shadow: var(--dashboard-shadow);
421 transform: translateY(-1px);
422 font-weight: 600;
423 }
424
425 .metasync-tab-nav a.active::after {
426 content: '';
427 position: absolute;
428 bottom: 0;
429 left: 0;
430 right: 0;
431 height: 2px;
432 background: var(--dashboard-accent);
433 border-radius: 1px;
434 }
435
436 .metasync-tab-content {
437 display: none;
438 padding: 20px 0;
439 }
440
441 .metasync-tab-content.active {
442 display: block;
443 }
444 </style>
445
446 <script>
447 jQuery(document).ready(function($) {
448 function switchToTab(targetTab) {
449 $('.metasync-tab-nav a').removeClass('active');
450 $('.metasync-tab-nav a[data-tab="' + targetTab + '"]').addClass('active');
451
452 $('.metasync-tab-content').removeClass('active');
453 $('#' + targetTab + '-content').addClass('active');
454 }
455
456 function initializeTabs() {
457 var urlParams = new URLSearchParams(window.location.search);
458 var currentTab = urlParams.get('tab');
459
460 if (currentTab && (currentTab === 'redirections' || currentTab === '404-monitor')) {
461 switchToTab(currentTab);
462 } else {
463 switchToTab('redirections');
464 currentTab = 'redirections';
465 }
466
467 var needsCleanup = false;
468 if (currentTab === '404-monitor') {
469 if (urlParams.has('paged') || urlParams.has('paged_redir')) {
470 urlParams.delete('paged');
471 urlParams.delete('paged_redir');
472 needsCleanup = true;
473 }
474 } else if (currentTab === 'redirections') {
475 if (urlParams.has('paged') || urlParams.has('paged_404')) {
476 urlParams.delete('paged');
477 urlParams.delete('paged_404');
478 needsCleanup = true;
479 }
480 }
481
482 if (needsCleanup) {
483 var newUrl = window.location.pathname + '?' + urlParams.toString();
484 window.history.replaceState({}, '', newUrl);
485 }
486 }
487
488 initializeTabs();
489
490 setTimeout(initializeTabs, 100);
491
492 $('.metasync-tab-nav a').on('click', function(e) {
493 e.preventDefault();
494
495 var targetTab = $(this).data('tab');
496 switchToTab(targetTab);
497
498 var url = new URL(window.location);
499 url.searchParams.set('tab', targetTab);
500
501 url.searchParams.delete('paged');
502 url.searchParams.delete('paged_404');
503 url.searchParams.delete('paged_redir');
504
505 window.history.pushState({}, '', url);
506 });
507 });
508 </script>
509 <?php
510 }
511
512 private function render_tab_navigation($current_tab)
513 {
514 $base_url = admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections');
515
516 ?>
517 <div class="metasync-tabs">
518 <ul class="metasync-tab-nav">
519 <li>
520 <a href="<?php echo esc_url($base_url . '&tab=redirections'); ?>"
521 data-tab="redirections"
522 class="<?php echo $current_tab === 'redirections' ? 'active' : ''; ?>">
523 Redirections
524 </a>
525 </li>
526 <li>
527 <a href="<?php echo esc_url($base_url . '&tab=404-monitor'); ?>"
528 data-tab="404-monitor"
529 class="<?php echo $current_tab === '404-monitor' ? 'active' : ''; ?>">
530 404 Monitor
531 </a>
532 </li>
533 </ul>
534 </div>
535 <?php
536 }
537
538 private function render_tab_content($current_tab)
539 {
540 // Both tables are rendered onto this one page (CSS hides the inactive
541 // tab), so record which tab is actually being viewed. The list tables
542 // use this to keep the hidden tab from emitting a duplicate set of
543 // bulk-action controls - see get_bulk_actions() in either table.
544 self::$combined_screen_active_tab = ($current_tab === '404-monitor')
545 ? '404-monitor'
546 : 'redirections';
547
548 $this->render_redirections_tab($current_tab);
549 $this->render_404_monitor_tab($current_tab);
550 }
551
552 private function render_redirections_tab($current_tab = null)
553 {
554 if ($current_tab === null) {
555 $current_tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : 'redirections'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab selection
556 }
557 $active_class = $current_tab === 'redirections' ? 'active' : '';
558
559 ?>
560 <div id="redirections-content" class="metasync-tab-content <?php echo $active_class; ?>">
561 <?php
562 $redirection = new Metasync_Redirection($this->db_redirection);
563 $redirection->create_admin_redirection_interface();
564 ?>
565 </div>
566 <?php
567 }
568
569 private function render_404_monitor_tab($current_tab = null)
570 {
571 if ($current_tab === null) {
572 $current_tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : 'redirections'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab selection
573 }
574 $active_class = $current_tab === '404-monitor' ? 'active' : '';
575
576 ?>
577 <div id="404-monitor-content" class="metasync-tab-content <?php echo $active_class; ?>">
578 <?php
579 try {
580 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor-database.php';
581 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor.php';
582
583 $db_404 = new Metasync_Error_Monitor_Database();
584 $ErrorMonitor = new Metasync_Error_Monitor($db_404);
585
586 $ErrorMonitor->create_admin_plugin_interface();
587
588 } catch (Exception $e) {
589 error_log('MetaSync 404 Monitor Error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
590 echo '<div class="notice notice-error"><p>An error occurred while loading the 404 monitor. Please check the server logs for details.</p></div>';
591 } catch (Error $e) {
592 error_log('MetaSync 404 Monitor Fatal Error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
593 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>';
594 }
595 ?>
596 </div>
597 <?php
598 }
599
600 private function ensure_404_monitor_table()
601 {
602 global $wpdb;
603 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor-database.php';
604
605 $table_name = $wpdb->prefix . Metasync_Error_Monitor_Database::$table_name;
606
607 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
608 require_once plugin_dir_path(dirname(__FILE__)) . 'database/class-db-migrations.php';
609 MetaSync_DBMigration::run_migrations();
610 }
611 }
612
613 private function check_database_structure()
614 {
615 global $wpdb;
616 $table_name = $wpdb->prefix . 'metasync_redirections';
617
618 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
619 return;
620 }
621
622 $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
623 $required_columns = ['pattern_type', 'regex_pattern', 'description', 'created_at', 'updated_at', 'last_accessed_at'];
624
625 $missing_columns = array_diff($required_columns, $columns);
626
627 if (!empty($missing_columns)) {
628 add_action('admin_notices', function() use ($missing_columns) {
629 echo '<div class="notice notice-warning is-dismissible">';
630 echo '<p><strong>MetaSync:</strong> Database structure needs updating. Missing columns: ' . implode(', ', $missing_columns) . '</p>';
631 echo '<p><button type="button" class="button button-secondary" onclick="updateDatabaseStructure()">Update Database Structure</button></p>';
632 echo '</div>';
633
634 echo '<script>
635 function updateDatabaseStructure() {
636 if (confirm("This will update your database structure. Continue?")) {
637 const formData = new FormData();
638 formData.append("action", "metasync_update_db_structure");
639 formData.append("nonce", "' . wp_create_nonce('metasync_update_db_nonce') . '");
640
641 fetch(ajaxurl, {
642 method: "POST",
643 body: formData
644 })
645 .then(response => response.json())
646 .then(data => {
647 if (data.success) {
648 alert("Database structure updated successfully!");
649 location.reload();
650 } else {
651 alert("Error updating database: " + (data.data || "Unknown error"));
652 }
653 })
654 .catch(error => {
655 alert("Error updating database: " + error.message);
656 });
657 }
658 }
659 </script>';
660 });
661 }
662 }
663 }
664