PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.25
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.25
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.6.25, at includes/class-metasync-redirections-admin.php

661 lines 26.9 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($_GET['tab']) : 'redirections';
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_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 $source_urls = isset($_POST['source_url']) ? array_map('sanitize_text_field', $_POST['source_url']) : [];
140 $search_types = isset($_POST['search_type']) ? array_map('sanitize_text_field', $_POST['search_type']) : [];
141 $destination_url = isset($_POST['destination_url']) ? sanitize_text_field($_POST['destination_url']) : '';
142 $redirect_type = isset($_POST['redirect_type']) ? intval($_POST['redirect_type']) : 301;
143 $status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'active';
144 $regex_pattern = isset($_POST['regex_pattern']) ? wp_unslash(trim($_POST['regex_pattern'])) : '';
145 $description = isset($_POST['description']) ? sanitize_text_field($_POST['description']) : '';
146 $redirect_id = isset($_POST['redirect_id']) ? intval($_POST['redirect_id']) : 0;
147
148 $validation_errors = [];
149
150 if (empty($source_urls)) {
151 $validation_errors[] = 'Please enter at least one source URL.';
152 } else {
153 $processed_sources = [];
154 $empty_count = 0;
155
156 foreach ($source_urls as $source_url) {
157 $trimmed_url = trim($source_url);
158
159 if (empty($trimmed_url)) {
160 $empty_count++;
161 continue;
162 }
163
164 if (!$this->is_valid_url($trimmed_url)) {
165 $validation_errors[] = 'Invalid source URL format: "' . esc_html($trimmed_url) . '". URLs should start with / for relative paths or be complete URLs.';
166 }
167
168 if (in_array($trimmed_url, $processed_sources)) {
169 $validation_errors[] = 'Duplicate source URL detected: "' . esc_html($trimmed_url) . '".';
170 } else {
171 $processed_sources[] = $trimmed_url;
172 }
173 }
174
175 if ($empty_count === count($source_urls)) {
176 $validation_errors[] = 'All source URL fields are empty. Please enter at least one source URL.';
177 }
178 }
179
180 $allowed_redirect_types = [301, 302, 307, 410, 451];
181 if (!in_array($redirect_type, $allowed_redirect_types)) {
182 $validation_errors[] = 'Invalid redirection type selected.';
183 }
184
185 if (!in_array($redirect_type, [410, 451])) {
186 $trimmed_dest = trim($destination_url);
187 if (empty($trimmed_dest)) {
188 $validation_errors[] = 'Destination URL is required for this redirect type.';
189 } elseif (!$this->is_valid_url($trimmed_dest)) {
190 $validation_errors[] = 'Invalid destination URL format. URLs should start with / for relative paths or be complete URLs.';
191 } elseif (!Metasync_Redirection_Validator::is_safe_destination_syntax($trimmed_dest)) {
192 // Browsers treat backslashes as path separators, so '/\evil.com'
193 // passes wp_validate_redirect yet navigates off-site; a leading
194 // '//' is a protocol-relative host. Neither is a valid internal
195 // destination regardless of the external-redirects setting.
196 $validation_errors[] = 'Destination URL contains invalid characters.';
197 } elseif (!get_option('metasync_allow_external_redirects', 0) && wp_validate_redirect($trimmed_dest, '') !== $trimmed_dest) {
198 $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.';
199 }
200 }
201
202 $allowed_statuses = ['active', 'inactive'];
203 if (!in_array($status, $allowed_statuses)) {
204 $validation_errors[] = 'Invalid status selected.';
205 }
206
207 if (!empty($validation_errors)) {
208 $error_message = implode(' ', $validation_errors);
209 set_transient('metasync_redirection_error_' . $uid, $error_message, 45);
210 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
211 }
212
213 $sources_from = [];
214 foreach ($source_urls as $index => $source_url) {
215 $trimmed_url = trim($source_url);
216 if (!empty($trimmed_url)) {
217 $search_type = isset($search_types[$index]) ? $search_types[$index] : 'exact';
218 $sources_from[$trimmed_url] = $search_type;
219 }
220 }
221
222 $pattern_type = 'exact';
223 foreach ($search_types as $search_type) {
224 if (!empty($search_type)) {
225 $pattern_type = $search_type;
226 break;
227 }
228 }
229
230 if ($pattern_type === 'regex') {
231 if (empty($regex_pattern)) {
232 set_transient('metasync_redirection_error_' . $uid, 'Please enter a regex pattern when using "Regex Pattern" as the pattern type.', 45);
233 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
234 return;
235 }
236
237 // Limit pattern length to prevent ReDoS via catastrophic backtracking
238 if (strlen($regex_pattern) > 500) {
239 set_transient('metasync_redirection_error_' . $uid, 'Regex pattern is too long (max 500 characters).', 45);
240 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
241 return;
242 }
243
244 // Reject patterns with nested quantification that could cause ReDoS,
245 // including alternation blowup ('((a|a)*)*') the older guard missed.
246 $raw_check = preg_replace('/^\S(.*)\S[a-zA-Z]*$/', '$1', $regex_pattern);
247 if (!Metasync_Redirection_Validator::is_regex_safe($raw_check)) {
248 set_transient('metasync_redirection_error_' . $uid, 'Regex pattern is too long or contains potentially unsafe nested quantifiers (including nested alternation).', 45);
249 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
250 return;
251 }
252
253 $test_pattern = $regex_pattern;
254 $delimiter_chars = ['/', '#', '~', '%', '@'];
255 $has_valid_delimiters = false;
256
257 if (strlen($test_pattern) >= 2) {
258 $first_char = $test_pattern[0];
259 if (in_array($first_char, $delimiter_chars)) {
260 $last_pos = strrpos($test_pattern, $first_char);
261 if ($last_pos > 0) {
262 $has_valid_delimiters = true;
263 }
264 }
265 }
266
267 if (!$has_valid_delimiters) {
268 $test_pattern = '/' . $test_pattern . '/';
269 }
270 $is_valid = Metasync_Regex_Validator::is_valid($test_pattern);
271 if ($is_valid === false) {
272 $error_message = error_get_last();
273 $error_text = isset($error_message['message']) ? $error_message['message'] : 'Unknown regex error';
274 set_transient('metasync_redirection_error_' . $uid, 'Invalid Regex Pattern: ' . $error_text . ' Please fix the regex pattern and try again.', 45);
275 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
276 return;
277 }
278 }
279
280 // Loop detection: refuse to persist a chain that would resolve back to any source
281 if (!in_array($redirect_type, [410, 451])) {
282 require_once dirname(__FILE__, 2) . '/redirections/class-metasync-redirection.php';
283 require_once dirname(__FILE__, 2) . '/redirections/class-metasync-redirection-validator.php';
284 $redirection_helper = new Metasync_Redirection($this->db_redirection);
285 foreach (array_keys($sources_from) as $source_url) {
286 $loop_error = $redirection_helper->validate_no_loop($source_url, $destination_url);
287 if ($loop_error !== null) {
288 set_transient('metasync_redirection_error_' . $uid, $loop_error, 45);
289 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
290 return;
291 }
292 }
293
294 // Non-blocking destination reachability check — surface a warning, never block the save.
295 $dest_warning = $redirection_helper->destination_resolves_warning($destination_url, $redirect_type, $pattern_type);
296 if ($dest_warning !== null) {
297 set_transient('metasync_redirection_warning_' . $uid, $dest_warning, 45);
298 }
299 }
300
301 $data = [
302 'sources_from' => serialize($sources_from),
303 'url_redirect_to' => $destination_url,
304 'http_code' => $redirect_type,
305 'status' => $status,
306 'pattern_type' => $pattern_type,
307 'regex_pattern' => $regex_pattern,
308 'description' => $description,
309 ];
310
311 try {
312 if ($redirect_id > 0) {
313 $result = $this->db_redirection->update($data, $redirect_id);
314 if ($result === false) {
315 throw new Exception('Failed to update redirection');
316 }
317 $message = 'Redirection updated successfully.';
318 } else {
319 $result = $this->db_redirection->add($data);
320 if ($result === false) {
321 throw new Exception('Failed to add redirection');
322 }
323 $message = 'Redirection added successfully.';
324 }
325
326 } catch (Exception $e) {
327 error_log('MetaSync error: ' . $e->getMessage());
328 set_transient('metasync_redirection_error_' . $uid, 'An error occurred while saving the redirection. Please try again.', 45);
329 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
330 }
331
332 set_transient('metasync_redirection_success_' . $uid, $message, 45);
333
334 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
335 }
336
337 private function is_valid_url($url)
338 {
339 if (strpos($url, '/') === 0) {
340 return true;
341 }
342
343 return filter_var($url, FILTER_VALIDATE_URL) !== false;
344 }
345
346 private function add_tabbed_interface_assets()
347 {
348 ?>
349 <style>
350 /* Root Variables - Dashboard Color Scheme */
351 :root {
352 --dashboard-bg: #0f1419;
353 --dashboard-card-bg: #1a1f26;
354 --dashboard-card-hover: #222831;
355 --dashboard-text-primary: #ffffff;
356 --dashboard-text-secondary: #9ca3af;
357 --dashboard-accent: #3b82f6;
358 --dashboard-accent-hover: #2563eb;
359 --dashboard-success: #10b981;
360 --dashboard-warning: #f59e0b;
361 --dashboard-error: #ef4444;
362 --dashboard-border: #374151;
363 --dashboard-gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
364 --dashboard-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.1);
365 --dashboard-shadow-hover: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -2px rgba(0, 0, 0, 0.2);
366 }
367
368 .metasync-tabs {
369 margin: 20px 0;
370 background: var(--dashboard-card-bg);
371 border: 1px solid var(--dashboard-border);
372 border-radius: 12px;
373 padding: 6px;
374 box-shadow: var(--dashboard-shadow);
375 }
376
377 .metasync-tab-nav {
378 border-bottom: none;
379 margin: 0;
380 padding: 0;
381 display: flex;
382 gap: 4px;
383 background: transparent;
384 }
385
386 .metasync-tab-nav li {
387 display: inline-block;
388 margin: 0;
389 list-style: none;
390 }
391
392 .metasync-tab-nav a {
393 display: block;
394 padding: 12px 20px;
395 text-decoration: none;
396 color: var(--dashboard-text-secondary);
397 border-bottom: none;
398 border-radius: 8px;
399 transition: all 0.3s ease;
400 font-weight: 500;
401 font-size: 14px;
402 background: transparent;
403 position: relative;
404 overflow: hidden;
405 }
406
407 .metasync-tab-nav a:hover {
408 color: var(--dashboard-text-primary);
409 background: rgba(255, 255, 255, 0.05);
410 transform: translateY(-1px);
411 }
412
413 .metasync-tab-nav a.active {
414 color: var(--dashboard-text-primary);
415 background: var(--dashboard-card-hover);
416 border-bottom: none;
417 box-shadow: var(--dashboard-shadow);
418 transform: translateY(-1px);
419 font-weight: 600;
420 }
421
422 .metasync-tab-nav a.active::after {
423 content: '';
424 position: absolute;
425 bottom: 0;
426 left: 0;
427 right: 0;
428 height: 2px;
429 background: var(--dashboard-accent);
430 border-radius: 1px;
431 }
432
433 .metasync-tab-content {
434 display: none;
435 padding: 20px 0;
436 }
437
438 .metasync-tab-content.active {
439 display: block;
440 }
441 </style>
442
443 <script>
444 jQuery(document).ready(function($) {
445 function switchToTab(targetTab) {
446 $('.metasync-tab-nav a').removeClass('active');
447 $('.metasync-tab-nav a[data-tab="' + targetTab + '"]').addClass('active');
448
449 $('.metasync-tab-content').removeClass('active');
450 $('#' + targetTab + '-content').addClass('active');
451 }
452
453 function initializeTabs() {
454 var urlParams = new URLSearchParams(window.location.search);
455 var currentTab = urlParams.get('tab');
456
457 if (currentTab && (currentTab === 'redirections' || currentTab === '404-monitor')) {
458 switchToTab(currentTab);
459 } else {
460 switchToTab('redirections');
461 currentTab = 'redirections';
462 }
463
464 var needsCleanup = false;
465 if (currentTab === '404-monitor') {
466 if (urlParams.has('paged') || urlParams.has('paged_redir')) {
467 urlParams.delete('paged');
468 urlParams.delete('paged_redir');
469 needsCleanup = true;
470 }
471 } else if (currentTab === 'redirections') {
472 if (urlParams.has('paged') || urlParams.has('paged_404')) {
473 urlParams.delete('paged');
474 urlParams.delete('paged_404');
475 needsCleanup = true;
476 }
477 }
478
479 if (needsCleanup) {
480 var newUrl = window.location.pathname + '?' + urlParams.toString();
481 window.history.replaceState({}, '', newUrl);
482 }
483 }
484
485 initializeTabs();
486
487 setTimeout(initializeTabs, 100);
488
489 $('.metasync-tab-nav a').on('click', function(e) {
490 e.preventDefault();
491
492 var targetTab = $(this).data('tab');
493 switchToTab(targetTab);
494
495 var url = new URL(window.location);
496 url.searchParams.set('tab', targetTab);
497
498 url.searchParams.delete('paged');
499 url.searchParams.delete('paged_404');
500 url.searchParams.delete('paged_redir');
501
502 window.history.pushState({}, '', url);
503 });
504 });
505 </script>
506 <?php
507 }
508
509 private function render_tab_navigation($current_tab)
510 {
511 $base_url = admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections');
512
513 ?>
514 <div class="metasync-tabs">
515 <ul class="metasync-tab-nav">
516 <li>
517 <a href="<?php echo esc_url($base_url . '&tab=redirections'); ?>"
518 data-tab="redirections"
519 class="<?php echo $current_tab === 'redirections' ? 'active' : ''; ?>">
520 Redirections
521 </a>
522 </li>
523 <li>
524 <a href="<?php echo esc_url($base_url . '&tab=404-monitor'); ?>"
525 data-tab="404-monitor"
526 class="<?php echo $current_tab === '404-monitor' ? 'active' : ''; ?>">
527 404 Monitor
528 </a>
529 </li>
530 </ul>
531 </div>
532 <?php
533 }
534
535 private function render_tab_content($current_tab)
536 {
537 // Both tables are rendered onto this one page (CSS hides the inactive
538 // tab), so record which tab is actually being viewed. The list tables
539 // use this to keep the hidden tab from emitting a duplicate set of
540 // bulk-action controls - see get_bulk_actions() in either table.
541 self::$combined_screen_active_tab = ($current_tab === '404-monitor')
542 ? '404-monitor'
543 : 'redirections';
544
545 $this->render_redirections_tab($current_tab);
546 $this->render_404_monitor_tab($current_tab);
547 }
548
549 private function render_redirections_tab($current_tab = null)
550 {
551 if ($current_tab === null) {
552 $current_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'redirections';
553 }
554 $active_class = $current_tab === 'redirections' ? 'active' : '';
555
556 ?>
557 <div id="redirections-content" class="metasync-tab-content <?php echo $active_class; ?>">
558 <?php
559 $redirection = new Metasync_Redirection($this->db_redirection);
560 $redirection->create_admin_redirection_interface();
561 ?>
562 </div>
563 <?php
564 }
565
566 private function render_404_monitor_tab($current_tab = null)
567 {
568 if ($current_tab === null) {
569 $current_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'redirections';
570 }
571 $active_class = $current_tab === '404-monitor' ? 'active' : '';
572
573 ?>
574 <div id="404-monitor-content" class="metasync-tab-content <?php echo $active_class; ?>">
575 <?php
576 try {
577 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor-database.php';
578 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor.php';
579
580 $db_404 = new Metasync_Error_Monitor_Database();
581 $ErrorMonitor = new Metasync_Error_Monitor($db_404);
582
583 $ErrorMonitor->create_admin_plugin_interface();
584
585 } catch (Exception $e) {
586 error_log('MetaSync 404 Monitor Error: ' . $e->getMessage());
587 echo '<div class="notice notice-error"><p>An error occurred while loading the 404 monitor. Please check the server logs for details.</p></div>';
588 } catch (Error $e) {
589 error_log('MetaSync 404 Monitor Fatal Error: ' . $e->getMessage());
590 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>';
591 }
592 ?>
593 </div>
594 <?php
595 }
596
597 private function ensure_404_monitor_table()
598 {
599 global $wpdb;
600 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor-database.php';
601
602 $table_name = $wpdb->prefix . Metasync_Error_Monitor_Database::$table_name;
603
604 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) != $table_name) {
605 require_once plugin_dir_path(dirname(__FILE__)) . 'database/class-db-migrations.php';
606 MetaSync_DBMigration::run_migrations();
607 }
608 }
609
610 private function check_database_structure()
611 {
612 global $wpdb;
613 $table_name = $wpdb->prefix . 'metasync_redirections';
614
615 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) != $table_name) {
616 return;
617 }
618
619 $columns = $wpdb->get_col("DESCRIBE {$table_name}");
620 $required_columns = ['pattern_type', 'regex_pattern', 'description', 'created_at', 'updated_at', 'last_accessed_at'];
621
622 $missing_columns = array_diff($required_columns, $columns);
623
624 if (!empty($missing_columns)) {
625 add_action('admin_notices', function() use ($missing_columns) {
626 echo '<div class="notice notice-warning is-dismissible">';
627 echo '<p><strong>MetaSync:</strong> Database structure needs updating. Missing columns: ' . implode(', ', $missing_columns) . '</p>';
628 echo '<p><button type="button" class="button button-secondary" onclick="updateDatabaseStructure()">Update Database Structure</button></p>';
629 echo '</div>';
630
631 echo '<script>
632 function updateDatabaseStructure() {
633 if (confirm("This will update your database structure. Continue?")) {
634 const formData = new FormData();
635 formData.append("action", "metasync_update_db_structure");
636 formData.append("nonce", "' . wp_create_nonce('metasync_update_db_nonce') . '");
637
638 fetch(ajaxurl, {
639 method: "POST",
640 body: formData
641 })
642 .then(response => response.json())
643 .then(data => {
644 if (data.success) {
645 alert("Database structure updated successfully!");
646 location.reload();
647 } else {
648 alert("Error updating database: " + (data.data || "Unknown error"));
649 }
650 })
651 .catch(error => {
652 alert("Error updating database: " + error.message);
653 });
654 }
655 }
656 </script>';
657 });
658 }
659 }
660 }
661