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

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