PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 All 83 releases
yatra / app / Core / Routing / PermalinkCanonical.php

PermalinkCanonical.php in Yatra – Travel Booking & Tour Operator Software trunk, at app/Core/Routing/PermalinkCanonical.php

493 lines 17.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Yatra\Core\Routing;
6
7 use Yatra\Services\SettingsService;
8
9 /**
10 * Enforces permalink mode: plain sites must not resolve path-based (pretty) plugin routes;
11 * pretty sites redirect plain query-string routing to canonical pretty URLs.
12 */
13 final class PermalinkCanonical
14 {
15 public static function enforce(): void
16 {
17 if (!apply_filters('yatra_enforce_permalink_canonical', true)) {
18 return;
19 }
20
21 if (is_admin() || wp_doing_ajax() || wp_doing_cron() || (defined('REST_REQUEST') && REST_REQUEST)) {
22 return;
23 }
24
25 $method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
26 if ($method !== 'GET' && $method !== 'HEAD') {
27 return;
28 }
29
30 $plain_wp = self::isPlainWordPressPermalink();
31
32 $path = UrlParser::getCleanRequestPath();
33
34 if ($plain_wp) {
35 if ($path !== '' && PrettyRouteMatcher::match($path) !== null) {
36 global $wp_query;
37 $wp_query->set_404();
38 status_header(404);
39 nocache_headers();
40 }
41
42 return;
43 }
44
45 if (!self::requestHasPlainYatraRoutingQuery()) {
46 return;
47 }
48
49 $current = self::currentRequestUrl();
50 $target = self::buildPrettyCanonicalUrl();
51 $target = apply_filters('yatra_permalink_canonical_redirect_target', $target, $current);
52
53 if ($target === null || $target === '') {
54 return;
55 }
56
57 $target = self::mergePreservedMarketingArgs($current, $target);
58
59 $stripped = self::stripPlainYatraArgsFromUrl($current);
60 if (self::urlsAreCanonicallyEqual($stripped, $target)) {
61 return;
62 }
63
64 wp_safe_redirect($target, 301);
65 exit;
66 }
67
68 public static function isPlainWordPressPermalink(): bool
69 {
70 return (string) get_option('permalink_structure', '') === '';
71 }
72
73 /**
74 * True when the request path is the trip archive or its paged variant (e.g. /trip/, /trip/page/2/).
75 * Query params {@see destination_base} / {@see activity_base} are listing filters here, not plain taxonomy routing.
76 */
77 private static function isTripArchivePath(): bool
78 {
79 $tripSeg = preg_replace('/[^a-z0-9_-]/i', '', SettingsService::getTripBase()) ?: 'trip';
80 if ($tripSeg === '') {
81 return false;
82 }
83 $cleanPath = trim(UrlParser::getCleanRequestPath(), '/');
84 if ($cleanPath === $tripSeg) {
85 return true;
86 }
87
88 return (bool) preg_match('/^' . preg_quote($tripSeg, '/') . '\/page\/\d+$/', $cleanPath);
89 }
90
91 /**
92 * True when the request carries plain-style Yatra routing parameters (query string).
93 */
94 public static function requestHasPlainYatraRoutingQuery(): bool
95 {
96 foreach (['yatra_page', 'yatra_trip', 'yatra_trip_slug', 'yatra_destination_slug', 'yatra_activity_slug', 'yatra_category_slug',
97 'yatra_booking_confirmation', 'yatra_remaining_checkout', 'yatra_verify_email'] as $key) {
98 if (!isset($_GET[$key])) {
99 continue;
100 }
101 $v = wp_unslash($_GET[$key]);
102 if (is_string($v) && trim($v) !== '') {
103 return true;
104 }
105 }
106
107 $tripSlugKey = preg_replace('/[^a-z0-9_-]/i', '', SettingsService::getTripBase()) ?: 'trip';
108 if (isset($_GET[$tripSlugKey]) && is_string($_GET[$tripSlugKey]) && trim(wp_unslash($_GET[$tripSlugKey])) !== '') {
109 return true;
110 }
111
112 foreach (
113 [
114 SettingsService::getDestinationBase(),
115 SettingsService::getActivityBase(),
116 SettingsService::getTripCategoryBase(),
117 ] as $base
118 ) {
119 $bk = preg_replace('/[^a-z0-9_-]/i', '', $base) ?: '';
120 if ($bk === '' || $bk === $tripSlugKey) {
121 continue;
122 }
123 if (isset($_GET[$bk]) && is_string($_GET[$bk]) && trim(wp_unslash($_GET[$bk])) !== '') {
124 if (self::isTripArchivePath()) {
125 continue;
126 }
127 return true;
128 }
129 }
130
131 if (!empty($_GET['reference']) && is_string($_GET['reference']) && trim(wp_unslash($_GET['reference'])) !== '') {
132 return true;
133 }
134
135 $booking_base = SettingsService::getBookingBase();
136 if (!empty($_GET['trip']) && is_string($_GET['trip']) && trim(wp_unslash($_GET['trip'])) !== '') {
137 $yp = isset($_GET['yatra_page']) ? (string) wp_unslash($_GET['yatra_page']) : '';
138 $yp = preg_replace('/[^a-z0-9_-]/i', '', $yp) ?? '';
139 if ($yp === $booking_base) {
140 return true;
141 }
142 }
143
144 if (isset($_GET['paged']) && (int) wp_unslash($_GET['paged']) > 1) {
145 return isset($_GET['yatra_page']) && is_string($_GET['yatra_page']) && trim(wp_unslash($_GET['yatra_page'])) !== '';
146 }
147
148 if (isset($_GET['page']) && (int) wp_unslash($_GET['page']) > 1) {
149 return isset($_GET['yatra_page']) && is_string($_GET['yatra_page']) && trim(wp_unslash($_GET['yatra_page'])) !== '';
150 }
151
152 return false;
153 }
154
155 /**
156 * Build the canonical pretty URL for the current plain query request, or null if unknown.
157 */
158 public static function buildPrettyCanonicalUrl(): ?string
159 {
160 $ref = isset($_GET['reference']) ? sanitize_text_field(wp_unslash((string) $_GET['reference'])) : '';
161 $conf = isset($_GET['yatra_booking_confirmation']) ? sanitize_text_field(wp_unslash((string) $_GET['yatra_booking_confirmation'])) : '';
162 $bookingRef = $conf !== '' ? $conf : $ref;
163 if ($bookingRef !== '') {
164 if (function_exists('yatra_get_booking_confirmation_url')) {
165 return yatra_get_booking_confirmation_url($bookingRef);
166 }
167
168 return null;
169 }
170
171 $rem = isset($_GET['yatra_remaining_checkout']) ? (string) wp_unslash($_GET['yatra_remaining_checkout']) : '';
172 if ($rem !== '') {
173 $t = preg_replace('/[^a-zA-Z0-9_-]/', '', $rem) ?? '';
174 if ($t !== '') {
175 $pb = SettingsService::getPermalinkBases();
176
177 return trailingslashit(home_url('/' . $pb['remaining_checkout_prefix'] . '/' . $t . '/'));
178 }
179 }
180
181 $verify = isset($_GET['yatra_verify_email']) ? (string) wp_unslash($_GET['yatra_verify_email']) : '';
182 if ($verify !== '') {
183 $t = preg_replace('/[^a-zA-Z0-9_-]/', '', $verify) ?? '';
184 if ($t !== '') {
185 $pb = SettingsService::getPermalinkBases();
186
187 return trailingslashit(home_url('/' . $pb['email_verification_prefix'] . '/' . $t . '/'));
188 }
189 }
190
191 $trip_base = SettingsService::getTripBase();
192 $booking_base = SettingsService::getBookingBase();
193 $account_base = SettingsService::getAccountBase();
194 $dest_base = SettingsService::getDestinationBase();
195 $act_base = SettingsService::getActivityBase();
196 $cat_base = SettingsService::getTripCategoryBase();
197
198 $rawPage = isset($_GET['yatra_page']) ? trim(wp_unslash((string) $_GET['yatra_page'])) : '';
199 $page = $rawPage !== '' ? (preg_replace('/[^a-z0-9_-]/i', '', $rawPage) ?? '') : '';
200
201 $tripSlugKey = preg_replace('/[^a-z0-9_-]/i', '', $trip_base) ?: 'trip';
202 $tripSlug = '';
203 if (isset($_GET[$tripSlugKey])) {
204 $tripSlug = \Yatra\Helpers\SlugHelper::generate(wp_unslash((string) $_GET[$tripSlugKey]));
205 } elseif (isset($_GET['yatra_trip'])) {
206 $tripSlug = \Yatra\Helpers\SlugHelper::generate(wp_unslash((string) $_GET['yatra_trip']));
207 } elseif (isset($_GET['yatra_trip_slug'])) {
208 $tripSlug = \Yatra\Helpers\SlugHelper::generate(wp_unslash((string) $_GET['yatra_trip_slug']));
209 }
210
211 if ($page === '' && $tripSlug !== '') {
212 return trailingslashit(home_url('/' . $trip_base . '/' . $tripSlug . '/'));
213 }
214
215 if ($page === '') {
216 if (self::isTripArchivePath()) {
217 return null;
218 }
219 foreach (
220 [
221 [$dest_base, 'yatra_destination_slug'],
222 [$act_base, 'yatra_activity_slug'],
223 [$cat_base, 'yatra_category_slug'],
224 ] as [$tbase, $legacySlugKey]
225 ) {
226 $bk = preg_replace('/[^a-z0-9_-]/i', '', $tbase) ?: '';
227 if ($bk === '' || $bk === $tripSlugKey) {
228 continue;
229 }
230 $s = '';
231 if (isset($_GET[$bk])) {
232 $s = \Yatra\Helpers\SlugHelper::generate(wp_unslash((string) $_GET[$bk]));
233 }
234 if ($s === '' && isset($_GET[$legacySlugKey])) {
235 $s = \Yatra\Helpers\SlugHelper::generate(wp_unslash((string) $_GET[$legacySlugKey]));
236 }
237 if ($s !== '') {
238 return trailingslashit(home_url('/' . $tbase . '/' . $s . '/'));
239 }
240 }
241
242 return null;
243 }
244
245 $paged = self::plainPagedFromRequest();
246
247 if ($page === $account_base) {
248 $tab = isset($_GET['tab']) ? sanitize_key((string) wp_unslash($_GET['tab'])) : '';
249 $accountPage = self::accountQueryTabToPage($tab);
250
251 return self::accountPrettyUrl($account_base, $accountPage);
252 }
253
254 if ($page === $trip_base) {
255 if ($tripSlug !== '') {
256 return trailingslashit(home_url('/' . $trip_base . '/' . $tripSlug . '/'));
257 }
258 if ($paged > 1) {
259 return trailingslashit(home_url('/' . $trip_base . '/page/' . $paged . '/'));
260 }
261
262 return function_exists('yatra_get_trip_listing_url') ? yatra_get_trip_listing_url() : trailingslashit(home_url('/' . $trip_base . '/'));
263 }
264
265 if ($page === $booking_base && !SettingsService::useCustomBookingPage()) {
266 $tripParam = isset($_GET['trip']) ? \Yatra\Helpers\SlugHelper::generate(wp_unslash((string) $_GET['trip'])) : '';
267 if ($tripParam !== '') {
268 return function_exists('yatra_get_booking_url') ? yatra_get_booking_url($tripParam) : trailingslashit(home_url('/' . $booking_base . '/' . $tripParam . '/'));
269 }
270
271 return function_exists('yatra_get_checkout_url') ? yatra_get_checkout_url() : trailingslashit(home_url('/' . $booking_base . '/'));
272 }
273
274 if ($page === $booking_base && SettingsService::useCustomBookingPage()) {
275 $page_id = SettingsService::getBookingPageId();
276 if ($page_id > 0) {
277 $url = get_permalink((int) $page_id);
278 if ($url) {
279 $tripParam = isset($_GET['trip']) ? \Yatra\Helpers\SlugHelper::generate(wp_unslash((string) $_GET['trip'])) : '';
280 if ($tripParam !== '') {
281 return add_query_arg('trip', $tripParam, $url);
282 }
283
284 return $url;
285 }
286 }
287
288 return null;
289 }
290
291 foreach (
292 [
293 [$dest_base, 'yatra_destination_slug'],
294 [$act_base, 'yatra_activity_slug'],
295 [$cat_base, 'yatra_category_slug'],
296 ] as [$base, $legacySlugKey]
297 ) {
298 if ($page !== $base) {
299 continue;
300 }
301 $bk = preg_replace('/[^a-z0-9_-]/i', '', $base) ?: '';
302 $slug = '';
303 if ($bk !== '' && isset($_GET[$bk])) {
304 $slug = \Yatra\Helpers\SlugHelper::generate(wp_unslash((string) $_GET[$bk]));
305 }
306 if ($slug === '' && isset($_GET[$legacySlugKey])) {
307 $slug = \Yatra\Helpers\SlugHelper::generate(wp_unslash((string) $_GET[$legacySlugKey]));
308 }
309 $url = trailingslashit(home_url('/' . $base . '/'));
310 if ($slug !== '') {
311 $url = trailingslashit(home_url('/' . $base . '/' . $slug . '/'));
312 if ($paged > 1) {
313 $url = trailingslashit(home_url('/' . $base . '/' . $slug . '/page/' . $paged . '/'));
314 }
315 } elseif ($paged > 1) {
316 $url = add_query_arg('paged', (string) $paged, $url);
317 }
318
319 return $url;
320 }
321
322 return null;
323 }
324
325 private static function plainPagedFromRequest(): int
326 {
327 $p = get_query_var('paged');
328 if ($p !== '' && $p !== null) {
329 return max(1, (int) $p);
330 }
331 if (isset($_GET['paged'])) {
332 return max(1, (int) wp_unslash($_GET['paged']));
333 }
334 if (isset($_GET['page'])) {
335 return max(1, (int) wp_unslash($_GET['page']));
336 }
337
338 return 1;
339 }
340
341 private static function accountQueryTabToPage(string $tab): string
342 {
343 $allowed = ['dashboard', 'bookings', 'payments', 'documents', 'profile', 'saved-trips'];
344 if ($tab === '' || !in_array($tab, $allowed, true)) {
345 return 'dashboard';
346 }
347
348 return $tab;
349 }
350
351 private static function accountPrettyUrl(string $account_base, string $page): string
352 {
353 $segmentMap = [
354 'dashboard' => '',
355 'profile' => 'profile',
356 'bookings' => 'bookings',
357 'payments' => 'payments',
358 'documents' => 'documents',
359 'saved-trips' => 'saved-trips',
360 ];
361 $seg = $segmentMap[$page] ?? '';
362
363 if ($seg === '') {
364 return trailingslashit(home_url('/' . $account_base . '/'));
365 }
366
367 return trailingslashit(home_url('/' . $account_base . '/' . $seg . '/'));
368 }
369
370 private static function currentRequestUrl(): string
371 {
372 $scheme = is_ssl() ? 'https://' : 'http://';
373 $host = isset($_SERVER['HTTP_HOST']) ? (string) $_SERVER['HTTP_HOST'] : '';
374 $uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
375
376 return esc_url_raw($scheme . $host . $uri);
377 }
378
379 private static function urlsAreCanonicallyEqual(string $current, string $target): bool
380 {
381 $a = wp_parse_url($current);
382 $b = wp_parse_url($target);
383 if ($a === false || $b === false) {
384 return false;
385 }
386
387 $pathA = isset($a['path']) ? untrailingslashit($a['path']) : '';
388 $pathB = isset($b['path']) ? untrailingslashit($b['path']) : '';
389 if ($pathA !== $pathB) {
390 return false;
391 }
392
393 parse_str($a['query'] ?? '', $qa);
394 parse_str($b['query'] ?? '', $qb);
395
396 foreach (self::plainQueryKeysToStrip() as $k) {
397 unset($qa[$k], $qb[$k]);
398 }
399
400 return $qa == $qb;
401 }
402
403 /**
404 * @return list<string>
405 */
406 private static function plainQueryKeysToStrip(): array
407 {
408 $tripKey = preg_replace('/[^a-z0-9_-]/i', '', SettingsService::getTripBase()) ?: 'trip';
409 $destKey = preg_replace('/[^a-z0-9_-]/i', '', SettingsService::getDestinationBase()) ?: 'destination';
410 $actKey = preg_replace('/[^a-z0-9_-]/i', '', SettingsService::getActivityBase()) ?: 'activity';
411 $catKey = preg_replace('/[^a-z0-9_-]/i', '', SettingsService::getTripCategoryBase()) ?: 'trip-category';
412
413 return array_values(array_unique([
414 'yatra_page',
415 'yatra_trip',
416 'yatra_trip_slug',
417 'yatra_destination_slug',
418 'yatra_activity_slug',
419 'yatra_category_slug',
420 'yatra_booking_confirmation',
421 'yatra_remaining_checkout',
422 'yatra_verify_email',
423 'reference',
424 'trip',
425 'paged',
426 'page',
427 $tripKey,
428 $destKey,
429 $actKey,
430 $catKey,
431 ]));
432 }
433
434 private static function stripPlainYatraArgsFromUrl(string $url): string
435 {
436 $parts = wp_parse_url($url);
437 if ($parts === false) {
438 return $url;
439 }
440
441 parse_str($parts['query'] ?? '', $q);
442 foreach (self::plainQueryKeysToStrip() as $k) {
443 unset($q[$k]);
444 }
445
446 $path = $parts['path'] ?? '/';
447 $scheme = isset($parts['scheme']) ? $parts['scheme'] . '://' : '';
448 $host = $parts['host'] ?? '';
449 $port = isset($parts['port']) ? ':' . $parts['port'] : '';
450 $rebuilt = $scheme . $host . $port . $path;
451 if ($q !== []) {
452 $rebuilt = add_query_arg($q, $rebuilt);
453 }
454 if (!empty($parts['fragment'])) {
455 $rebuilt .= '#' . $parts['fragment'];
456 }
457
458 return $rebuilt;
459 }
460
461 private static function mergePreservedMarketingArgs(string $current, string $target): string
462 {
463 $keys = apply_filters(
464 'yatra_permalink_canonical_preserve_query_keys',
465 ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid', 'fbclid']
466 );
467 if (!is_array($keys) || $keys === []) {
468 return $target;
469 }
470
471 $parts = wp_parse_url($current);
472 if ($parts === false) {
473 return $target;
474 }
475 parse_str($parts['query'] ?? '', $q);
476 $extra = [];
477 foreach ($keys as $k) {
478 if (!is_string($k) || $k === '') {
479 continue;
480 }
481 if (isset($q[$k]) && $q[$k] !== '' && $q[$k] !== null) {
482 $extra[$k] = $q[$k];
483 }
484 }
485
486 if ($extra === []) {
487 return $target;
488 }
489
490 return add_query_arg($extra, $target);
491 }
492 }
493