PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
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 / Ajax / GeocodingAjax.php

GeocodingAjax.php in Yatra – Travel Booking & Tour Operator Software 3.0.16, at app/Ajax/GeocodingAjax.php

223 lines 7.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\Ajax;
6
7 use Yatra\Repositories\GeocodingRepository;
8
9 /**
10 * AJAX handlers for Geocoding functionality
11 *
12 * Rate limiting uses WordPress transients (always on). Response caching goes through
13 * {@see GeocodingRepository} → {@see \Yatra\Utils\Cache} (not options).
14 */
15 class GeocodingAjax
16 {
17 private GeocodingRepository $geocodingRepository;
18
19 public function __construct()
20 {
21 $this->geocodingRepository = new GeocodingRepository();
22 add_action('wp_ajax_yatra_search_locations', [$this, 'searchLocations']);
23 add_action('wp_ajax_nopriv_yatra_search_locations', [$this, 'searchLocations']);
24 add_action('wp_ajax_yatra_reverse_geocode', [$this, 'reverseGeocode']);
25 add_action('wp_ajax_nopriv_yatra_reverse_geocode', [$this, 'reverseGeocode']);
26 }
27
28 /**
29 * Search for locations using Nominatim API
30 */
31 public function searchLocations(): void
32 {
33 // Verify nonce
34 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'yatra_geocoding_nonce')) {
35 wp_send_json_error(['message' => 'Security check failed']);
36 return;
37 }
38
39 $query = sanitize_text_field($_POST['query'] ?? '');
40 $limit = intval($_POST['limit'] ?? 5);
41
42 if (empty($query) || strlen($query) < 2) {
43 wp_send_json_error(['message' => 'Query too short']);
44 return;
45 }
46
47 // Rate limiting - check if user has made too many requests
48 $user_ip = $_SERVER['REMOTE_ADDR'];
49 $cache_key = 'yatra_geocoding_' . md5($user_ip);
50 $request_count = get_transient($cache_key);
51
52 if ($request_count === false) {
53 // First request, set counter
54 set_transient($cache_key, 1, 60); // 1 minute window
55 } elseif ($request_count >= 10) {
56 // Too many requests
57 wp_send_json_error(['message' => 'Too many requests. Please wait a moment.']);
58 return;
59 } else {
60 // Increment counter
61 set_transient($cache_key, $request_count + 1, 60);
62 }
63
64 $resultCacheKey = $this->geocodingRepository->cacheKeySearch($query, $limit);
65 $cached = $this->geocodingRepository->getPayload($resultCacheKey);
66 if (is_array($cached)) {
67 wp_send_json_success(['results' => $cached]);
68 return;
69 }
70
71 $limit = min(max($limit, 1), 50);
72
73 // Forward geocode: global results by default. Optional filter
74 // `yatra_nominatim_search_query_args` may add `countrycodes` etc. if a site must
75 // restrict to specific ISO regions.
76 $baseArgs = [
77 'format' => 'json',
78 'q' => $query,
79 'limit' => $limit,
80 'addressdetails' => 1,
81 'dedupe' => 1,
82 'accept-language' => $this->nominatimAcceptLanguage(),
83 ];
84
85 $args = apply_filters('yatra_nominatim_search_query_args', $baseArgs, $query, $limit);
86
87 $url = add_query_arg($args, 'https://nominatim.openstreetmap.org/search');
88
89 $response = wp_remote_get($url, [
90 'timeout' => 10,
91 'user-agent' => 'Yatra Travel Plugin (https://wpyatra.com/)',
92 'headers' => [
93 'Accept' => 'application/json',
94 ],
95 ]);
96
97 if (is_wp_error($response)) {
98 wp_send_json_error(['message' => 'Search failed. Please try again.']);
99 return;
100 }
101
102 $body = wp_remote_retrieve_body($response);
103 $data = json_decode($body, true);
104 if (!is_array($data) || empty($data)) {
105 wp_send_json_success(['results' => []]);
106 return;
107 }
108
109 $data = self::sortNominatimSearchResults($data);
110
111 $this->geocodingRepository->setPayload($resultCacheKey, $data, 300);
112
113 wp_send_json_success(['results' => $data]);
114 }
115
116 /**
117 * Reverse geocode coordinates to get location name
118 */
119 public function reverseGeocode(): void
120 {
121 // Verify nonce
122 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'yatra_geocoding_nonce')) {
123 wp_send_json_error(['message' => 'Security check failed']);
124 return;
125 }
126
127 $lat = floatval($_POST['lat'] ?? 0);
128 $lng = floatval($_POST['lng'] ?? 0);
129
130 if ($lat === 0 || $lng === 0) {
131 wp_send_json_error(['message' => 'Invalid coordinates']);
132 return;
133 }
134
135 $resultCacheKey = $this->geocodingRepository->cacheKeyReverse($lat, $lng);
136 $cached = $this->geocodingRepository->getPayload($resultCacheKey);
137 if (is_array($cached)) {
138 wp_send_json_success(['result' => $cached]);
139 return;
140 }
141
142 $url = add_query_arg([
143 'format' => 'json',
144 'lat' => $lat,
145 'lon' => $lng,
146 'zoom' => 18,
147 'addressdetails' => 1,
148 'accept-language' => $this->nominatimAcceptLanguage(),
149 ], 'https://nominatim.openstreetmap.org/reverse');
150
151 $response = wp_remote_get($url, [
152 'timeout' => 10,
153 'user-agent' => 'Yatra Travel Plugin (https://wpyatra.com/)',
154 'headers' => [
155 'Accept' => 'application/json',
156 ],
157 ]);
158
159 if (is_wp_error($response)) {
160 wp_send_json_error(['message' => 'Reverse geocoding failed']);
161 return;
162 }
163
164 $body = wp_remote_retrieve_body($response);
165 $data = json_decode($body, true);
166 if (!is_array($data)) {
167 wp_send_json_error(['message' => 'Reverse geocoding failed']);
168 return;
169 }
170
171 $this->geocodingRepository->setPayload($resultCacheKey, $data, 600);
172
173 wp_send_json_success(['result' => $data]);
174 }
175
176 /**
177 * Prefer the site / user locale for Nominatim labels.
178 * English regions like en-US can skew free-text search ordering toward US places;
179 * use a neutral "en" first, then the full tag, so global queries (e.g. "Japan") stay global.
180 */
181 private function nominatimAcceptLanguage(): string
182 {
183 $locale = function_exists('get_user_locale') ? (string) get_user_locale() : '';
184 if ($locale === '') {
185 $locale = (string) get_locale();
186 }
187 $primary = str_replace('_', '-', $locale);
188 if ($primary === '' || $primary === 'C') {
189 return 'en';
190 }
191
192 if (preg_match('/^en-/i', $primary)) {
193 return 'en,' . $primary . ';q=0.88,*;q=0.5';
194 }
195
196 return $primary . ',en;q=0.8';
197 }
198
199 /**
200 * Nominatim relevance order is not always ideal for short queries; re-rank by importance
201 * (countries/cities rank higher than homonymous hamlets). No country filter — full planet.
202 *
203 * @param list<array<string, mixed>> $rows
204 * @return list<array<string, mixed>>
205 */
206 private static function sortNominatimSearchResults(array $rows): array
207 {
208 usort($rows, static function (array $a, array $b): int {
209 $ia = isset($a['importance']) ? (float) $a['importance'] : 0.0;
210 $ib = isset($b['importance']) ? (float) $b['importance'] : 0.0;
211 if ($ia !== $ib) {
212 return $ib <=> $ia;
213 }
214 $rankA = isset($a['place_rank']) ? (int) $a['place_rank'] : 0;
215 $rankB = isset($b['place_rank']) ? (int) $b['place_rank'] : 0;
216
217 return $rankB <=> $rankA;
218 });
219
220 return $rows;
221 }
222 }
223