PluginProbe
404 Solution / 4.2.0
404 Solution v4.2.0
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 / SpellURLMatcher.php

SpellURLMatcher.php in 404 Solution 4.2.0, at includes/SpellURLMatcher.php

356 lines 10.4 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 * URL-level matching: regex-based matching, slug matching, image detection,
9 * permalink lookup, cache retrieval, and URL utility helpers.
10 *
11 * Extracted from SpellCheckerTrait_URLMatching as a standalone class with
12 * explicit dependency injection.
13 */
14 class ABJ_404_Solution_SpellURLMatcher {
15
16 /** @var ABJ_404_Solution_Functions */
17 private $f;
18
19 /** @var ABJ_404_Solution_PluginLogic */
20 private $logic;
21
22 /** @var ABJ_404_Solution_Logging */
23 private $logger;
24
25 /** @var ABJ_404_Solution_ContentRepository */
26 private $contentRepository;
27
28 /** @var mixed */
29 private $viewReadService;
30
31 /** @var string|int|null */
32 private $custom404PageID;
33
34 /** @var array<string, string> */
35 private array $preparedRegexPatternCache = array();
36
37 /**
38 * @param ABJ_404_Solution_Functions $functions
39 * @param ABJ_404_Solution_PluginLogic $logic
40 * @param ABJ_404_Solution_Logging $logger
41 * @param ABJ_404_Solution_ContentRepository $contentRepository
42 * @param mixed $viewReadService
43 * @param string|int|null $custom404PageID
44 */
45 public function __construct($functions, $logic, $logger, $contentRepository, $viewReadService, $custom404PageID) {
46 $this->f = $functions;
47 $this->logic = $logic;
48 $this->logger = $logger;
49 $this->contentRepository = $contentRepository;
50 $this->viewReadService = $viewReadService;
51 $this->custom404PageID = $custom404PageID;
52 }
53
54 /** @return array<int, array<string, mixed>> */
55 private function getRedirectsWithRegEx(): array {
56 if (!is_object($this->viewReadService) || !is_callable(array($this->viewReadService, 'getRedirectsWithRegEx'))) {
57 return array();
58 }
59 $rows = call_user_func(array($this->viewReadService, 'getRedirectsWithRegEx'));
60 return $this->normalizeRows($rows);
61 }
62
63 /** @return array<int, array<string, mixed>> */
64 private function getManualRedirectsWithRegexMetachars(): array {
65 if (!is_object($this->viewReadService) || !is_callable(array($this->viewReadService, 'getManualRedirectsWithRegexMetachars'))) {
66 return array();
67 }
68 $rows = call_user_func(array($this->viewReadService, 'getManualRedirectsWithRegexMetachars'));
69 return $this->normalizeRows($rows);
70 }
71
72 /**
73 * @param mixed $rows
74 * @return array<int, array<string, mixed>>
75 */
76 private function normalizeRows($rows): array {
77 if (!is_array($rows)) {
78 return array();
79 }
80 $normalized = array();
81 foreach ($rows as $row) {
82 if (is_array($row)) {
83 $normalized[] = $row;
84 }
85 }
86 return $normalized;
87 }
88
89 /** Find a match using the user-defined regex patterns.
90 * @param string $requestedURL
91 * @param array<string, mixed>|null $options
92 * @return array<string, mixed>|null
93 */
94 function getPermalinkUsingRegEx(string $requestedURL, $options = null) {
95 if (!is_array($options)) {
96 $options = $this->logic->getOptions();
97 }
98 $isDebug = $this->logger->isDebug();
99
100 $regexURLsRows = $this->getRedirectsWithRegEx();
101
102 $manualWithMetachars = $this->getManualRedirectsWithRegexMetachars();
103 if (!empty($manualWithMetachars)) {
104 $filtered = array();
105 foreach ($manualWithMetachars as $manualRow) {
106 $manualUrl = isset($manualRow['url']) && is_string($manualRow['url']) ? $manualRow['url'] : '';
107 if (!ABJ_404_Solution_RegexAutoPromote::looksLikeUnambiguousRegex($manualUrl)) {
108 continue;
109 }
110 $glob = ABJ_404_Solution_RegexAutoPromote::applyGlobFixup($manualUrl);
111 $manualRow['url'] = $glob['url'];
112 $filtered[] = $manualRow;
113 }
114 if (!empty($filtered)) {
115 if ($isDebug) {
116 $this->logger->debugMessage(
117 "Runtime regex fallback: trying " . count($filtered) .
118 " MANUAL row(s) with regex metachars against URL: " . $requestedURL
119 );
120 }
121 $regexURLsRows = array_merge($regexURLsRows, $filtered);
122 }
123 }
124
125 foreach ($regexURLsRows as $row) {
126 $regexURL = $row['url'];
127
128 if ($isDebug) {
129 abj_service('request_context')->debug_info = 'Applying custom regex "' . $regexURL . '" to URL: ' .
130 $requestedURL;
131 }
132 $regexURLStr = is_string($regexURL) ? $regexURL : '';
133 $preparedURL = $this->getPreparedRegexPattern($regexURLStr);
134 if (@$this->f->regexMatch($preparedURL, $requestedURL)) {
135 if ($isDebug) {
136 abj_service('request_context')->debug_info = 'Cleared after regex.';
137 }
138 $rowType = isset($row['type']) && is_scalar($row['type']) ? (int)$row['type'] : 0;
139 $rowDest = isset($row['final_dest']) && is_scalar($row['final_dest']) ? (string)$row['final_dest'] : '';
140 if ($rowType === (int)ABJ404_TYPE_EXTERNAL) {
141 $permalink = array(
142 'id' => 0,
143 'type' => ABJ404_TYPE_EXTERNAL,
144 'link' => $rowDest,
145 'title' => '',
146 'score' => 100,
147 );
148 } else {
149 $idAndType = $rowDest . '|' . $row['type'];
150 $permalink = ABJ_404_Solution_Functions::permalinkInfoToArray($idAndType, 0,
151 null, $options);
152 }
153 $permalink['matching_regex'] = $regexURL;
154 $permalink['code'] = isset($row['code']) && is_scalar($row['code']) ? (int)$row['code'] : 0;
155 $originalPermalink = $isDebug ? $permalink : null;
156
157 $permLinkStr = isset($permalink['link']) && is_string($permalink['link']) ? $permalink['link'] : '';
158 $hasCaptureGroup = ($this->f->strpos($regexURLStr, '(') !== false);
159 $hasReplacementToken = ($this->f->strpos($permLinkStr, '$') !== false);
160 if ($hasCaptureGroup && $hasReplacementToken) {
161 $results = array();
162 @$this->f->regexMatch($regexURLStr, $requestedURL, $results);
163
164 $final = $permLinkStr;
165 for ($x = 1; $x < count($results); $x++) {
166 $final = $this->f->str_replace('$' . $x, $results[$x], $final);
167 }
168
169 $permalink['link'] = $final;
170 }
171
172 if ($isDebug) {
173 $this->logger->debugMessage("Found matching regex. Original permalink" .
174 json_encode($originalPermalink) . ", final: " .
175 json_encode($permalink));
176 }
177
178 return $permalink;
179 }
180
181 if ($isDebug) {
182 abj_service('request_context')->debug_info = 'Cleared after regex.';
183 }
184 }
185
186 return null;
187 }
188
189 /**
190 * @param string $regexURL
191 * @return string
192 */
193 private function getPreparedRegexPattern($regexURL) {
194 if (isset($this->preparedRegexPatternCache[$regexURL])) {
195 return $this->preparedRegexPatternCache[$regexURL];
196 }
197
198 $prepared = $this->f->str_replace('/', '\/', $regexURL);
199 $this->preparedRegexPatternCache[$regexURL] = $prepared;
200 return $prepared;
201 }
202
203 /** Find a match using an exact slug match.
204 * @param string $requestedURL
205 * @return array<string, mixed>|null
206 */
207 function getPermalinkUsingSlug(string $requestedURL) {
208
209 $exploded = array_filter(explode('/', $requestedURL));
210 if (count($exploded) === 0) {
211 return null;
212 }
213 $postSlug = end($exploded);
214 $postsBySlugRows = $this->contentRepository->getPublishedPagesAndPostsIDs($postSlug);
215 if (count($postsBySlugRows) == 1) {
216 $post = reset($postsBySlugRows);
217 $postId = (is_object($post) && property_exists($post, 'id')) ? $post->id : null;
218 if ($postId === null) {
219 return null;
220 }
221 $permalink = array();
222 $permalink['id'] = $postId;
223 $permalink['type'] = ABJ404_TYPE_POST;
224 $permalink['score'] = 100;
225 $permalink['title'] = get_the_title($postId);
226 $permalink['link'] = get_permalink($postId);
227
228 return $permalink;
229
230 } else if (count($postsBySlugRows) > 1) {
231 $this->logger->debugMessage("More than one post found with the slug, so no redirect was " .
232 "created. Slug: " . $postSlug);
233 } else {
234 $this->logger->debugMessage("No posts or pages matching slug: " . esc_html($postSlug));
235 }
236
237 return null;
238 }
239
240 /**
241 * @param string $requestedURL
242 * @return bool
243 */
244 function requestIsForAnImage(string $requestedURL): bool {
245 $imageExtensions = array(".jpg", ".jpeg", ".gif", ".png", ".tif", ".tiff", ".bmp", ".pdf",
246 ".jif", ".jif", ".jp2", ".jpx", ".j2k", ".j2c", ".pcd");
247
248 $returnVal = false;
249
250 foreach ($imageExtensions as $extension) {
251 if ($this->f->endsWithCaseInsensitive($requestedURL, $extension)) {
252 $returnVal = true;
253 break;
254 }
255 }
256
257 return $returnVal;
258 }
259
260 /**
261 * @param array<int, object> $rowsAsObject
262 * @return array<int, array<string, mixed>>
263 */
264 function getOnlyIDandTermID(array $rowsAsObject): array {
265 $rows = array();
266 $objectRow = array_pop($rowsAsObject);
267 while ($objectRow != null) {
268 $rows[] = array(
269 'id' => property_exists($objectRow, 'id') == true ? $objectRow->id : null,
270 'term_id' => property_exists($objectRow, 'term_id') == true ? $objectRow->term_id : null,
271 'url' => property_exists($objectRow, 'url') == true ? $objectRow->url : null
272 );
273 $objectRow = array_pop($rowsAsObject);
274 }
275
276 return $rows;
277 }
278
279 /**
280 * @param string $requestedURL
281 * @return array<int|string, mixed>
282 */
283 function getFromPermalinkCache(string $requestedURL): array {
284 $ctx = abj_service('request_context');
285 if (!empty($ctx->permalinks_found)) {
286 $permalinks = json_decode($ctx->permalinks_found, true);
287 if (is_array($permalinks)) {
288 return $permalinks;
289 }
290 }
291
292 $returnValue = $this->contentRepository->getSpellingPermalinksFromCache($requestedURL);
293 if (is_array($returnValue) && !empty($returnValue)) {
294 return $returnValue;
295 }
296
297 return array();
298 }
299
300 /**
301 * @param int $id
302 * @param string $rowType
303 * @return string|null
304 * @throws Exception
305 */
306 function getPermalink($id, $rowType) {
307 if ($rowType == 'pages') {
308 $link = $this->contentRepository->getPermalinkFromCache($id);
309
310 if ($link === null || trim((string)$link) === '') {
311 $linkResult = get_the_permalink($id);
312 $link = ($linkResult !== false) ? $linkResult : null;
313 }
314 return $this->f->normalizeUrlString($link);
315
316 } else if ($rowType == 'tags') {
317 return $this->f->normalizeUrlString(get_tag_link($id));
318
319 } else if ($rowType == 'categories') {
320 return $this->f->normalizeUrlString(get_category_link($id));
321
322 } else if ($rowType == 'image') {
323 $src = wp_get_attachment_image_src($id, "attached-image");
324 if ($src == false || !is_array($src)) {
325 return null;
326 }
327 return $this->f->normalizeUrlString($src[0]);
328
329 } else {
330 throw new \Exception("Unknown row type ..."); // allow-raw-error: assertion, should never reach user
331 }
332 }
333
334 /** Turns "/abc/defg" into "defg"
335 * @param string $url
336 * @return string
337 */
338 function getLastURLPart($url) {
339 $parts = explode("/", $url);
340 $lastPart = '';
341 for ($i = count($parts) - 1; $i >= 0; $i--) {
342 $lastPart = $parts[$i];
343 if (trim($lastPart) != "") {
344 break;
345 }
346 }
347
348 if (trim($lastPart) == "") {
349 return $url;
350 }
351
352 return $lastPart;
353 }
354
355 }
356