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

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