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 / PluginLogicUrlNormalization.php

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

438 lines 13.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 if (!defined('ABSPATH')) {
5 exit;
6 }
7
8 /**
9 * URL normalization and multilingual redirect translation helpers.
10 * Standalone class extracted from PluginLogicTrait_UrlNormalization.
11 */
12 class ABJ_404_Solution_PluginLogicUrlNormalization {
13
14 /** @var ABJ_404_Solution_Functions */
15 private $f;
16
17 /** @var string */
18 private $urlHomeDirectory;
19
20 /** @var int */
21 private $urlHomeDirectoryLength;
22
23 /**
24 * @param ABJ_404_Solution_Functions $f
25 * @param string $urlHomeDirectory
26 * @param int $urlHomeDirectoryLength
27 */
28 function __construct($f, $urlHomeDirectory, $urlHomeDirectoryLength) {
29 $this->f = $f;
30 $this->urlHomeDirectory = $urlHomeDirectory;
31 $this->urlHomeDirectoryLength = $urlHomeDirectoryLength;
32 }
33
34 /** If a page's URL is /blogName/pageName then this returns /pageName.
35 * @param string|null $urlRequest
36 * @return string
37 */
38 function removeHomeDirectory($urlRequest): string {
39 if ($urlRequest === null) {
40 return '';
41 }
42 $f = $this->f;
43 $urlHomeDirectory = $this->urlHomeDirectory;
44 $homeLen = $this->urlHomeDirectoryLength;
45
46 if ($homeLen === 0) {
47 return $urlRequest;
48 }
49
50 if ($this->f->substr($urlRequest, 0, $homeLen) == $urlHomeDirectory) {
51 $nextChar = $this->f->substr($urlRequest, $homeLen, 1);
52 if ($nextChar === '/' || $nextChar === '?' || $nextChar === '#' || $nextChar === '') {
53 if ($nextChar === '/' || $nextChar === '') {
54 $urlRequest = $this->f->substr($urlRequest, ($homeLen + 1));
55 } else {
56 $urlRequest = '/' . $this->f->substr($urlRequest, $homeLen);
57 }
58 }
59 }
60
61 return $urlRequest;
62 }
63
64 /**
65 * Normalize URL to relative path by removing WordPress subdirectory.
66 *
67 * @param string|null $url Full URL or path
68 * @return string Relative path without subdirectory
69 */
70 function normalizeToRelativePath($url): string {
71 if ($url === '') {
72 return '/';
73 }
74
75 if ($url === null) {
76 return '/';
77 }
78 $url = trim($url);
79
80 if (preg_match('#^https?://#i', $url)) {
81 $parsed = parse_url($url);
82 if ($parsed === false || !isset($parsed['path'])) {
83 return '/';
84 }
85 $url = $parsed['path'];
86 if (!empty($parsed['query'])) {
87 $url .= '?' . $parsed['query'];
88 }
89 if (!empty($parsed['fragment'])) {
90 $url .= '#' . $parsed['fragment'];
91 }
92 }
93
94 if (strpos($url, '//') === 0) {
95 $parsed = parse_url('http:' . $url);
96 if ($parsed !== false && isset($parsed['path'])) {
97 $url = $parsed['path'];
98 if (!empty($parsed['query'])) {
99 $url .= '?' . $parsed['query'];
100 }
101 if (!empty($parsed['fragment'])) {
102 $url .= '#' . $parsed['fragment'];
103 }
104 } else {
105 return '/';
106 }
107 }
108
109 $relativePath = $this->removeHomeDirectory($url);
110
111 if ($relativePath === '') {
112 return '/';
113 }
114
115 $relativePathCleaned = preg_replace('#/+#', '/', $relativePath);
116 $relativePath = is_string($relativePathCleaned) ? $relativePathCleaned : $relativePath;
117
118 $relativePath = '/' . ltrim($relativePath, '/');
119
120 return $relativePath;
121 }
122
123 /**
124 * Normalize a user-provided path for storage/matching.
125 *
126 * @param string|null $url
127 * @return string
128 */
129 function normalizeUserProvidedPath($url) {
130 $url = $this->f->normalizeUrlString($url);
131 if ($url === '') {
132 return '';
133 }
134
135 return $this->normalizeToRelativePath($url);
136 }
137
138 /**
139 * Normalize an external destination URL for storage.
140 *
141 * @param string|null $url
142 * @return string
143 */
144 function normalizeExternalDestinationUrl($url) {
145 return $this->f->normalizeUrlString($url);
146 }
147
148 /**
149 * Generate normalized lookup variants for URL matching.
150 *
151 * @param string|null $url
152 * @return array<int, string>
153 */
154 function getNormalizedUrlCandidates($url) {
155 $decoded = $this->normalizeUserProvidedPath($url);
156 if ($decoded === '') {
157 return array();
158 }
159
160 $candidates = array($decoded);
161
162 $lower = function_exists('mb_strtolower') ? mb_strtolower($decoded, 'UTF-8') : strtolower($decoded);
163 if ($lower !== $decoded) {
164 $candidates[] = $lower;
165 }
166
167 $encoded = $this->normalizeToRelativePath($this->f->encodeUrlForLegacyMatch($decoded));
168 if ($encoded !== $decoded) {
169 $candidates[] = $encoded;
170 }
171
172 return array_values(array_unique($candidates));
173 }
174
175 /**
176 * Translate a redirect destination URL to the current language when possible.
177 *
178 * @param string $location Full URL or path to redirect to.
179 * @param string $requestedURL Original requested path/URL that triggered the 404.
180 * @return string URL to use for redirect.
181 */
182 function maybeTranslateRedirectUrl($location, $requestedURL = '') {
183 if (!is_string($location) || $location === '') {
184 return $location;
185 }
186
187 $translated = $this->translatePressRedirectUrl($location, $requestedURL);
188 if ($translated !== null && $translated !== '') {
189 $location = $translated;
190 }
191
192 if ($translated === null || $translated === '') {
193 $translated = $this->wpmlRedirectUrl($location, $requestedURL);
194 if ($translated !== null && $translated !== '') {
195 $location = $translated;
196 }
197 }
198
199 if ($translated === null || $translated === '') {
200 $translated = $this->polylangRedirectUrl($location, $requestedURL);
201 if ($translated !== null && $translated !== '') {
202 $location = $translated;
203 }
204 }
205
206 return apply_filters('abj404_translate_redirect_url', $location, $requestedURL);
207 }
208
209 /** @return string|null */
210 public function translatePressRedirectUrl(string $location, string $requestedURL) {
211 if (!$this->translatePressIntegrationAvailable()) {
212 return null;
213 }
214
215 if (!$this->isLocalUrl($location)) {
216 return null;
217 }
218
219 $language = $this->getTranslatePressLanguageFromRequest($requestedURL);
220 if ($language === '') {
221 return null;
222 }
223
224 $translated = $this->translatePressTranslateUrl($location, $language);
225 if (!is_string($translated) || $translated === '' || $translated === $location) {
226 return null;
227 }
228
229 if (!$this->isLocalUrl($translated)) {
230 return null;
231 }
232
233 return $translated;
234 }
235
236 /** @return bool */
237 public function translatePressIntegrationAvailable(): bool {
238 return function_exists('trp_get_language_from_url') ||
239 function_exists('trp_get_current_language') ||
240 function_exists('trp_get_url_for_language') ||
241 function_exists('trp_translate_url') ||
242 has_filter('trp_translate_url');
243 }
244
245 /** @return mixed */
246 public function translatePressTranslateUrl(string $url, string $language) {
247 if (function_exists('trp_get_url_for_language')) {
248 return trp_get_url_for_language($language, $url);
249 }
250
251 if (function_exists('trp_translate_url')) {
252 return trp_translate_url($url, $language);
253 }
254
255 return apply_filters('trp_translate_url', $url, $language);
256 }
257
258 public function getTranslatePressLanguageFromRequest(string $requestedURL): string {
259 $fullRequestedUrl = $this->buildFullUrlFromRequest($requestedURL);
260
261 if (function_exists('trp_get_language_from_url')) {
262 $language = trp_get_language_from_url($fullRequestedUrl);
263 if (is_string($language) && $language !== '') {
264 return $language;
265 }
266 }
267
268 if (function_exists('trp_get_current_language')) {
269 $language = trp_get_current_language();
270 if (is_string($language) && $language !== '') {
271 return $language;
272 }
273 }
274
275 return '';
276 }
277
278 /** @return string|null */
279 private function wpmlRedirectUrl(string $location, string $requestedURL) {
280 if (!$this->wpmlIntegrationAvailable()) {
281 return null;
282 }
283
284 if (!$this->isLocalUrl($location)) {
285 return null;
286 }
287
288 $language = $this->getWpmlLanguageFromRequest($requestedURL);
289 if ($language === '') {
290 return null;
291 }
292
293 $translated = $this->wpmlTranslateUrl($location, $language);
294 if (!is_string($translated) || $translated === '' || $translated === $location) {
295 return null;
296 }
297
298 if (!$this->isLocalUrl($translated)) {
299 return null;
300 }
301
302 return $translated;
303 }
304
305 private function wpmlIntegrationAvailable(): bool {
306 return function_exists('wpml_current_language') ||
307 has_filter('wpml_current_language') ||
308 has_filter('wpml_language_from_url') ||
309 has_filter('wpml_permalink');
310 }
311
312 /** @return mixed */
313 private function wpmlTranslateUrl(string $url, string $language) {
314 if (has_filter('wpml_permalink')) {
315 return apply_filters('wpml_permalink', $url, $language);
316 }
317
318 return null;
319 }
320
321 private function getWpmlLanguageFromRequest(string $requestedURL): string {
322 $fullRequestedUrl = $this->buildFullUrlFromRequest($requestedURL);
323
324 if (has_filter('wpml_language_from_url')) {
325 $language = apply_filters('wpml_language_from_url', '', $fullRequestedUrl);
326 if (is_string($language) && $language !== '') {
327 return $language;
328 }
329 }
330
331 if (function_exists('wpml_current_language')) {
332 $language = wpml_current_language();
333 if (is_string($language) && $language !== '') {
334 return $language;
335 }
336 }
337
338 if (has_filter('wpml_current_language')) {
339 $language = apply_filters('wpml_current_language', null);
340 if (is_string($language) && $language !== '') {
341 return $language;
342 }
343 }
344
345 return '';
346 }
347
348 /** @return string|null */
349 private function polylangRedirectUrl(string $location, string $requestedURL) {
350 if (!$this->polylangIntegrationAvailable()) {
351 return null;
352 }
353
354 if (!$this->isLocalUrl($location)) {
355 return null;
356 }
357
358 $language = $this->getPolylangLanguageFromRequest($requestedURL);
359 if ($language === '') {
360 return null;
361 }
362
363 $translated = $this->polylangTranslateUrl($location, $language);
364 if (!is_string($translated) || $translated === '' || $translated === $location) {
365 return null;
366 }
367
368 if (!$this->isLocalUrl($translated)) {
369 return null;
370 }
371
372 return $translated;
373 }
374
375 private function polylangIntegrationAvailable(): bool {
376 return function_exists('pll_current_language') ||
377 function_exists('pll_translate_url');
378 }
379
380 /** @return mixed */
381 private function polylangTranslateUrl(string $url, string $language) {
382 if (function_exists('pll_translate_url')) {
383 return pll_translate_url($url, $language);
384 }
385
386 return null;
387 }
388
389 private function getPolylangLanguageFromRequest(string $requestedURL): string {
390 if (function_exists('pll_current_language')) {
391 $language = pll_current_language();
392 if (is_string($language) && $language !== '') {
393 return $language;
394 }
395 }
396
397 return '';
398 }
399
400 public function buildFullUrlFromRequest(string $requestedURL): string {
401 $path = $requestedURL;
402 if ($path === '') {
403 $userRequest = ABJ_404_Solution_UserRequest::getInstance();
404 if ($userRequest !== null) {
405 $path = $userRequest->getPathWithSortedQueryString();
406 }
407 }
408
409 if ($path === '') {
410 return home_url('/');
411 }
412
413 if (preg_match('#^https?://#i', $path)) {
414 return $path;
415 }
416
417 return home_url($path);
418 }
419
420 public function isLocalUrl(string $url): bool {
421 if (!is_string($url) || $url === '') {
422 return false;
423 }
424
425 $parsedUrl = function_exists('wp_parse_url') ? wp_parse_url($url) : parse_url($url);
426 if (!is_array($parsedUrl) || !isset($parsedUrl['host'])) {
427 return true;
428 }
429
430 $siteUrl = home_url();
431 $parsedSite = function_exists('wp_parse_url') ? wp_parse_url($siteUrl) : parse_url($siteUrl);
432 $siteHost = is_array($parsedSite) && isset($parsedSite['host']) ? strtolower($parsedSite['host']) : '';
433
434 return $siteHost !== '' && strtolower($parsedUrl['host']) === $siteHost;
435 }
436
437 }
438