| @@ -18,10 +18,26 @@ | ||
| 18 | 18 | public static function match(string $path): ?array |
| 19 | 19 | { |
| 20 | 20 | $path = trim($path, '/'); |
| 21 | 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 | + | |
| 22 | 37 | // 1. Email verification |
| 23 | - if (preg_match('/^yatra-verify-email\/([a-zA-Z0-9_-]+)$/', $path, $matches)) { | |
| 38 | + $evQuoted = preg_quote($pb['email_verification_prefix'], '/'); | |
| 39 | + if (preg_match('/^' . $evQuoted . '\/([a-zA-Z0-9_-]+)$/', $path, $matches)) { | |
| 24 | 40 | return [ |
| 25 | 41 | 'type' => 'email_verification', |
| 26 | 42 | 'token' => $matches[1], |
| 27 | 43 | ]; |
| @@ -26,18 +42,9 @@ | ||
| 26 | 42 | 'token' => $matches[1], |
| 27 | 43 | ]; |
| 28 | 44 | } |
| 29 | 45 | |
| 30 | - // 2. Dedicated login slug (rewrite: ^login/?$) | |
| 31 | - if ($path === 'login') { | |
| 32 | - return [ | |
| 33 | - 'type' => 'login', | |
| 34 | - 'page' => 'main', | |
| 35 | - 'base' => 'login', | |
| 36 | - ]; | |
| 37 | - } | |
| 38 | - | |
| 39 | - // 3. Account page | |
| 46 | + // 2. Account page | |
| 40 | 47 | $account_base = SettingsService::getAccountBase(); |
| 41 | 48 | $account_route = self::matchAccountRoute($path, $account_base); |
| 42 | 49 | if ($account_route !== null) { |
| 43 | 50 | return $account_route; |
| @@ -42,9 +49,9 @@ | ||
| 42 | 49 | if ($account_route !== null) { |
| 43 | 50 | return $account_route; |
| 44 | 51 | } |
| 45 | 52 | |
| 46 | - // 4. Trip archive pagination: {trip_base}/page/{n} | |
| 53 | + // 3. Trip archive pagination: {trip_base}/page/{n} | |
| 47 | 54 | $trip_base = SettingsService::getTripBase(); |
| 48 | 55 | if (preg_match('/^' . preg_quote($trip_base, '/') . '\/page\/(\d+)\/?$/', $path, $matches)) { |
| 49 | 56 | return [ |
| 50 | 57 | 'type' => 'listing', |
| @@ -53,14 +60,25 @@ | ||
| 53 | 60 | 'paged' => max(1, (int) $matches[1]), |
| 54 | 61 | ]; |
| 55 | 62 | } |
| 56 | 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 | + | |
| 57 | 75 | // 5. Single trip |
| 58 | 76 | if (preg_match('/^' . preg_quote($trip_base, '/') . '\/([^\/]+)\/?$/', $path, $matches)) { |
| 59 | 77 | if ($matches[1] !== 'page') { |
| 60 | 78 | return [ |
| 61 | 79 | 'type' => 'trip', |
| 62 | - 'slug' => $matches[1], | |
| 80 | + 'slug' => $decodeSlug($matches[1]), | |
| 63 | 81 | 'base' => $trip_base, |
| 64 | 82 | ]; |
| 65 | 83 | } |
| 66 | 84 | } |
| @@ -66,11 +84,11 @@ | ||
| 66 | 84 | } |
| 67 | 85 | |
| 68 | 86 | // 6. Taxonomy (pagination before single slug) |
| 69 | 87 | $bases = [ |
| 70 | - 'destination' => SettingsService::getString('destination_base', 'destination'), | |
| 71 | - 'activity' => SettingsService::getString('activity_base', 'activity'), | |
| 72 | - 'category' => SettingsService::getString('trip_category_base', 'trip-category'), | |
| 88 | + 'destination' => SettingsService::getDestinationBase(), | |
| 89 | + 'activity' => SettingsService::getActivityBase(), | |
| 90 | + 'category' => SettingsService::getTripCategoryBase(), | |
| 73 | 91 | ]; |
| 74 | 92 | |
| 75 | 93 | foreach ($bases as $type => $base) { |
| 76 | 94 | if (preg_match('/^' . preg_quote($base, '/') . '\/([^\/]+)\/page\/(\d+)\/?$/', $path, $matches)) { |
| @@ -76,9 +94,9 @@ | ||
| 76 | 94 | if (preg_match('/^' . preg_quote($base, '/') . '\/([^\/]+)\/page\/(\d+)\/?$/', $path, $matches)) { |
| 77 | 95 | return [ |
| 78 | 96 | 'type' => 'taxonomy', |
| 79 | 97 | 'taxonomy_type' => $type, |
| 80 | - 'slug' => $matches[1], | |
| 98 | + 'slug' => $decodeSlug($matches[1]), | |
| 81 | 99 | 'base' => $base, |
| 82 | 100 | 'paged' => max(1, (int) $matches[2]), |
| 83 | 101 | ]; |
| 84 | 102 | } |
| @@ -88,9 +106,9 @@ | ||
| 88 | 106 | if (preg_match('/^' . preg_quote($base, '/') . '\/([^\/]+)\/?$/', $path, $matches)) { |
| 89 | 107 | return [ |
| 90 | 108 | 'type' => 'taxonomy', |
| 91 | 109 | 'taxonomy_type' => $type, |
| 92 | - 'slug' => $matches[1], | |
| 110 | + 'slug' => $decodeSlug($matches[1]), | |
| 93 | 111 | 'base' => $base, |
| 94 | 112 | ]; |
| 95 | 113 | } |
| 96 | 114 | } |
| @@ -113,11 +131,12 @@ | ||
| 113 | 131 | 'base' => $trip_base, |
| 114 | 132 | ]; |
| 115 | 133 | } |
| 116 | 134 | |
| 117 | - // 8. Booking confirmation (pageless: /{booking_base}/confirmation/{ref}/ + legacy slug) | |
| 135 | + // 8. Booking confirmation (pageless: /{booking_base}/{confirmation_segment}/{ref}/ + legacy slug) | |
| 118 | 136 | $booking_base = SettingsService::getBookingBase(); |
| 119 | - if (preg_match('/^' . preg_quote($booking_base, '/') . '\/confirmation\/([a-zA-Z0-9_-]+)$/', $path, $matches)) { | |
| 137 | + $confirmSeg = preg_quote($pb['booking_flow_confirmation_segment'], '/'); | |
| 138 | + if (preg_match('/^' . preg_quote($booking_base, '/') . '\/' . $confirmSeg . '\/([a-zA-Z0-9_-]+)$/', $path, $matches)) { | |
| 120 | 139 | return [ |
| 121 | 140 | 'type' => 'booking_confirmation', |
| 122 | 141 | 'confirmation_id' => $matches[1], |
| 123 | 142 | ]; |
| @@ -122,9 +141,10 @@ | ||
| 122 | 141 | 'confirmation_id' => $matches[1], |
| 123 | 142 | ]; |
| 124 | 143 | } |
| 125 | 144 | |
| 126 | - if (preg_match('/^booking-confirmation\/([a-zA-Z0-9_-]+)$/', $path, $matches)) { | |
| 145 | + $legacyConfQuoted = preg_quote($pb['legacy_booking_confirmation_prefix'], '/'); | |
| 146 | + if (preg_match('/^' . $legacyConfQuoted . '\/([a-zA-Z0-9_-]+)$/', $path, $matches)) { | |
| 127 | 147 | return [ |
| 128 | 148 | 'type' => 'booking_confirmation', |
| 129 | 149 | 'confirmation_id' => $matches[1], |
| 130 | 150 | ]; |
| @@ -130,9 +150,10 @@ | ||
| 130 | 150 | ]; |
| 131 | 151 | } |
| 132 | 152 | |
| 133 | 153 | // 9. Remaining checkout (rewrite) and legacy checkout/ path |
| 134 | - if (preg_match('/^remaining-checkout\/([a-zA-Z0-9_-]+)$/', $path, $matches)) { | |
| 154 | + $remQuoted = preg_quote($pb['remaining_checkout_prefix'], '/'); | |
| 155 | + if (preg_match('/^' . $remQuoted . '\/([a-zA-Z0-9_-]+)$/', $path, $matches)) { | |
| 135 | 156 | return [ |
| 136 | 157 | 'type' => 'checkout', |
| 137 | 158 | 'token' => $matches[1], |
| 138 | 159 | ]; |