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.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 / redirections / class-metasync-redirection-importer.php

class-metasync-redirection-importer.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.22, at redirections/class-metasync-redirection-importer.php

1,031 lines 35.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Import redirections from other SEO plugins.
5 *
6 * @link https://searchatlas.com
7 * @since 1.0.0
8 * @package Metasync
9 * @subpackage Metasync/redirections
10 * @author Engineering Team <[email protected]>
11 */
12
13 # Abort if this file is accessed directly.
14 if (!defined('ABSPATH')) {
15 exit;
16 }
17
18 class Metasync_Redirection_Importer
19 {
20 private $db_redirection;
21
22 private $redirection_helper = null;
23
24 /**
25 * Supported plugins for import
26 */
27 const SUPPORTED_PLUGINS = [
28 'redirection' => [
29 'name' => 'Redirection',
30 'slug' => 'redirection/redirection.php',
31 'table' => 'redirection_items'
32 ],
33 'yoast' => [
34 'name' => 'Yoast SEO Premium',
35 'slugs' => [
36 'wordpress-seo-premium/wp-seo-premium.php',
37 'wordpress-seo-premium-main/wp-seo-premium.php',
38 'wordpress-seo/wp-seo.php'
39 ],
40 'options' => [
41 'wpseo-premium-redirects-base',
42 'wpseo-premium-redirects-export-plain',
43 'wpseo-premium-redirects-export-regex',
44 'wpseo-premium-redirects',
45 'wpseo-premium-redirects-regex'
46 ]
47 ],
48 'rankmath' => [
49 'name' => 'Rank Math',
50 'slug' => 'seo-by-rank-math/rank-math.php',
51 'table' => 'rank_math_redirections'
52 ],
53 'aioseo' => [
54 'name' => 'All in One SEO',
55 'slug' => 'all-in-one-seo-pack/all_in_one_seo_pack.php',
56 'table' => 'aioseo_redirects'
57 ],
58 'simple301' => [
59 'name' => 'Simple 301 Redirects',
60 'slug' => 'simple-301-redirects/simple-301-redirects.php',
61 'option' => '301_redirects'
62 ]
63 ];
64
65 public function __construct(&$db_redirection)
66 {
67 $this->db_redirection = $db_redirection;
68 }
69
70 /**
71 * Lazy-load the Metasync_Redirection helper used for loop detection.
72 */
73 private function get_redirection_helper()
74 {
75 if ($this->redirection_helper === null) {
76 require_once dirname(__FILE__) . '/class-metasync-redirection.php';
77 $this->redirection_helper = new Metasync_Redirection($this->db_redirection);
78 }
79 return $this->redirection_helper;
80 }
81
82 /**
83 * Get list of available plugins for import
84 *
85 * @return array List of plugins with their status
86 */
87 public function get_available_plugins()
88 {
89 global $wpdb;
90 $available = [];
91
92 foreach (self::SUPPORTED_PLUGINS as $key => $plugin) {
93 $status = [
94 'key' => $key,
95 'name' => $plugin['name'],
96 'installed' => false,
97 'has_data' => false,
98 'count' => 0
99 ];
100
101 # Check if plugin is installed/active
102 if (isset($plugin['slug'])) {
103 $status['installed'] = is_plugin_active($plugin['slug']) || $this->plugin_exists($plugin['slug']);
104 } elseif (isset($plugin['slugs'])) {
105 # Check multiple possible slugs (for Yoast)
106 foreach ($plugin['slugs'] as $slug) {
107 if (is_plugin_active($slug) || $this->plugin_exists($slug)) {
108 $status['installed'] = true;
109 break;
110 }
111 }
112 }
113
114 # Check if data exists
115 if (isset($plugin['table'])) {
116 $table_name = $wpdb->prefix . $plugin['table'];
117 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name) {
118 $count = $wpdb->get_var("SELECT COUNT(*) FROM $table_name");
119 if ($count > 0) {
120 $status['has_data'] = true;
121 $status['count'] = (int)$count;
122 }
123 }
124 } elseif (isset($plugin['option'])) {
125 $option_data = get_option($plugin['option'], []);
126 if (!empty($option_data)) {
127 $status['has_data'] = true;
128 $status['count'] = is_array($option_data) ? count($option_data) : 0;
129 }
130 } elseif (isset($plugin['options'])) {
131 # Check multiple options (for Yoast)
132 $total_count = 0;
133 foreach ($plugin['options'] as $option_name) {
134 $option_data = get_option($option_name, []);
135 if (!empty($option_data)) {
136 $status['has_data'] = true;
137 if (is_array($option_data)) {
138 $total_count += count($option_data);
139 }
140 }
141 }
142 if ($total_count > 0) {
143 $status['count'] = $total_count;
144 }
145 }
146
147 $available[] = $status;
148 }
149
150 return $available;
151 }
152
153 /**
154 * Check if plugin exists (even if not active)
155 */
156 private function plugin_exists($plugin_path)
157 {
158 $plugin_file = WP_PLUGIN_DIR . '/' . $plugin_path;
159 return file_exists($plugin_file);
160 }
161
162 /**
163 * Import redirections from specified plugin
164 *
165 * @param string $plugin Plugin key
166 * @return array Result with success status and message
167 */
168 public function import_from_plugin($plugin)
169 {
170 if (!isset(self::SUPPORTED_PLUGINS[$plugin])) {
171 return [
172 'success' => false,
173 'message' => 'Unknown plugin specified.',
174 'imported' => 0,
175 'skipped' => 0
176 ];
177 }
178
179 $method = "import_from_{$plugin}";
180 if (method_exists($this, $method)) {
181 return $this->$method();
182 }
183
184 return [
185 'success' => false,
186 'message' => 'Import method not implemented.',
187 'imported' => 0,
188 'skipped' => 0
189 ];
190 }
191
192 /**
193 * Import from Redirection plugin
194 */
195 private function import_from_redirection()
196 {
197 global $wpdb;
198 $table_name = $wpdb->prefix . 'redirection_items';
199
200 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") !== $table_name) {
201 return [
202 'success' => false,
203 'message' => 'Redirection plugin table not found.',
204 'imported' => 0,
205 'skipped' => 0,
206 'loop_skipped' => 0
207 ];
208 }
209
210 $redirects = $wpdb->get_results("
211 SELECT * FROM $table_name
212 WHERE status = 'enabled'
213 ");
214
215 $imported = 0;
216 $skipped = 0;
217 $loop_skipped = 0;
218
219 foreach ($redirects as $redirect) {
220 $http_code = intval($redirect->action_code ?? 301);
221 $target_url = $redirect->action_data ?? '';
222
223 # For 410 and 451, target URL can be empty (they don't redirect)
224 if (empty($target_url) && !in_array($http_code, [410, 451])) {
225 $skipped++;
226 continue;
227 }
228
229 # Check if already exists
230 if ($this->redirection_exists($redirect->url)) {
231 $skipped++;
232 continue;
233 }
234
235 # Map Redirection plugin format to MetaSync format
236 $pattern_type = 'exact';
237 $regex_pattern = null;
238
239 # Check if it's a regex
240 if (isset($redirect->regex) && $redirect->regex == 1) {
241 $pattern_type = 'regex';
242 $regex_pattern = $redirect->url;
243 }
244
245 $sources = [
246 $redirect->url => $pattern_type
247 ];
248
249 $args = [
250 'sources_from' => serialize($sources),
251 'url_redirect_to' => $target_url,
252 'http_code' => $http_code,
253 'hits_count' => $redirect->hits ?? 0,
254 'status' => 'active',
255 'pattern_type' => $pattern_type,
256 'regex_pattern' => $regex_pattern,
257 'description' => 'Imported from Redirection plugin',
258 'created_at' => current_time('mysql'),
259 'updated_at' => current_time('mysql')
260 ];
261
262 if (!$this->is_safe_destination($target_url, $http_code)) {
263 $skipped++;
264 continue;
265 }
266 $args['url_redirect_to'] = $target_url;
267
268 if (!in_array($http_code, [410, 451]) && $this->get_redirection_helper()->validate_no_loop($redirect->url, $target_url) !== null) {
269 $loop_skipped++;
270 continue;
271 }
272
273 if ($this->db_redirection->add($args)) {
274 $imported++;
275 }
276 }
277
278 $message = "Successfully imported $imported redirections from Redirection plugin.";
279 if ($loop_skipped > 0) {
280 $message .= " Skipped $loop_skipped redirect(s) that would have created loops.";
281 }
282
283 return [
284 'success' => true,
285 'message' => $message,
286 'imported' => $imported,
287 'skipped' => $skipped,
288 'loop_skipped' => $loop_skipped
289 ];
290 }
291
292 /**
293 * Import from Yoast SEO Premium
294 *
295 * Yoast stores redirects in multiple options:
296 * - wpseo-premium-redirects-base (main storage since v3.1)
297 * - wpseo-premium-redirects-export-plain (plain redirects)
298 * - wpseo-premium-redirects-export-regex (regex redirects)
299 * - wpseo-premium-redirects (legacy plain)
300 * - wpseo-premium-redirects-regex (legacy regex)
301 */
302 private function import_from_yoast()
303 {
304 $imported = 0;
305 $skipped = 0;
306 $errors = [];
307 $loop_skipped = 0;
308
309 # Try to import from base option first (preferred method)
310 $base_redirects = get_option('wpseo-premium-redirects-base', []);
311
312 if (!empty($base_redirects) && is_array($base_redirects)) {
313 foreach ($base_redirects as $redirect) {
314 $result = $this->process_yoast_redirect_base($redirect);
315 if ($result === true) {
316 $imported++;
317 } elseif ($result === 'skipped') {
318 $skipped++;
319 } elseif ($result === 'loop_skipped') {
320 $loop_skipped++;
321 } else {
322 $errors[] = $result;
323 }
324 }
325 }
326
327 # If no base redirects, try export options
328 if ($imported === 0 && $skipped === 0) {
329 # Import plain redirects
330 $plain_redirects = get_option('wpseo-premium-redirects-export-plain', []);
331 if (!empty($plain_redirects) && is_array($plain_redirects)) {
332 foreach ($plain_redirects as $origin => $redirect_data) {
333 $result = $this->process_yoast_redirect_export($origin, $redirect_data, 'plain');
334 if ($result === true) {
335 $imported++;
336 } elseif ($result === 'skipped') {
337 $skipped++;
338 } elseif ($result === 'loop_skipped') {
339 $loop_skipped++;
340 } else {
341 $errors[] = $result;
342 }
343 }
344 }
345
346 # Import regex redirects
347 $regex_redirects = get_option('wpseo-premium-redirects-export-regex', []);
348 if (!empty($regex_redirects) && is_array($regex_redirects)) {
349 foreach ($regex_redirects as $origin => $redirect_data) {
350 $result = $this->process_yoast_redirect_export($origin, $redirect_data, 'regex');
351 if ($result === true) {
352 $imported++;
353 } elseif ($result === 'skipped') {
354 $skipped++;
355 } elseif ($result === 'loop_skipped') {
356 $loop_skipped++;
357 } else {
358 $errors[] = $result;
359 }
360 }
361 }
362 }
363
364 # If still no redirects, try legacy options
365 if ($imported === 0 && $skipped === 0) {
366 # Legacy plain redirects
367 $legacy_plain = get_option('wpseo-premium-redirects', []);
368 if (!empty($legacy_plain) && is_array($legacy_plain)) {
369 foreach ($legacy_plain as $origin => $redirect_data) {
370 $result = $this->process_yoast_redirect_export($origin, $redirect_data, 'plain');
371 if ($result === true) {
372 $imported++;
373 } elseif ($result === 'skipped') {
374 $skipped++;
375 } elseif ($result === 'loop_skipped') {
376 $loop_skipped++;
377 } else {
378 $errors[] = $result;
379 }
380 }
381 }
382
383 # Legacy regex redirects
384 $legacy_regex = get_option('wpseo-premium-redirects-regex', []);
385 if (!empty($legacy_regex) && is_array($legacy_regex)) {
386 foreach ($legacy_regex as $origin => $redirect_data) {
387 $result = $this->process_yoast_redirect_export($origin, $redirect_data, 'regex');
388 if ($result === true) {
389 $imported++;
390 } elseif ($result === 'skipped') {
391 $skipped++;
392 } elseif ($result === 'loop_skipped') {
393 $loop_skipped++;
394 } else {
395 $errors[] = $result;
396 }
397 }
398 }
399 }
400
401 # Return results
402 if ($imported === 0 && $skipped === 0 && $loop_skipped === 0) {
403 return [
404 'success' => false,
405 'message' => 'No Yoast redirections found. ' . (!empty($errors) ? implode(' ', array_slice($errors, 0, 2)) : ''),
406 'imported' => 0,
407 'skipped' => 0,
408 'loop_skipped' => 0,
409 'errors' => $errors
410 ];
411 }
412
413 $message = $imported > 0
414 ? "Successfully imported $imported redirections from Yoast SEO."
415 : "All redirections already exist. Skipped $skipped duplicates.";
416 if ($loop_skipped > 0) {
417 $message .= " Skipped $loop_skipped redirect(s) that would have created loops.";
418 }
419
420 return [
421 'success' => true,
422 'message' => $message,
423 'imported' => $imported,
424 'skipped' => $skipped,
425 'loop_skipped' => $loop_skipped,
426 'errors' => $errors
427 ];
428 }
429
430 /**
431 * Process a redirect from Yoast base option
432 *
433 * @param mixed $redirect Redirect data from base option
434 * @return bool|string True on success, 'skipped' if exists, error message on failure
435 */
436 private function process_yoast_redirect_base($redirect)
437 {
438 try {
439 # Base format: ['origin' => string, 'url' => string, 'type' => int, 'format' => string]
440 if (!is_array($redirect)) {
441 return 'Invalid redirect format';
442 }
443
444 $origin = isset($redirect['origin']) ? trim($redirect['origin']) : '';
445 $target = isset($redirect['url']) ? trim($redirect['url']) : '';
446 $type = isset($redirect['type']) ? $redirect['type'] : 301;
447 $format = isset($redirect['format']) ? $redirect['format'] : 'plain';
448
449 # Validate origin
450 if (empty($origin)) {
451 return 'Empty origin URL';
452 }
453
454 # For 410 and 451, target URL can be empty (they don't redirect)
455 if (empty($target) && !in_array($type, [410, 451])) {
456 return 'Empty target URL for non-410/451 redirect';
457 }
458
459 # Check if already exists
460 if ($this->redirection_exists($origin)) {
461 return 'skipped';
462 }
463
464 # Determine pattern type
465 $pattern_type = ($format === 'regex') ? 'regex' : 'exact';
466 $regex_pattern = ($format === 'regex') ? $origin : null;
467
468 $sources = [
469 $origin => $pattern_type
470 ];
471
472 $args = [
473 'sources_from' => serialize($sources),
474 'url_redirect_to' => $target,
475 'http_code' => intval($type),
476 'hits_count' => 0,
477 'status' => 'active',
478 'pattern_type' => $pattern_type,
479 'regex_pattern' => $regex_pattern,
480 'description' => 'Imported from Yoast SEO',
481 'created_at' => current_time('mysql'),
482 'updated_at' => current_time('mysql')
483 ];
484
485 if (!$this->is_safe_destination($target, intval($type))) {
486 return 'skipped';
487 }
488 $args['url_redirect_to'] = $target;
489
490 if (!in_array(intval($type), [410, 451]) && $this->get_redirection_helper()->validate_no_loop($origin, $target) !== null) {
491 return 'loop_skipped';
492 }
493
494 if ($this->db_redirection->add($args)) {
495 return true;
496 }
497
498 return 'Failed to insert redirect';
499
500 } catch (Exception $e) {
501 return 'Exception: ' . $e->getMessage();
502 }
503 }
504
505 /**
506 * Process a redirect from Yoast export options
507 *
508 * @param string $origin Source URL
509 * @param mixed $redirect_data Redirect data from export option
510 * @param string $format Format type ('plain' or 'regex')
511 * @return bool|string True on success, 'skipped' if exists, error message on failure
512 */
513 private function process_yoast_redirect_export($origin, $redirect_data, $format)
514 {
515 try {
516 $origin = trim($origin);
517
518 # Export format: ['url' => string, 'type' => int]
519 if (is_array($redirect_data)) {
520 $target = isset($redirect_data['url']) ? trim($redirect_data['url']) : '';
521 $type = isset($redirect_data['type']) ? $redirect_data['type'] : 301;
522 } elseif (is_string($redirect_data)) {
523 # Simple string format (target URL only)
524 $target = trim($redirect_data);
525 $type = 301;
526 } else {
527 return 'Invalid redirect data format';
528 }
529
530 # Validate origin
531 if (empty($origin)) {
532 return 'Empty origin URL';
533 }
534
535 # For 410 and 451, target URL can be empty (they don't redirect)
536 if (empty($target) && !in_array($type, [410, 451])) {
537 return 'Empty target URL for non-410/451 redirect';
538 }
539
540 # Check if already exists
541 if ($this->redirection_exists($origin)) {
542 return 'skipped';
543 }
544
545 # Determine pattern type
546 $pattern_type = ($format === 'regex') ? 'regex' : 'exact';
547 $regex_pattern = ($format === 'regex') ? $origin : null;
548
549 $sources = [
550 $origin => $pattern_type
551 ];
552
553 $args = [
554 'sources_from' => serialize($sources),
555 'url_redirect_to' => $target,
556 'http_code' => intval($type),
557 'hits_count' => 0,
558 'status' => 'active',
559 'pattern_type' => $pattern_type,
560 'regex_pattern' => $regex_pattern,
561 'description' => 'Imported from Yoast SEO',
562 'created_at' => current_time('mysql'),
563 'updated_at' => current_time('mysql')
564 ];
565
566 if (!$this->is_safe_destination($target, intval($type))) {
567 return 'skipped';
568 }
569 $args['url_redirect_to'] = $target;
570
571 if (!in_array(intval($type), [410, 451]) && $this->get_redirection_helper()->validate_no_loop($origin, $target) !== null) {
572 return 'loop_skipped';
573 }
574
575 if ($this->db_redirection->add($args)) {
576 return true;
577 }
578
579 return 'Failed to insert redirect';
580
581 } catch (Exception $e) {
582 return 'Exception: ' . $e->getMessage();
583 }
584 }
585
586 /**
587 * Import from Rank Math
588 */
589 private function import_from_rankmath()
590 {
591 global $wpdb;
592 $table_name = $wpdb->prefix . 'rank_math_redirections';
593
594 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") !== $table_name) {
595 return [
596 'success' => false,
597 'message' => 'Rank Math redirections table not found.',
598 'imported' => 0,
599 'skipped' => 0,
600 'loop_skipped' => 0,
601 'errors' => ['Table not found: ' . $table_name]
602 ];
603 }
604
605 # Get all redirections (not just active ones, Rank Math uses different status field)
606 $redirects = $wpdb->get_results("SELECT * FROM $table_name");
607
608 if (empty($redirects)) {
609 return [
610 'success' => false,
611 'message' => 'No redirections found in Rank Math.',
612 'imported' => 0,
613 'skipped' => 0,
614 'loop_skipped' => 0,
615 'errors' => []
616 ];
617 }
618
619 $imported = 0;
620 $skipped = 0;
621 $errors = [];
622 $loop_skipped = 0;
623
624 foreach ($redirects as $redirect) {
625 try {
626 # Rank Math uses 'sources' field - it's a serialized PHP array
627 $source_url = '';
628 $comparison_type = 'exact'; // Default to exact
629
630 if (isset($redirect->sources)) {
631 # First, try to unserialize (Rank Math format)
632 $unserialized = @maybe_unserialize($redirect->sources);
633
634 if (is_array($unserialized) && !empty($unserialized)) {
635 # Rank Math format: array of arrays with 'pattern', 'comparison', 'ignore' keys
636 $first_source = reset($unserialized);
637
638 if (is_array($first_source) && isset($first_source['pattern'])) {
639 # Extract the actual URL pattern
640 $source_url = $first_source['pattern'];
641
642 # Extract comparison type (exact, regex, contains, starts, ends)
643 if (isset($first_source['comparison'])) {
644 $comparison_type = $first_source['comparison'];
645 }
646 } elseif (is_string($first_source)) {
647 # Simple string format
648 $source_url = $first_source;
649 }
650 } elseif (is_string($unserialized)) {
651 # Direct string value
652 $source_url = $unserialized;
653 } else {
654 # Try as JSON
655 $parsed = json_decode($redirect->sources, true);
656 if (json_last_error() === JSON_ERROR_NONE && is_array($parsed)) {
657 $source_url = is_array($parsed) ? reset($parsed) : $parsed;
658 } else {
659 # Last resort - use as-is
660 $source_url = $redirect->sources;
661 }
662 }
663 } elseif (isset($redirect->url_from)) {
664 $source_url = $redirect->url_from;
665 }
666
667 # Clean up the source URL
668 $source_url = trim($source_url);
669
670 if (empty($source_url)) {
671 $errors[] = 'Empty source URL in redirect ID: ' . ($redirect->id ?? 'unknown');
672 $skipped++;
673 continue;
674 }
675
676 # Check if already exists
677 if ($this->redirection_exists($source_url)) {
678 $skipped++;
679 continue;
680 }
681
682 # Map Rank Math comparison types to MetaSync pattern types
683 # Rank Math: exact, regex, contains, starts, ends
684 # MetaSync: exact, regex, contain, start, end
685 $pattern_type_map = [
686 'exact' => 'exact',
687 'regex' => 'regex',
688 'contains' => 'contain',
689 'starts' => 'start',
690 'ends' => 'end'
691 ];
692
693 $pattern_type = isset($pattern_type_map[$comparison_type])
694 ? $pattern_type_map[$comparison_type]
695 : 'exact';
696
697 # For regex patterns, store the pattern
698 $regex_pattern = ($pattern_type === 'regex') ? $source_url : null;
699
700 $sources = [
701 $source_url => $pattern_type
702 ];
703
704 $target_url = $redirect->url_to ?? '';
705 $http_code = intval($redirect->header_code ?? 301);
706
707 # For 410 and 451, target URL can be empty (they don't redirect)
708 if (empty($target_url) && !in_array($http_code, [410, 451])) {
709 $errors[] = 'Empty target URL for source: ' . $source_url;
710 $skipped++;
711 continue;
712 }
713
714 $args = [
715 'sources_from' => serialize($sources),
716 'url_redirect_to' => $target_url,
717 'http_code' => $http_code,
718 'hits_count' => $redirect->hits ?? 0,
719 'status' => 'active',
720 'pattern_type' => $pattern_type,
721 'regex_pattern' => $regex_pattern,
722 'description' => 'Imported from Rank Math',
723 'created_at' => current_time('mysql'),
724 'updated_at' => current_time('mysql')
725 ];
726
727 if (!$this->is_safe_destination($target_url, $http_code)) {
728 $skipped++;
729 continue;
730 }
731 $args['url_redirect_to'] = $target_url;
732
733 if (!in_array($http_code, [410, 451]) && $this->get_redirection_helper()->validate_no_loop($source_url, $target_url) !== null) {
734 $loop_skipped++;
735 continue;
736 }
737
738 $result = $this->db_redirection->add($args);
739
740 if ($result !== false && $result > 0) {
741 $imported++;
742 } else {
743 $errors[] = 'Failed to insert redirect: ' . $source_url . ' -> ' . $target_url;
744 if ($wpdb->last_error) {
745 $errors[] = 'DB Error: ' . $wpdb->last_error;
746 }
747 }
748
749 } catch (Exception $e) {
750 $errors[] = 'Exception: ' . $e->getMessage();
751 $skipped++;
752 }
753 }
754
755 # Return success if we processed redirections (even if all skipped)
756 $has_results = ($imported + $skipped + $loop_skipped) > 0;
757
758 $message = $imported > 0
759 ? "Successfully imported $imported redirections from Rank Math."
760 : ($skipped > 0
761 ? "All redirections already exist. Skipped $skipped duplicates."
762 : "No redirections found to import. " . (count($errors) > 0 ? implode(' ', array_slice($errors, 0, 2)) : ''));
763 if ($loop_skipped > 0) {
764 $message .= " Skipped $loop_skipped redirect(s) that would have created loops.";
765 }
766
767 return [
768 'success' => $has_results,
769 'message' => $message,
770 'imported' => $imported,
771 'skipped' => $skipped,
772 'loop_skipped' => $loop_skipped,
773 'errors' => $errors
774 ];
775 }
776
777 /**
778 * Import from All in One SEO
779 */
780 private function import_from_aioseo()
781 {
782 global $wpdb;
783 $table_name = $wpdb->prefix . 'aioseo_redirects';
784
785 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") !== $table_name) {
786 return [
787 'success' => false,
788 'message' => 'All in One SEO redirects table not found.',
789 'imported' => 0,
790 'skipped' => 0,
791 'loop_skipped' => 0
792 ];
793 }
794
795 $redirects = $wpdb->get_results("
796 SELECT * FROM $table_name
797 WHERE enabled = 1
798 ");
799
800 $imported = 0;
801 $skipped = 0;
802 $loop_skipped = 0;
803
804 foreach ($redirects as $redirect) {
805 $http_code = intval($redirect->redirect_code ?? 301);
806 $target_url = $redirect->target_url ?? '';
807
808 # For 410 and 451, target URL can be empty (they don't redirect)
809 if (empty($target_url) && !in_array($http_code, [410, 451])) {
810 $skipped++;
811 continue;
812 }
813
814 # Check if already exists
815 if ($this->redirection_exists($redirect->source_url)) {
816 $skipped++;
817 continue;
818 }
819
820 # Determine pattern type
821 $pattern_type = 'exact';
822 $regex_pattern = null;
823
824 if (isset($redirect->regex) && $redirect->regex == 1) {
825 $pattern_type = 'regex';
826 $regex_pattern = $redirect->source_url;
827 }
828
829 $sources = [
830 $redirect->source_url => $pattern_type
831 ];
832
833 $args = [
834 'sources_from' => serialize($sources),
835 'url_redirect_to' => $target_url,
836 'http_code' => $http_code,
837 'hits_count' => $redirect->hits ?? 0,
838 'status' => 'active',
839 'pattern_type' => $pattern_type,
840 'regex_pattern' => $regex_pattern,
841 'description' => 'Imported from All in One SEO',
842 'created_at' => current_time('mysql'),
843 'updated_at' => current_time('mysql')
844 ];
845
846 if (!$this->is_safe_destination($target_url, $http_code)) {
847 $skipped++;
848 continue;
849 }
850 $args['url_redirect_to'] = $target_url;
851
852 if (!in_array($http_code, [410, 451]) && $this->get_redirection_helper()->validate_no_loop($redirect->source_url, $target_url) !== null) {
853 $loop_skipped++;
854 continue;
855 }
856
857 if ($this->db_redirection->add($args)) {
858 $imported++;
859 }
860 }
861
862 $message = "Successfully imported $imported redirections from All in One SEO.";
863 if ($loop_skipped > 0) {
864 $message .= " Skipped $loop_skipped redirect(s) that would have created loops.";
865 }
866
867 return [
868 'success' => true,
869 'message' => $message,
870 'imported' => $imported,
871 'skipped' => $skipped,
872 'loop_skipped' => $loop_skipped
873 ];
874 }
875
876 /**
877 * Import from Simple 301 Redirects
878 */
879 private function import_from_simple301()
880 {
881 # Simple 301 Redirects stores in options
882 $redirects = get_option('301_redirects', []);
883
884 if (empty($redirects)) {
885 return [
886 'success' => false,
887 'message' => 'No Simple 301 Redirects found.',
888 'imported' => 0,
889 'skipped' => 0,
890 'loop_skipped' => 0
891 ];
892 }
893
894 $imported = 0;
895 $skipped = 0;
896 $loop_skipped = 0;
897
898 foreach ($redirects as $old_url => $new_url) {
899 # Skip empty entries
900 if (empty($old_url) || empty($new_url)) {
901 $skipped++;
902 continue;
903 }
904
905 # Check if already exists
906 if ($this->redirection_exists($old_url)) {
907 $skipped++;
908 continue;
909 }
910
911 $sources = [
912 $old_url => 'exact'
913 ];
914
915 $args = [
916 'sources_from' => serialize($sources),
917 'url_redirect_to' => $new_url,
918 'http_code' => 301,
919 'hits_count' => 0,
920 'status' => 'active',
921 'pattern_type' => 'exact',
922 'regex_pattern' => null,
923 'description' => 'Imported from Simple 301 Redirects',
924 'created_at' => current_time('mysql'),
925 'updated_at' => current_time('mysql')
926 ];
927
928 if (!$this->is_safe_destination($new_url, 301)) {
929 $skipped++;
930 continue;
931 }
932 $args['url_redirect_to'] = $new_url;
933
934 if ($this->get_redirection_helper()->validate_no_loop($old_url, $new_url) !== null) {
935 $loop_skipped++;
936 continue;
937 }
938
939 if ($this->db_redirection->add($args)) {
940 $imported++;
941 }
942 }
943
944 $message = "Successfully imported $imported redirections from Simple 301 Redirects.";
945 if ($loop_skipped > 0) {
946 $message .= " Skipped $loop_skipped redirect(s) that would have created loops.";
947 }
948
949 return [
950 'success' => true,
951 'message' => $message,
952 'imported' => $imported,
953 'skipped' => $skipped,
954 'loop_skipped' => $loop_skipped
955 ];
956 }
957
958 /**
959 * Reject off-site destination URLs at import time so attacker-controlled
960 * exports cannot smuggle external redirects in via plugin migration.
961 *
962 * 410 and 451 entries have no destination, so they are always safe.
963 *
964 * @param string $url Destination URL from the source plugin.
965 * @param int $http_code HTTP status the redirect will serve.
966 * @return bool True when the destination is empty-by-design or resolves on-site.
967 */
968 private function is_safe_destination(&$url, $http_code)
969 {
970 if (in_array(intval($http_code), [410, 451], true)) {
971 $url = '';
972 return true;
973 }
974
975 $url = (string) $url;
976 if ($url === '') {
977 return false;
978 }
979
980 if (get_option('metasync_allow_external_redirects', 0)) {
981 return true;
982 }
983
984 return wp_validate_redirect($url, '') !== '';
985 }
986
987 /**
988 * Check if a redirection already exists
989 *
990 * @param string $source_url Source URL to check
991 * @return bool True if exists, false otherwise
992 */
993 private function redirection_exists($source_url)
994 {
995 global $wpdb;
996 $table_name = $wpdb->prefix . Metasync_Redirection_Database::$table_name;
997
998 $count = $wpdb->get_var($wpdb->prepare(
999 "SELECT COUNT(*) FROM $table_name WHERE sources_from LIKE %s",
1000 '%' . $wpdb->esc_like($source_url) . '%'
1001 ));
1002
1003 return $count > 0;
1004 }
1005
1006 /**
1007 * Get import statistics
1008 *
1009 * @return array Statistics about imports
1010 */
1011 public function get_import_stats()
1012 {
1013 global $wpdb;
1014 $table_name = $wpdb->prefix . Metasync_Redirection_Database::$table_name;
1015
1016 $stats = [
1017 'total_redirections' => 0,
1018 'imported_redirections' => 0
1019 ];
1020
1021 $stats['total_redirections'] = $wpdb->get_var("SELECT COUNT(*) FROM $table_name");
1022
1023 $stats['imported_redirections'] = $wpdb->get_var($wpdb->prepare(
1024 "SELECT COUNT(*) FROM $table_name WHERE description LIKE %s",
1025 'Imported from%'
1026 ));
1027
1028 return $stats;
1029 }
1030 }
1031