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

602 lines 23.0 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 /** @var object Redirection database helper */
20 private $db_redirection;
21
22 /** @var Metasync_Admin Back-reference used for shared UI helpers */
23 private $admin;
24
25 private function __construct($db_redirection, $admin)
26 {
27 $this->db_redirection = $db_redirection;
28 $this->admin = $admin;
29 }
30
31 /**
32 * @param object|null $db_redirection Required on first call.
33 * @param Metasync_Admin|null $admin Required on first call.
34 */
35 public static function get_instance($db_redirection = null, $admin = null)
36 {
37 if (self::$instance === null) {
38 self::$instance = new self($db_redirection, $admin);
39 }
40 return self::$instance;
41 }
42
43 /* ------------------------------------------------------------------
44 * Public entry points (called from Metasync_Admin delegation stubs)
45 * ------------------------------------------------------------------ */
46
47 public function create_admin_redirections_page()
48 {
49 $this->handle_redirection_form_processing();
50
51 $this->check_database_structure();
52
53 $this->ensure_404_monitor_table();
54
55 $current_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'redirections';
56
57 $this->add_tabbed_interface_assets();
58 $this->admin->render_layout_open('Redirections', 'redirections', 'Manage URL redirects and monitor 404 errors on your site.');
59 $this->render_tab_navigation($current_tab);
60 $this->render_tab_content($current_tab);
61 $this->admin->render_layout_close();
62 }
63
64 public function display_redirection_messages()
65 {
66 if ($error = get_transient('metasync_redirection_error')) {
67 echo '<div class="notice notice-error is-dismissible"><p>' . esc_html($error) . '</p></div>';
68 delete_transient('metasync_redirection_error');
69 }
70
71 if ($success = get_transient('metasync_redirection_success')) {
72 echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($success) . '</p></div>';
73 delete_transient('metasync_redirection_success');
74 }
75 }
76
77 /* ------------------------------------------------------------------
78 * Private helpers
79 * ------------------------------------------------------------------ */
80
81 private function safe_redirect($url)
82 {
83 if (!headers_sent()) {
84 wp_redirect($url);
85 exit;
86 } else {
87 echo '<script type="text/javascript">window.location.href = "' . esc_url($url) . '";</script>';
88 echo '<noscript><meta http-equiv="refresh" content="0;url=' . esc_url($url) . '"></noscript>';
89 exit;
90 }
91 }
92
93 private function handle_redirection_form_processing()
94 {
95 if (!isset($_POST['submit'])) {
96 return;
97 }
98
99 $nonce_valid = false;
100
101 if (isset($_POST['metasync_redirection_nonce']) && wp_verify_nonce($_POST['metasync_redirection_nonce'], 'metasync_redirection_form')) {
102 $nonce_valid = true;
103 }
104 elseif (isset($_POST['_wpnonce']) && wp_verify_nonce($_POST['_wpnonce'], 'metasync_redirection_form')) {
105 $nonce_valid = true;
106 }
107
108 if (!$nonce_valid) {
109 wp_die('Security check failed. Please refresh and try again.');
110 }
111
112 if (!Metasync::current_user_has_plugin_access()) {
113 wp_die('Insufficient permissions.');
114 }
115
116 $source_urls = isset($_POST['source_url']) ? array_map('sanitize_text_field', $_POST['source_url']) : [];
117 $search_types = isset($_POST['search_type']) ? array_map('sanitize_text_field', $_POST['search_type']) : [];
118 $destination_url = isset($_POST['destination_url']) ? sanitize_text_field($_POST['destination_url']) : '';
119 $redirect_type = isset($_POST['redirect_type']) ? intval($_POST['redirect_type']) : 301;
120 $status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'active';
121 $regex_pattern = isset($_POST['regex_pattern']) ? wp_unslash(trim($_POST['regex_pattern'])) : '';
122 $description = isset($_POST['description']) ? sanitize_text_field($_POST['description']) : '';
123 $redirect_id = isset($_POST['redirect_id']) ? intval($_POST['redirect_id']) : 0;
124
125 $validation_errors = [];
126
127 if (empty($source_urls)) {
128 $validation_errors[] = 'Please enter at least one source URL.';
129 } else {
130 $processed_sources = [];
131 $empty_count = 0;
132
133 foreach ($source_urls as $source_url) {
134 $trimmed_url = trim($source_url);
135
136 if (empty($trimmed_url)) {
137 $empty_count++;
138 continue;
139 }
140
141 if (!$this->is_valid_url($trimmed_url)) {
142 $validation_errors[] = 'Invalid source URL format: "' . esc_html($trimmed_url) . '". URLs should start with / for relative paths or be complete URLs.';
143 }
144
145 if (in_array($trimmed_url, $processed_sources)) {
146 $validation_errors[] = 'Duplicate source URL detected: "' . esc_html($trimmed_url) . '".';
147 } else {
148 $processed_sources[] = $trimmed_url;
149 }
150 }
151
152 if ($empty_count === count($source_urls)) {
153 $validation_errors[] = 'All source URL fields are empty. Please enter at least one source URL.';
154 }
155 }
156
157 $allowed_redirect_types = [301, 302, 307, 410, 451];
158 if (!in_array($redirect_type, $allowed_redirect_types)) {
159 $validation_errors[] = 'Invalid redirection type selected.';
160 }
161
162 if (!in_array($redirect_type, [410, 451])) {
163 $trimmed_dest = trim($destination_url);
164 if (empty($trimmed_dest)) {
165 $validation_errors[] = 'Destination URL is required for this redirect type.';
166 } elseif (!$this->is_valid_url($trimmed_dest)) {
167 $validation_errors[] = 'Invalid destination URL format. URLs should start with / for relative paths or be complete URLs.';
168 }
169 }
170
171 $allowed_statuses = ['active', 'inactive'];
172 if (!in_array($status, $allowed_statuses)) {
173 $validation_errors[] = 'Invalid status selected.';
174 }
175
176 if (!empty($validation_errors)) {
177 $error_message = implode(' ', $validation_errors);
178 set_transient('metasync_redirection_error', $error_message, 45);
179 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
180 }
181
182 $sources_from = [];
183 foreach ($source_urls as $index => $source_url) {
184 $trimmed_url = trim($source_url);
185 if (!empty($trimmed_url)) {
186 $search_type = isset($search_types[$index]) ? $search_types[$index] : 'exact';
187 $sources_from[$trimmed_url] = $search_type;
188 }
189 }
190
191 $pattern_type = 'exact';
192 foreach ($search_types as $search_type) {
193 if (!empty($search_type)) {
194 $pattern_type = $search_type;
195 break;
196 }
197 }
198
199 if ($pattern_type === 'regex') {
200 if (empty($regex_pattern)) {
201 set_transient('metasync_redirection_error', 'Please enter a regex pattern when using "Regex Pattern" as the pattern type.', 45);
202 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
203 return;
204 }
205
206 $test_pattern = $regex_pattern;
207 $delimiter_chars = ['/', '#', '~', '%', '@'];
208 $has_valid_delimiters = false;
209
210 if (strlen($test_pattern) >= 2) {
211 $first_char = $test_pattern[0];
212 if (in_array($first_char, $delimiter_chars)) {
213 $last_pos = strrpos($test_pattern, $first_char);
214 if ($last_pos > 0) {
215 $has_valid_delimiters = true;
216 }
217 }
218 }
219
220 if (!$has_valid_delimiters) {
221 $test_pattern = '/' . $test_pattern . '/';
222 }
223 $is_valid = @preg_match($test_pattern, '');
224 if ($is_valid === false) {
225 $error_message = error_get_last();
226 $error_text = isset($error_message['message']) ? $error_message['message'] : 'Unknown regex error';
227 set_transient('metasync_redirection_error', 'Invalid Regex Pattern: ' . $error_text . ' Please fix the regex pattern and try again.', 45);
228 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
229 return;
230 }
231 }
232
233 // Loop detection: refuse to persist a chain that would resolve back to any source
234 if (!in_array($redirect_type, [410, 451]) && $redirect_id <= 0) {
235 require_once dirname(__FILE__, 2) . '/redirections/class-metasync-redirection.php';
236 $redirection_helper = new Metasync_Redirection($this->db_redirection);
237 foreach (array_keys($sources_from) as $source_url) {
238 $loop_chain = [];
239 if ($redirection_helper->would_create_loop($source_url, $destination_url, $loop_chain)) {
240 set_transient(
241 'metasync_redirection_error',
242 'Redirect would create a loop: ' . implode(' → ', $loop_chain),
243 45
244 );
245 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
246 return;
247 }
248 }
249 }
250
251 $data = [
252 'sources_from' => serialize($sources_from),
253 'url_redirect_to' => $destination_url,
254 'http_code' => $redirect_type,
255 'status' => $status,
256 'pattern_type' => $pattern_type,
257 'regex_pattern' => $regex_pattern,
258 'description' => $description,
259 ];
260
261 try {
262 if ($redirect_id > 0) {
263 $result = $this->db_redirection->update($data, $redirect_id);
264 if ($result === false) {
265 throw new Exception('Failed to update redirection');
266 }
267 $message = 'Redirection updated successfully.';
268 } else {
269 $result = $this->db_redirection->add($data);
270 if ($result === false) {
271 throw new Exception('Failed to add redirection');
272 }
273 $message = 'Redirection added successfully.';
274 }
275
276 } catch (Exception $e) {
277 set_transient('metasync_redirection_error', 'Error saving redirection: ' . $e->getMessage(), 45);
278 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
279 }
280
281 set_transient('metasync_redirection_success', $message, 45);
282
283 $this->safe_redirect(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections'));
284 }
285
286 private function is_valid_url($url)
287 {
288 if (strpos($url, '/') === 0) {
289 return true;
290 }
291
292 return filter_var($url, FILTER_VALIDATE_URL) !== false;
293 }
294
295 private function add_tabbed_interface_assets()
296 {
297 ?>
298 <style>
299 /* Root Variables - Dashboard Color Scheme */
300 :root {
301 --dashboard-bg: #0f1419;
302 --dashboard-card-bg: #1a1f26;
303 --dashboard-card-hover: #222831;
304 --dashboard-text-primary: #ffffff;
305 --dashboard-text-secondary: #9ca3af;
306 --dashboard-accent: #3b82f6;
307 --dashboard-accent-hover: #2563eb;
308 --dashboard-success: #10b981;
309 --dashboard-warning: #f59e0b;
310 --dashboard-error: #ef4444;
311 --dashboard-border: #374151;
312 --dashboard-gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
313 --dashboard-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.1);
314 --dashboard-shadow-hover: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -2px rgba(0, 0, 0, 0.2);
315 }
316
317 .metasync-tabs {
318 margin: 20px 0;
319 background: var(--dashboard-card-bg);
320 border: 1px solid var(--dashboard-border);
321 border-radius: 12px;
322 padding: 6px;
323 box-shadow: var(--dashboard-shadow);
324 }
325
326 .metasync-tab-nav {
327 border-bottom: none;
328 margin: 0;
329 padding: 0;
330 display: flex;
331 gap: 4px;
332 background: transparent;
333 }
334
335 .metasync-tab-nav li {
336 display: inline-block;
337 margin: 0;
338 list-style: none;
339 }
340
341 .metasync-tab-nav a {
342 display: block;
343 padding: 12px 20px;
344 text-decoration: none;
345 color: var(--dashboard-text-secondary);
346 border-bottom: none;
347 border-radius: 8px;
348 transition: all 0.3s ease;
349 font-weight: 500;
350 font-size: 14px;
351 background: transparent;
352 position: relative;
353 overflow: hidden;
354 }
355
356 .metasync-tab-nav a:hover {
357 color: var(--dashboard-text-primary);
358 background: rgba(255, 255, 255, 0.05);
359 transform: translateY(-1px);
360 }
361
362 .metasync-tab-nav a.active {
363 color: var(--dashboard-text-primary);
364 background: var(--dashboard-card-hover);
365 border-bottom: none;
366 box-shadow: var(--dashboard-shadow);
367 transform: translateY(-1px);
368 font-weight: 600;
369 }
370
371 .metasync-tab-nav a.active::after {
372 content: '';
373 position: absolute;
374 bottom: 0;
375 left: 0;
376 right: 0;
377 height: 2px;
378 background: var(--dashboard-accent);
379 border-radius: 1px;
380 }
381
382 .metasync-tab-content {
383 display: none;
384 padding: 20px 0;
385 }
386
387 .metasync-tab-content.active {
388 display: block;
389 }
390 </style>
391
392 <script>
393 jQuery(document).ready(function($) {
394 function switchToTab(targetTab) {
395 $('.metasync-tab-nav a').removeClass('active');
396 $('.metasync-tab-nav a[data-tab="' + targetTab + '"]').addClass('active');
397
398 $('.metasync-tab-content').removeClass('active');
399 $('#' + targetTab + '-content').addClass('active');
400 }
401
402 function initializeTabs() {
403 var urlParams = new URLSearchParams(window.location.search);
404 var currentTab = urlParams.get('tab');
405
406 if (currentTab && (currentTab === 'redirections' || currentTab === '404-monitor')) {
407 switchToTab(currentTab);
408 } else {
409 switchToTab('redirections');
410 currentTab = 'redirections';
411 }
412
413 var needsCleanup = false;
414 if (currentTab === '404-monitor') {
415 if (urlParams.has('paged') || urlParams.has('paged_redir')) {
416 urlParams.delete('paged');
417 urlParams.delete('paged_redir');
418 needsCleanup = true;
419 }
420 } else if (currentTab === 'redirections') {
421 if (urlParams.has('paged') || urlParams.has('paged_404')) {
422 urlParams.delete('paged');
423 urlParams.delete('paged_404');
424 needsCleanup = true;
425 }
426 }
427
428 if (needsCleanup) {
429 var newUrl = window.location.pathname + '?' + urlParams.toString();
430 window.history.replaceState({}, '', newUrl);
431 }
432 }
433
434 initializeTabs();
435
436 setTimeout(initializeTabs, 100);
437
438 $('.metasync-tab-nav a').on('click', function(e) {
439 e.preventDefault();
440
441 var targetTab = $(this).data('tab');
442 switchToTab(targetTab);
443
444 var url = new URL(window.location);
445 url.searchParams.set('tab', targetTab);
446
447 url.searchParams.delete('paged');
448 url.searchParams.delete('paged_404');
449 url.searchParams.delete('paged_redir');
450
451 window.history.pushState({}, '', url);
452 });
453 });
454 </script>
455 <?php
456 }
457
458 private function render_tab_navigation($current_tab)
459 {
460 $base_url = admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-redirections');
461
462 ?>
463 <div class="metasync-tabs">
464 <ul class="metasync-tab-nav">
465 <li>
466 <a href="<?php echo esc_url($base_url . '&tab=redirections'); ?>"
467 data-tab="redirections"
468 class="<?php echo $current_tab === 'redirections' ? 'active' : ''; ?>">
469 Redirections
470 </a>
471 </li>
472 <li>
473 <a href="<?php echo esc_url($base_url . '&tab=404-monitor'); ?>"
474 data-tab="404-monitor"
475 class="<?php echo $current_tab === '404-monitor' ? 'active' : ''; ?>">
476 404 Monitor
477 </a>
478 </li>
479 </ul>
480 </div>
481 <?php
482 }
483
484 private function render_tab_content($current_tab)
485 {
486 $this->render_redirections_tab($current_tab);
487 $this->render_404_monitor_tab($current_tab);
488 }
489
490 private function render_redirections_tab($current_tab = null)
491 {
492 if ($current_tab === null) {
493 $current_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'redirections';
494 }
495 $active_class = $current_tab === 'redirections' ? 'active' : '';
496
497 ?>
498 <div id="redirections-content" class="metasync-tab-content <?php echo $active_class; ?>">
499 <?php
500 $redirection = new Metasync_Redirection($this->db_redirection);
501 $redirection->create_admin_redirection_interface();
502 ?>
503 </div>
504 <?php
505 }
506
507 private function render_404_monitor_tab($current_tab = null)
508 {
509 if ($current_tab === null) {
510 $current_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'redirections';
511 }
512 $active_class = $current_tab === '404-monitor' ? 'active' : '';
513
514 ?>
515 <div id="404-monitor-content" class="metasync-tab-content <?php echo $active_class; ?>">
516 <?php
517 try {
518 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor-database.php';
519 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor.php';
520
521 $db_404 = new Metasync_Error_Monitor_Database();
522 $ErrorMonitor = new Metasync_Error_Monitor($db_404);
523
524 $ErrorMonitor->create_admin_plugin_interface();
525
526 } catch (Exception $e) {
527 echo '<div class="notice notice-error"><p>Error loading 404 monitor: ' . esc_html($e->getMessage()) . '</p></div>';
528 error_log('MetaSync 404 Monitor Error: ' . $e->getMessage());
529 } catch (Error $e) {
530 echo '<div class="notice notice-error"><p>Fatal error loading 404 monitor: ' . esc_html($e->getMessage()) . '</p></div>';
531 error_log('MetaSync 404 Monitor Fatal Error: ' . $e->getMessage());
532 }
533 ?>
534 </div>
535 <?php
536 }
537
538 private function ensure_404_monitor_table()
539 {
540 global $wpdb;
541 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor-database.php';
542
543 $table_name = $wpdb->prefix . Metasync_Error_Monitor_Database::$table_name;
544
545 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) != $table_name) {
546 require_once plugin_dir_path(dirname(__FILE__)) . 'database/class-db-migrations.php';
547 MetaSync_DBMigration::run_migrations();
548 }
549 }
550
551 private function check_database_structure()
552 {
553 global $wpdb;
554 $table_name = $wpdb->prefix . 'metasync_redirections';
555
556 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) != $table_name) {
557 return;
558 }
559
560 $columns = $wpdb->get_col("DESCRIBE {$table_name}");
561 $required_columns = ['pattern_type', 'regex_pattern', 'description', 'created_at', 'updated_at', 'last_accessed_at'];
562
563 $missing_columns = array_diff($required_columns, $columns);
564
565 if (!empty($missing_columns)) {
566 add_action('admin_notices', function() use ($missing_columns) {
567 echo '<div class="notice notice-warning is-dismissible">';
568 echo '<p><strong>MetaSync:</strong> Database structure needs updating. Missing columns: ' . implode(', ', $missing_columns) . '</p>';
569 echo '<p><button type="button" class="button button-secondary" onclick="updateDatabaseStructure()">Update Database Structure</button></p>';
570 echo '</div>';
571
572 echo '<script>
573 function updateDatabaseStructure() {
574 if (confirm("This will update your database structure. Continue?")) {
575 const formData = new FormData();
576 formData.append("action", "metasync_update_db_structure");
577 formData.append("nonce", "' . wp_create_nonce('metasync_update_db_nonce') . '");
578
579 fetch(ajaxurl, {
580 method: "POST",
581 body: formData
582 })
583 .then(response => response.json())
584 .then(data => {
585 if (data.success) {
586 alert("Database structure updated successfully!");
587 location.reload();
588 } else {
589 alert("Error updating database: " + (data.data || "Unknown error"));
590 }
591 })
592 .catch(error => {
593 alert("Error updating database: " + error.message);
594 });
595 }
596 }
597 </script>';
598 });
599 }
600 }
601 }
602