PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
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 2.0.11 All 82 releases
yatra / app / Core / Routing / PrettyRouteMatcher.php

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

254 lines 8.8 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 * Matches request paths served as path-based (pretty) URLs.
11 * Used for: plain-permalink mode 404 when path looks like a plugin pretty URL, and by {@see Router}.
12 */
13 final class PrettyRouteMatcher
14 {
15 /**
16 * @return array<string, mixed>|null Same shape as {@see Router} route_data
17 */
18 public static function match(string $path): ?array
19 {
20 $path = trim($path, '/');
21
22 /**
23 * Override pretty-path routing entirely (runs before built-in rules).
24 * Return a route array shaped like handler data: keys include `type` (trip|taxonomy|listing|…),
25 * and type-specific keys such as `slug`, `taxonomy_type`, `listing_type`, `base`, `paged`, etc.
26 *
27 * @param array<string,mixed>|null $route_data Resolved route or null to use core matching.
28 * @param string $path Trimmed relative path ({@see UrlParser::getCleanRequestPath()} after `yatra_frontend_request_path`).
29 */
30 $override = apply_filters('yatra_pretty_route_match', null, $path);
31 if (is_array($override) && isset($override['type']) && is_string($override['type']) && $override['type'] !== '') {
32 return $override;
33 }
34
35 $pb = SettingsService::getPermalinkBases();
36
37 // 1. Email verification
38 $evQuoted = preg_quote($pb['email_verification_prefix'], '/');
39 if (preg_match('/^' . $evQuoted . '\/([a-zA-Z0-9_-]+)$/', $path, $matches)) {
40 return [
41 'type' => 'email_verification',
42 'token' => $matches[1],
43 ];
44 }
45
46 // 2. Account page
47 $account_base = SettingsService::getAccountBase();
48 $account_route = self::matchAccountRoute($path, $account_base);
49 if ($account_route !== null) {
50 return $account_route;
51 }
52
53 // 3. Trip archive pagination: {trip_base}/page/{n}
54 $trip_base = SettingsService::getTripBase();
55 if (preg_match('/^' . preg_quote($trip_base, '/') . '\/page\/(\d+)\/?$/', $path, $matches)) {
56 return [
57 'type' => 'listing',
58 'listing_type' => 'trip',
59 'base' => $trip_base,
60 'paged' => max(1, (int) $matches[1]),
61 ];
62 }
63
64 // Captured slug segments come in as URL-encoded UTF-8 when the request
65 // path contains non-ASCII characters (e.g. Cyrillic "моя-семья"
66 // arrives here as `%D0%BC%D0%BE%D1%8F-...`). The DB stores the raw
67 // decoded slug, so we must decode + normalise once here before
68 // returning the route data — otherwise downstream handlers query
69 // `findBySlug('%D0%BC...')` and 404 every non-Latin URL. Mirrors WP
70 // core's behaviour in `get_page_by_path`.
71 $decodeSlug = static function (string $raw): string {
72 return \Yatra\Helpers\SlugHelper::generate($raw);
73 };
74
75 // 5. Single trip
76 if (preg_match('/^' . preg_quote($trip_base, '/') . '\/([^\/]+)\/?$/', $path, $matches)) {
77 if ($matches[1] !== 'page') {
78 return [
79 'type' => 'trip',
80 'slug' => $decodeSlug($matches[1]),
81 'base' => $trip_base,
82 ];
83 }
84 }
85
86 // 6. Taxonomy (pagination before single slug)
87 $bases = [
88 'destination' => SettingsService::getDestinationBase(),
89 'activity' => SettingsService::getActivityBase(),
90 'category' => SettingsService::getTripCategoryBase(),
91 ];
92
93 foreach ($bases as $type => $base) {
94 if (preg_match('/^' . preg_quote($base, '/') . '\/([^\/]+)\/page\/(\d+)\/?$/', $path, $matches)) {
95 return [
96 'type' => 'taxonomy',
97 'taxonomy_type' => $type,
98 'slug' => $decodeSlug($matches[1]),
99 'base' => $base,
100 'paged' => max(1, (int) $matches[2]),
101 ];
102 }
103 }
104
105 foreach ($bases as $type => $base) {
106 if (preg_match('/^' . preg_quote($base, '/') . '\/([^\/]+)\/?$/', $path, $matches)) {
107 return [
108 'type' => 'taxonomy',
109 'taxonomy_type' => $type,
110 'slug' => $decodeSlug($matches[1]),
111 'base' => $base,
112 ];
113 }
114 }
115
116 // 7. Listing roots
117 foreach ($bases as $type => $base) {
118 if ($path === $base) {
119 return [
120 'type' => 'listing',
121 'listing_type' => $type,
122 'base' => $base,
123 ];
124 }
125 }
126
127 if ($path === $trip_base) {
128 return [
129 'type' => 'listing',
130 'listing_type' => 'trip',
131 'base' => $trip_base,
132 ];
133 }
134
135 // 8. Booking confirmation (pageless: /{booking_base}/{confirmation_segment}/{ref}/ + legacy slug)
136 $booking_base = SettingsService::getBookingBase();
137 $confirmSeg = preg_quote($pb['booking_flow_confirmation_segment'], '/');
138 if (preg_match('/^' . preg_quote($booking_base, '/') . '\/' . $confirmSeg . '\/([a-zA-Z0-9_-]+)$/', $path, $matches)) {
139 return [
140 'type' => 'booking_confirmation',
141 'confirmation_id' => $matches[1],
142 ];
143 }
144
145 $legacyConfQuoted = preg_quote($pb['legacy_booking_confirmation_prefix'], '/');
146 if (preg_match('/^' . $legacyConfQuoted . '\/([a-zA-Z0-9_-]+)$/', $path, $matches)) {
147 return [
148 'type' => 'booking_confirmation',
149 'confirmation_id' => $matches[1],
150 ];
151 }
152
153 // 9. Remaining checkout (rewrite) and legacy checkout/ path
154 $remQuoted = preg_quote($pb['remaining_checkout_prefix'], '/');
155 if (preg_match('/^' . $remQuoted . '\/([a-zA-Z0-9_-]+)$/', $path, $matches)) {
156 return [
157 'type' => 'checkout',
158 'token' => $matches[1],
159 ];
160 }
161
162 if (preg_match('/^checkout\/([a-zA-Z0-9_-]+)$/', $path, $matches)) {
163 return [
164 'type' => 'checkout',
165 'token' => $matches[1],
166 ];
167 }
168
169 // 10. Booking hub / trip booking
170 if (!SettingsService::useCustomBookingPage()) {
171 if (preg_match('/^' . preg_quote($booking_base, '/') . '\/([^\/]+)\/?$/', $path, $matches)) {
172 return [
173 'type' => 'booking',
174 'page' => 'main',
175 'base' => $booking_base,
176 'trip' => $matches[1],
177 ];
178 }
179 if ($path === $booking_base) {
180 return [
181 'type' => 'booking',
182 'page' => 'main',
183 'base' => $booking_base,
184 ];
185 }
186 }
187
188 return null;
189 }
190
191 /**
192 * @return array{type: string, page: string, base: string}|null
193 */
194 private static function matchAccountRoute(string $path, string $account_base): ?array
195 {
196 $path_trim = rtrim($path, '/');
197 $base_trim = rtrim($account_base, '/');
198
199 if ($path_trim === $base_trim) {
200 $tab = isset($_GET['tab']) ? sanitize_key((string) $_GET['tab']) : '';
201
202 return [
203 'type' => 'account',
204 'page' => self::accountQueryTabToPage($tab),
205 'base' => $account_base,
206 ];
207 }
208
209 $quoted = preg_quote($account_base, '/');
210 if (preg_match('/^' . $quoted . '\/([^\/]+)/', $path, $m)) {
211 $page = self::accountPathSegmentToPage($m[1]);
212 if ($page === null) {
213 return null;
214 }
215
216 return [
217 'type' => 'account',
218 'page' => $page,
219 'base' => $account_base,
220 ];
221 }
222
223 return null;
224 }
225
226 private static function accountQueryTabToPage(string $tab): string
227 {
228 $allowed = ['dashboard', 'bookings', 'payments', 'documents', 'profile', 'saved-trips'];
229 if ($tab === '' || !in_array($tab, $allowed, true)) {
230 return 'dashboard';
231 }
232
233 return $tab;
234 }
235
236 private static function accountPathSegmentToPage(string $segment): ?string
237 {
238 $segment = sanitize_title($segment);
239 $map = [
240 'dashboard' => 'dashboard',
241 'profile' => 'profile',
242 'bookings' => 'bookings',
243 'payments' => 'payments',
244 'documents' => 'documents',
245 'support' => 'dashboard',
246 'saved-trips' => 'saved-trips',
247 'wishlist' => 'saved-trips',
248 'settings' => 'profile',
249 ];
250
251 return $map[$segment] ?? null;
252 }
253 }
254