PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
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 3.0.3, at app/Core/Routing/PermalinkCanonical.php

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