PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.22
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.22
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 2.6.4 2.6.3 2.5.23 All 138 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.22, at includes/class-metasync-redirections-admin.php

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