PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.6
Yatra – Travel Booking & Tour Operator Software v3.0.2.6
3.0.16 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 All 84 releases
yatra / app / Core / Routing / PermalinkCanonical.php

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

498 lines 17.4 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::getString('destination_base', 'destination'),
115 SettingsService::getString('activity_base', 'activity'),
116 SettingsService::getString('trip_category_base', 'trip-category'),
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['yatra_login_page'])) {
132 return true;
133 }
134
135 if (!empty($_GET['reference']) && is_string($_GET['reference']) && trim(wp_unslash($_GET['reference'])) !== '') {
136 return true;
137 }
138
139 $booking_base = SettingsService::getBookingBase();
140 if (!empty($_GET['trip']) && is_string($_GET['trip']) && trim(wp_unslash($_GET['trip'])) !== '') {
141 $yp = isset($_GET['yatra_page']) ? (string) wp_unslash($_GET['yatra_page']) : '';
142 $yp = preg_replace('/[^a-z0-9_-]/i', '', $yp) ?? '';
143 if ($yp === $booking_base) {
144 return true;
145 }
146 }
147
148 if (isset($_GET['paged']) && (int) wp_unslash($_GET['paged']) > 1) {
149 return isset($_GET['yatra_page']) && is_string($_GET['yatra_page']) && trim(wp_unslash($_GET['yatra_page'])) !== '';
150 }
151
152 if (isset($_GET['page']) && (int) wp_unslash($_GET['page']) > 1) {
153 return isset($_GET['yatra_page']) && is_string($_GET['yatra_page']) && trim(wp_unslash($_GET['yatra_page'])) !== '';
154 }
155
156 return false;
157 }
158
159 /**
160 * Build the canonical pretty URL for the current plain query request, or null if unknown.
161 */
162 public static function buildPrettyCanonicalUrl(): ?string
163 {
164 $ref = isset($_GET['reference']) ? sanitize_text_field(wp_unslash((string) $_GET['reference'])) : '';
165 $conf = isset($_GET['yatra_booking_confirmation']) ? sanitize_text_field(wp_unslash((string) $_GET['yatra_booking_confirmation'])) : '';
166 $bookingRef = $conf !== '' ? $conf : $ref;
167 if ($bookingRef !== '') {
168 if (function_exists('yatra_get_booking_confirmation_url')) {
169 return yatra_get_booking_confirmation_url($bookingRef);
170 }
171
172 return null;
173 }
174
175 $rem = isset($_GET['yatra_remaining_checkout']) ? (string) wp_unslash($_GET['yatra_remaining_checkout']) : '';
176 if ($rem !== '') {
177 $t = preg_replace('/[^a-zA-Z0-9_-]/', '', $rem) ?? '';
178 if ($t !== '') {
179 return trailingslashit(home_url('/remaining-checkout/' . $t . '/'));
180 }
181 }
182
183 $verify = isset($_GET['yatra_verify_email']) ? (string) wp_unslash($_GET['yatra_verify_email']) : '';
184 if ($verify !== '') {
185 $t = preg_replace('/[^a-zA-Z0-9_-]/', '', $verify) ?? '';
186 if ($t !== '') {
187 return trailingslashit(home_url('/yatra-verify-email/' . $t . '/'));
188 }
189 }
190
191 if (!empty($_GET['yatra_login_page'])) {
192 return trailingslashit(home_url('/login/'));
193 }
194
195 $trip_base = SettingsService::getTripBase();
196 $booking_base = SettingsService::getBookingBase();
197 $account_base = SettingsService::getAccountBase();
198 $dest_base = SettingsService::getString('destination_base', 'destination');
199 $act_base = SettingsService::getString('activity_base', 'activity');
200 $cat_base = SettingsService::getString('trip_category_base', 'trip-category');
201
202 $rawPage = isset($_GET['yatra_page']) ? trim(wp_unslash((string) $_GET['yatra_page'])) : '';
203 $page = $rawPage !== '' ? (preg_replace('/[^a-z0-9_-]/i', '', $rawPage) ?? '') : '';
204
205 $tripSlugKey = preg_replace('/[^a-z0-9_-]/i', '', $trip_base) ?: 'trip';
206 $tripSlug = '';
207 if (isset($_GET[$tripSlugKey])) {
208 $tripSlug = sanitize_title(wp_unslash((string) $_GET[$tripSlugKey]));
209 } elseif (isset($_GET['yatra_trip'])) {
210 $tripSlug = sanitize_title(wp_unslash((string) $_GET['yatra_trip']));
211 } elseif (isset($_GET['yatra_trip_slug'])) {
212 $tripSlug = sanitize_title(wp_unslash((string) $_GET['yatra_trip_slug']));
213 }
214
215 if ($page === '' && $tripSlug !== '') {
216 return trailingslashit(home_url('/' . $trip_base . '/' . $tripSlug . '/'));
217 }
218
219 if ($page === '') {
220 if (self::isTripArchivePath()) {
221 return null;
222 }
223 foreach (
224 [
225 [$dest_base, 'yatra_destination_slug'],
226 [$act_base, 'yatra_activity_slug'],
227 [$cat_base, 'yatra_category_slug'],
228 ] as [$tbase, $legacySlugKey]
229 ) {
230 $bk = preg_replace('/[^a-z0-9_-]/i', '', $tbase) ?: '';
231 if ($bk === '' || $bk === $tripSlugKey) {
232 continue;
233 }
234 $s = '';
235 if (isset($_GET[$bk])) {
236 $s = sanitize_title(wp_unslash((string) $_GET[$bk]));
237 }
238 if ($s === '' && isset($_GET[$legacySlugKey])) {
239 $s = sanitize_title(wp_unslash((string) $_GET[$legacySlugKey]));
240 }
241 if ($s !== '') {
242 return trailingslashit(home_url('/' . $tbase . '/' . $s . '/'));
243 }
244 }
245
246 return null;
247 }
248
249 $paged = self::plainPagedFromRequest();
250
251 if ($page === $account_base) {
252 $tab = isset($_GET['tab']) ? sanitize_key((string) wp_unslash($_GET['tab'])) : '';
253 $accountPage = self::accountQueryTabToPage($tab);
254
255 return self::accountPrettyUrl($account_base, $accountPage);
256 }
257
258 if ($page === $trip_base) {
259 if ($tripSlug !== '') {
260 return trailingslashit(home_url('/' . $trip_base . '/' . $tripSlug . '/'));
261 }
262 if ($paged > 1) {
263 return trailingslashit(home_url('/' . $trip_base . '/page/' . $paged . '/'));
264 }
265
266 return function_exists('yatra_get_trip_listing_url') ? yatra_get_trip_listing_url() : trailingslashit(home_url('/' . $trip_base . '/'));
267 }
268
269 if ($page === $booking_base && !SettingsService::useCustomBookingPage()) {
270 $tripParam = isset($_GET['trip']) ? sanitize_title(wp_unslash((string) $_GET['trip'])) : '';
271 if ($tripParam !== '') {
272 return function_exists('yatra_get_booking_url') ? yatra_get_booking_url($tripParam) : trailingslashit(home_url('/' . $booking_base . '/' . $tripParam . '/'));
273 }
274
275 return function_exists('yatra_get_checkout_url') ? yatra_get_checkout_url() : trailingslashit(home_url('/' . $booking_base . '/'));
276 }
277
278 if ($page === $booking_base && SettingsService::useCustomBookingPage()) {
279 $page_id = SettingsService::getBookingPageId();
280 if ($page_id > 0) {
281 $url = get_permalink((int) $page_id);
282 if ($url) {
283 $tripParam = isset($_GET['trip']) ? sanitize_title(wp_unslash((string) $_GET['trip'])) : '';
284 if ($tripParam !== '') {
285 return add_query_arg('trip', $tripParam, $url);
286 }
287
288 return $url;
289 }
290 }
291
292 return null;
293 }
294
295 foreach (
296 [
297 [$dest_base, 'yatra_destination_slug'],
298 [$act_base, 'yatra_activity_slug'],
299 [$cat_base, 'yatra_category_slug'],
300 ] as [$base, $legacySlugKey]
301 ) {
302 if ($page !== $base) {
303 continue;
304 }
305 $bk = preg_replace('/[^a-z0-9_-]/i', '', $base) ?: '';
306 $slug = '';
307 if ($bk !== '' && isset($_GET[$bk])) {
308 $slug = sanitize_title(wp_unslash((string) $_GET[$bk]));
309 }
310 if ($slug === '' && isset($_GET[$legacySlugKey])) {
311 $slug = sanitize_title(wp_unslash((string) $_GET[$legacySlugKey]));
312 }
313 $url = trailingslashit(home_url('/' . $base . '/'));
314 if ($slug !== '') {
315 $url = trailingslashit(home_url('/' . $base . '/' . $slug . '/'));
316 if ($paged > 1) {
317 $url = trailingslashit(home_url('/' . $base . '/' . $slug . '/page/' . $paged . '/'));
318 }
319 } elseif ($paged > 1) {
320 $url = add_query_arg('paged', (string) $paged, $url);
321 }
322
323 return $url;
324 }
325
326 return null;
327 }
328
329 private static function plainPagedFromRequest(): int
330 {
331 $p = get_query_var('paged');
332 if ($p !== '' && $p !== null) {
333 return max(1, (int) $p);
334 }
335 if (isset($_GET['paged'])) {
336 return max(1, (int) wp_unslash($_GET['paged']));
337 }
338 if (isset($_GET['page'])) {
339 return max(1, (int) wp_unslash($_GET['page']));
340 }
341
342 return 1;
343 }
344
345 private static function accountQueryTabToPage(string $tab): string
346 {
347 $allowed = ['dashboard', 'bookings', 'payments', 'documents', 'profile', 'saved-trips'];
348 if ($tab === '' || !in_array($tab, $allowed, true)) {
349 return 'dashboard';
350 }
351
352 return $tab;
353 }
354
355 private static function accountPrettyUrl(string $account_base, string $page): string
356 {
357 $segmentMap = [
358 'dashboard' => '',
359 'profile' => 'profile',
360 'bookings' => 'bookings',
361 'payments' => 'payments',
362 'documents' => 'documents',
363 'saved-trips' => 'saved-trips',
364 ];
365 $seg = $segmentMap[$page] ?? '';
366
367 if ($seg === '') {
368 return trailingslashit(home_url('/' . $account_base . '/'));
369 }
370
371 return trailingslashit(home_url('/' . $account_base . '/' . $seg . '/'));
372 }
373
374 private static function currentRequestUrl(): string
375 {
376 $scheme = is_ssl() ? 'https://' : 'http://';
377 $host = isset($_SERVER['HTTP_HOST']) ? (string) $_SERVER['HTTP_HOST'] : '';
378 $uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
379
380 return esc_url_raw($scheme . $host . $uri);
381 }
382
383 private static function urlsAreCanonicallyEqual(string $current, string $target): bool
384 {
385 $a = wp_parse_url($current);
386 $b = wp_parse_url($target);
387 if ($a === false || $b === false) {
388 return false;
389 }
390
391 $pathA = isset($a['path']) ? untrailingslashit($a['path']) : '';
392 $pathB = isset($b['path']) ? untrailingslashit($b['path']) : '';
393 if ($pathA !== $pathB) {
394 return false;
395 }
396
397 parse_str($a['query'] ?? '', $qa);
398 parse_str($b['query'] ?? '', $qb);
399
400 foreach (self::plainQueryKeysToStrip() as $k) {
401 unset($qa[$k], $qb[$k]);
402 }
403
404 return $qa == $qb;
405 }
406
407 /**
408 * @return list<string>
409 */
410 private static function plainQueryKeysToStrip(): array
411 {
412 $tripKey = preg_replace('/[^a-z0-9_-]/i', '', SettingsService::getTripBase()) ?: 'trip';
413 $destKey = preg_replace('/[^a-z0-9_-]/i', '', SettingsService::getString('destination_base', 'destination')) ?: 'destination';
414 $actKey = preg_replace('/[^a-z0-9_-]/i', '', SettingsService::getString('activity_base', 'activity')) ?: 'activity';
415 $catKey = preg_replace('/[^a-z0-9_-]/i', '', SettingsService::getString('trip_category_base', 'trip-category')) ?: 'trip-category';
416
417 return array_values(array_unique([
418 'yatra_page',
419 'yatra_trip',
420 'yatra_trip_slug',
421 'yatra_destination_slug',
422 'yatra_activity_slug',
423 'yatra_category_slug',
424 'yatra_booking_confirmation',
425 'yatra_remaining_checkout',
426 'yatra_verify_email',
427 'yatra_login_page',
428 'reference',
429 'trip',
430 'paged',
431 'page',
432 $tripKey,
433 $destKey,
434 $actKey,
435 $catKey,
436 ]));
437 }
438
439 private static function stripPlainYatraArgsFromUrl(string $url): string
440 {
441 $parts = wp_parse_url($url);
442 if ($parts === false) {
443 return $url;
444 }
445
446 parse_str($parts['query'] ?? '', $q);
447 foreach (self::plainQueryKeysToStrip() as $k) {
448 unset($q[$k]);
449 }
450
451 $path = $parts['path'] ?? '/';
452 $scheme = isset($parts['scheme']) ? $parts['scheme'] . '://' : '';
453 $host = $parts['host'] ?? '';
454 $port = isset($parts['port']) ? ':' . $parts['port'] : '';
455 $rebuilt = $scheme . $host . $port . $path;
456 if ($q !== []) {
457 $rebuilt = add_query_arg($q, $rebuilt);
458 }
459 if (!empty($parts['fragment'])) {
460 $rebuilt .= '#' . $parts['fragment'];
461 }
462
463 return $rebuilt;
464 }
465
466 private static function mergePreservedMarketingArgs(string $current, string $target): string
467 {
468 $keys = apply_filters(
469 'yatra_permalink_canonical_preserve_query_keys',
470 ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid', 'fbclid']
471 );
472 if (!is_array($keys) || $keys === []) {
473 return $target;
474 }
475
476 $parts = wp_parse_url($current);
477 if ($parts === false) {
478 return $target;
479 }
480 parse_str($parts['query'] ?? '', $q);
481 $extra = [];
482 foreach ($keys as $k) {
483 if (!is_string($k) || $k === '') {
484 continue;
485 }
486 if (isset($q[$k]) && $q[$k] !== '' && $q[$k] !== null) {
487 $extra[$k] = $q[$k];
488 }
489 }
490
491 if ($extra === []) {
492 return $target;
493 }
494
495 return add_query_arg($extra, $target);
496 }
497 }
498