PluginProbe
404 Solution / 4.3.0
404 Solution v4.3.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 / spelling / SpellURLMatcher.php

SpellURLMatcher.php in 404 Solution 4.3.0, at includes/spelling/SpellURLMatcher.php

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