PluginProbe
404 Solution / trunk
404 Solution vtrunk
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / import / CrossPluginImporter.php

CrossPluginImporter.php in 404 Solution trunk, at includes/import/CrossPluginImporter.php

186 lines 7.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * Cross-plugin redirect importer.
9 *
10 * Public entry point for the cross-plugin import feature. It detects installed
11 * redirect plugins, previews available redirects, and imports them into the
12 * 404 Solution redirects table. Reading and normalizing the foreign sources is
13 * delegated to {@see ABJ_404_Solution_ForeignRedirectSourceReader} (data
14 * access); this class owns only the business logic of turning a normalized row
15 * into a 404 Solution redirect (status/type resolution, RedirectSpec creation).
16 *
17 * Supported sources:
18 * - Rank Math (rank_math_redirections)
19 * - Yoast SEO Premium (yoast_seo_redirects)
20 * - AIOSEO (aioseo_redirects)
21 * - Safe Redirect Manager (redirect_rule CPT)
22 * - Redirection plugin (redirection_items)
23 */
24 class ABJ_404_Solution_CrossPluginImporter {
25
26 /** @var ABJ_404_Solution_RedirectsRepositoryInterface */
27 private $redirectsRepository;
28
29 /** @var ABJ_404_Solution_Logging */
30 private $logger;
31
32 /** @var ABJ_404_Solution_ForeignRedirectSourceReader */
33 private $sourceReader;
34
35 /**
36 * @param ABJ_404_Solution_RedirectsRepositoryInterface $redirectsRepository
37 * @param ABJ_404_Solution_Logging $logger
38 * @param ABJ_404_Solution_DatabaseQueryInterface|null $dbQuery
39 */
40 public function __construct($redirectsRepository, $logger, $dbQuery = null) {
41 $this->redirectsRepository = $redirectsRepository;
42 $this->logger = $logger;
43 $this->sourceReader = new ABJ_404_Solution_ForeignRedirectSourceReader(
44 $redirectsRepository,
45 $logger,
46 $dbQuery
47 );
48 }
49
50 /**
51 * Detect which source plugins are installed by checking for their DB tables
52 * (or, for Safe Redirect Manager, by checking for the CPT).
53 *
54 * @return array<string, bool> e.g. ['rankmath' => true, 'redirection' => false, ...]
55 */
56 public function detectInstalledPlugins(): array {
57 return $this->sourceReader->detectInstalledPlugins();
58 }
59
60 /**
61 * Return a preview of redirects available from the given source plugin.
62 *
63 * Consumes {@see ABJ_404_Solution_ForeignRedirectSourceReader::readSource()}
64 * as a generator and stops as soon as $previewLimit rows are collected,
65 * rather than materializing the full source (M502, 2026-07-14) --
66 * readSource() only fetches one IMPORT_PAGE_SIZE page of rows per query,
67 * so a small $previewLimit (the AJAX preview UI's default) typically
68 * needs only a single page fetch regardless of the source's true size.
69 *
70 * @param string $source One of 'rankmath', 'yoast', 'aioseo', 'safe-redirect-manager', 'redirection'
71 * @param int $previewLimit Maximum rows to return for preview
72 * @return array<int, array<string, mixed>>
73 */
74 public function getImportPreview(string $source, int $previewLimit = 10): array {
75 if ($previewLimit <= 0) {
76 return array();
77 }
78
79 $preview = array();
80 foreach ($this->sourceReader->readSource($source) as $row) {
81 $preview[] = $row;
82 if (count($preview) >= $previewLimit) {
83 break;
84 }
85 }
86 return $preview;
87 }
88
89 /**
90 * Count redirects available from the given source plugin without
91 * materializing the full row set. Used by the AJAX preview handler,
92 * which only needs a number to display, not actual rows -- unlike
93 * getImportPreview() above, this never reads more than the source
94 * plugin's own storage needs to answer "how many" (see
95 * {@see ABJ_404_Solution_ForeignRedirectSourceReader::countSource()}).
96 *
97 * @param string $source One of 'rankmath', 'yoast', 'aioseo',
98 * 'safe-redirect-manager', 'redirection'
99 * @return int
100 */
101 public function countImportable(string $source): int {
102 return $this->sourceReader->countSource($source);
103 }
104
105 /**
106 * Import all redirects from the given source plugin.
107 * Returns the number of redirects successfully imported.
108 *
109 * Consumes {@see ABJ_404_Solution_ForeignRedirectSourceReader::readSource()}
110 * row-by-row as a generator rather than requiring a fully materialized
111 * array upfront (M502, 2026-07-14): each row is written via
112 * setupRedirect() as soon as it is read, so at most one source page is
113 * ever held in memory regardless of how many rows the source plugin has.
114 *
115 * @param string $source
116 * @return int
117 */
118 public function importFrom(string $source): int {
119 $imported = 0;
120 foreach ($this->sourceReader->readSource($source) as $row) {
121 $sourceUrl = isset($row['source_url']) && is_string($row['source_url']) ? $row['source_url'] : '';
122 $destUrl = isset($row['dest_url']) && is_string($row['dest_url']) ? $row['dest_url'] : '';
123 $code = isset($row['code']) && is_numeric($row['code']) ? (int)$row['code'] : 301;
124 $isRegex = isset($row['is_regex']) && (bool)$row['is_regex'];
125
126 if ($sourceUrl === '' || $destUrl === '') {
127 continue;
128 }
129
130 $status = $isRegex ? ABJ404_STATUS_REGEX : ABJ404_STATUS_MANUAL;
131
132 // Determine destination type and resolve internal paths to post IDs.
133 $resolved = $this->resolveDestinationType($destUrl);
134 $type = $resolved['type'];
135 $destUrl = $resolved['dest'];
136
137 $result = $this->redirectsRepository->setupRedirect(
138 ABJ_404_Solution_RedirectSpec::create(
139 $sourceUrl,
140 (string)$status,
141 (string)$type,
142 $destUrl,
143 (string)$code,
144 0
145 )
146 );
147
148 if ($result !== 0 && $result !== false) {
149 $imported++;
150 }
151 }
152
153 $this->logger->infoMessage(
154 'CrossPluginImporter: imported ' . $imported . ' redirect(s) from "' . $source . '".'
155 );
156
157 return $imported;
158 }
159
160 /**
161 * Resolve the redirect type and final destination for a given URL.
162 *
163 * External URLs (http/https) use ABJ404_TYPE_EXTERNAL with the URL as-is.
164 * Internal paths are resolved via url_to_postid() — if a post ID is found,
165 * ABJ404_TYPE_POST is used with the numeric ID. Otherwise ABJ404_TYPE_EXTERNAL
166 * is used so the path is preserved and used as-is by the redirect pipeline.
167 *
168 * @param string $destUrl
169 * @return array{type: int, dest: string}
170 */
171 private function resolveDestinationType(string $destUrl): array {
172 if (preg_match('/^https?:\/\//i', $destUrl)) {
173 return array('type' => ABJ404_TYPE_EXTERNAL, 'dest' => $destUrl);
174 }
175
176 if (function_exists('url_to_postid') && function_exists('home_url')) {
177 $postId = url_to_postid(home_url($destUrl));
178 if ($postId > 0) {
179 return array('type' => ABJ404_TYPE_POST, 'dest' => (string)$postId);
180 }
181 }
182
183 return array('type' => ABJ404_TYPE_EXTERNAL, 'dest' => $destUrl);
184 }
185 }
186