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 / services / ImportRedirectRowProcessor.php

ImportRedirectRowProcessor.php in 404 Solution trunk, at includes/services/ImportRedirectRowProcessor.php

327 lines 12.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 * Validates and persists one canonical redirect-import row.
9 */
10 class ABJ_404_Solution_ImportRedirectRowProcessor {
11
12 /** @var ABJ_404_Solution_RedirectsRepositoryInterface */
13 private $redirectsRepository;
14
15 /** @var ABJ_404_Solution_ContentRepositoryInterface */
16 private $contentRepository;
17
18 /** @var ABJ_404_Solution_Logging */
19 private $logger;
20
21 /**
22 * @param ABJ_404_Solution_RedirectsRepositoryInterface $redirectsRepository
23 * @param ABJ_404_Solution_ContentRepositoryInterface $contentRepository
24 */
25 function __construct($redirectsRepository, $contentRepository, ABJ_404_Solution_Logging $logger) {
26 $this->redirectsRepository = $redirectsRepository;
27 $this->contentRepository = $contentRepository;
28 $this->logger = $logger;
29 }
30
31 /**
32 * @param array<string, mixed> $dataArray
33 * @param bool $dryRun
34 * @param bool $overwriteExisting
35 * @return array<int, string>
36 */
37 function loadDataArrayFromFile($dataArray, $dryRun = false, $overwriteExisting = false): array {
38 $fromURL = isset($dataArray['from_url']) && is_string($dataArray['from_url']) ? $dataArray['from_url'] : '';
39 if ($fromURL === 'from_url' || $fromURL === 'request') {
40 return array();
41 }
42
43 $explicitRegex = $this->isExplicitRegexRow($dataArray);
44 $source = $this->resolveSourceAndStatus($fromURL, $explicitRegex);
45 $fromURL = $source['from_url'];
46 $status = $source['status'];
47 $final_dest = isset($dataArray['to_url']) && is_string($dataArray['to_url']) ? $dataArray['to_url'] : '';
48
49 $regexIssues = $this->regexValidationIssues($dataArray, $fromURL, $explicitRegex);
50 if (count($regexIssues) > 0) {
51 return $regexIssues;
52 }
53
54 $existingId = $this->existingRedirectId($fromURL);
55 $duplicateIssues = $this->duplicateIssues($existingId, $overwriteExisting, $fromURL);
56 if (count($duplicateIssues) > 0) {
57 return $duplicateIssues;
58 }
59
60 $destination = $this->resolveDestination($final_dest);
61 if (count($destination['issues']) > 0) {
62 return $destination['issues'];
63 }
64
65 if (!$dryRun) {
66 $this->writeRedirect($dataArray, $existingId, $overwriteExisting,
67 $fromURL, $status, $destination['type'], $destination['final_dest']);
68 }
69
70 return array();
71 }
72
73 /**
74 * @param string $fromURL
75 * @param bool $explicitRegex
76 * @return array{from_url: string, status: int}
77 */
78 private function resolveSourceAndStatus(string $fromURL, bool $explicitRegex): array {
79 $status = $explicitRegex ? ABJ404_STATUS_REGEX : ABJ404_STATUS_MANUAL;
80 if (!$explicitRegex &&
81 ABJ_404_Solution_RegexAutoPromote::looksLikeUnambiguousRegex($fromURL)) {
82 $status = ABJ404_STATUS_REGEX;
83 $glob = ABJ_404_Solution_RegexAutoPromote::applyGlobFixup($fromURL);
84 $fromURL = $glob['url'];
85 }
86 return array('from_url' => $fromURL, 'status' => (int)$status);
87 }
88
89 /**
90 * @param array<string, mixed> $dataArray
91 * @param string $fromURL
92 * @param bool $explicitRegex
93 * @return array<int, string>
94 */
95 private function regexValidationIssues(array $dataArray, string $fromURL, bool $explicitRegex): array {
96 if (!$explicitRegex) {
97 return array();
98 }
99 $patternError = $this->validateRegexPattern($fromURL);
100 if ($patternError === '') {
101 return array();
102 }
103
104 $lineNumber = $this->intFromArray($dataArray, '__line_number');
105 $msg = $lineNumber > 0
106 ? sprintf(__('Invalid regex pattern at line %d: %s (%s)', '404-solution'),
107 $lineNumber, $fromURL, $patternError)
108 : sprintf(__('Invalid regex pattern: %s (%s)', '404-solution'),
109 $fromURL, $patternError);
110 $this->logger->warn($msg);
111 return array($msg);
112 }
113
114 /**
115 * @param string $fromURL
116 * @return int
117 */
118 private function existingRedirectId(string $fromURL): int {
119 $existing = $this->redirectsRepository->getExistingRedirectForURL($fromURL);
120 if (!is_array($existing) || !isset($existing['id']) || !is_numeric($existing['id'])) {
121 return 0;
122 }
123 return (int)$existing['id'];
124 }
125
126 /**
127 * @param int $existingId
128 * @param bool $overwriteExisting
129 * @param string $fromURL
130 * @return array<int, string>
131 */
132 private function duplicateIssues(int $existingId, bool $overwriteExisting, string $fromURL): array {
133 if ($existingId === 0 || $overwriteExisting) {
134 return array();
135 }
136 $msg = __('Ignored importing redirect because a redirect with the same from URL already exists. URL:', '404-solution') . ' ' . $fromURL;
137 $this->logger->warn($msg);
138 return array($msg);
139 }
140
141 /**
142 * @param string $finalDest
143 * @return array{type: int, final_dest: string|int, issues: array<int, string>}
144 */
145 private function resolveDestination(string $finalDest): array {
146 if ($finalDest === '') {
147 return array('type' => ABJ404_TYPE_404_DISPLAYED, 'final_dest' => ABJ404_TYPE_404_DISPLAYED, 'issues' => array());
148 }
149 if ($finalDest === '5') {
150 return array('type' => ABJ404_TYPE_HOME, 'final_dest' => ABJ404_TYPE_HOME, 'issues' => array());
151 }
152 if (strpos($finalDest, 'http') !== false) {
153 return array('type' => ABJ404_TYPE_EXTERNAL, 'final_dest' => $finalDest, 'issues' => array());
154 }
155 if (strpos($finalDest, '/') !== 0) {
156 $msg = __('Unrecognized destination type while importing file. Destination:', '404-solution') . ' ' . $finalDest;
157 $this->logger->warn($msg);
158 return array('type' => ABJ404_TYPE_EXTERNAL, 'final_dest' => $finalDest, 'issues' => array($msg));
159 }
160
161 return $this->resolveSlugDestination($finalDest);
162 }
163
164 /**
165 * @param string $finalDest
166 * @return array{type: int, final_dest: string|int, issues: array<int, string>}
167 */
168 private function resolveSlugDestination(string $finalDest): array {
169 $slug = trim($finalDest, '/');
170 $postsFromSlugRows = $this->contentRepository->getPublishedPagesAndPostsIDs(array('slug' => $slug));
171 $postsFromCategoryRows = $this->contentRepository->getPublishedCategories(null, $slug);
172 $postsFromTagRows = $this->contentRepository->getPublishedTags($slug);
173
174 /** @var object{id?: int|string, term_id?: int|string}|null $postFromSlug */
175 $postFromSlug = isset($postsFromSlugRows[0]) ? $postsFromSlugRows[0] : null;
176 /** @var object{term_id?: int|string}|null $postFromCategory */
177 $postFromCategory = isset($postsFromCategoryRows[0]) ? $postsFromCategoryRows[0] : null;
178 /** @var object{term_id?: int|string}|null $postFromTag */
179 $postFromTag = isset($postsFromTagRows[0]) ? $postsFromTagRows[0] : null;
180
181 if ($postFromSlug && isset($postFromSlug->id)) {
182 return array('type' => $this->typeConstant('ABJ404_TYPE_POST', 1),
183 'final_dest' => (string)$postFromSlug->id, 'issues' => array());
184 }
185 if ($postFromCategory && isset($postFromCategory->term_id)) {
186 return array('type' => $this->typeConstant('ABJ404_TYPE_CAT', 2),
187 'final_dest' => (string)$postFromCategory->term_id, 'issues' => array());
188 }
189 if ($postFromTag && isset($postFromTag->term_id)) {
190 return array('type' => $this->typeConstant('ABJ404_TYPE_TAG', 3),
191 'final_dest' => (string)$postFromTag->term_id, 'issues' => array());
192 }
193
194 $this->logger->warn(__("Couldn't find post from slug. slug:", '404-solution') . ' ' . $slug);
195 return array('type' => ABJ404_TYPE_EXTERNAL, 'final_dest' => $finalDest, 'issues' => array());
196 }
197
198 /**
199 * @param array<string, mixed> $dataArray
200 * @param int $existingId
201 * @param bool $overwriteExisting
202 * @param string $fromURL
203 * @param int $status
204 * @param int $type
205 * @param string|int $finalDest
206 * @return void
207 */
208 private function writeRedirect(array $dataArray, int $existingId, bool $overwriteExisting,
209 string $fromURL, int $status, int $type, $finalDest): void {
210 $engine = isset($dataArray['engine']) && is_string($dataArray['engine']) && $dataArray['engine'] !== ''
211 ? $dataArray['engine'] : 'import';
212 $code = $this->redirectCode($dataArray);
213
214 if ($existingId !== 0 && $overwriteExisting) {
215 $this->redirectsRepository->updateRedirect(ABJ_404_Solution_RedirectUpdate::fromArray(array(
216 'id' => $existingId,
217 'type' => $type,
218 'fromUrl' => $fromURL,
219 'destination' => (string)$finalDest,
220 'code' => $code,
221 'statusType' => (string)$status,
222 )));
223 return;
224 }
225
226 $this->redirectsRepository->setupRedirect(ABJ_404_Solution_RedirectSpec::fromArray(array(
227 'fromURL' => $fromURL,
228 'status' => (string)$status,
229 'type' => (string)$type,
230 'finalDest' => (string)$finalDest,
231 'code' => $code,
232 'disabled' => 0,
233 'engine' => $engine,
234 )));
235 }
236
237 /**
238 * @param array<string, mixed> $dataArray
239 * @return string
240 */
241 private function redirectCode(array $dataArray): string {
242 $rawCode = $dataArray['code'] ?? null;
243 return is_scalar($rawCode) && is_numeric($rawCode) ? (string)(int)$rawCode : '301';
244 }
245
246 /**
247 * @param array<string, mixed> $dataArray
248 * @param string $key
249 * @return int
250 */
251 private function intFromArray(array $dataArray, string $key): int {
252 $value = $dataArray[$key] ?? 0;
253 return is_numeric($value) ? (int)$value : 0;
254 }
255
256 /**
257 * @param string $name
258 * @param int $default
259 * @return int
260 */
261 private function typeConstant(string $name, int $default): int {
262 $value = defined($name) ? constant($name) : $default;
263 return is_numeric($value) ? (int)$value : $default;
264 }
265
266 /**
267 * @param string $fromUrl
268 * @return string
269 */
270 private function validateRegexPattern(string $fromUrl): string {
271 if ($fromUrl === '') {
272 return __('pattern is empty', '404-solution');
273 }
274
275 $prepared = str_replace('/', '\/', $fromUrl);
276 $delimA = '{';
277 $delimB = '}';
278 if (strpos($prepared, '}') !== false) {
279 $candidates = array('`', '^', '|', '~', '!', ';', ':', ',', '@', "'", '/');
280 $picked = null;
281 foreach ($candidates as $c) {
282 if (strpos($prepared, $c) === false) {
283 $picked = $c;
284 break;
285 }
286 }
287 if ($picked === null) {
288 return __('cannot find a safe delimiter character', '404-solution');
289 }
290 $delimA = $delimB = $picked;
291 }
292
293 $compiled = $delimA . $prepared . $delimB;
294 $result = @preg_match($compiled, '');
295 if ($result === false) {
296 $errMsg = function_exists('preg_last_error_msg')
297 ? preg_last_error_msg()
298 : 'preg_match compilation failed';
299 return $errMsg;
300 }
301 return '';
302 }
303
304 /**
305 * @param array<string, mixed> $dataArray
306 * @return bool
307 */
308 private function isExplicitRegexRow(array $dataArray): bool {
309 if (isset($dataArray['status']) && is_scalar($dataArray['status'])) {
310 $raw = strtolower(trim((string)$dataArray['status']));
311 if ($raw === 'regex') {
312 return true;
313 }
314 if (is_numeric($raw) && (int)$raw === (int)ABJ404_STATUS_REGEX) {
315 return true;
316 }
317 }
318 if (isset($dataArray['regex']) && is_scalar($dataArray['regex'])) {
319 $raw = strtolower(trim((string)$dataArray['regex']));
320 if ($raw === '1' || $raw === 'true' || $raw === 'yes') {
321 return true;
322 }
323 }
324 return false;
325 }
326 }
327