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
← All changes | app/Repositories/SavedTripRepository.php +127 -35 3.0.2.8 → 3.0.16 View file →
@@ -18,8 +18,70 @@
18 18 */
19 19 private const META_KEY = 'yatra_saved_trips';
20 20
21 21 /**
22 + * Acquire a short-lived per-user lock to serialize read-modify-write on
23 + * the saved-trips meta. Without this, two concurrent tabs writing to the
24 + * same user's wishlist can lose one of the updates.
25 + */
26 + private function acquireUserLock(int $userId): bool
27 + {
28 + $key = 'yatra_saved_trips_lock_' . $userId;
29 + $deadline = microtime(true) + 1.5;
30 + do {
31 + // wp_cache_add returns false if the key already exists (atomic check-and-set).
32 + if (wp_cache_add($key, 1, 'yatra', 5)) {
33 + return true;
34 + }
35 + usleep(25000); // 25ms
36 + } while (microtime(true) < $deadline);
37 + return false;
38 + }
39 +
40 + private function releaseUserLock(int $userId): void
41 + {
42 + wp_cache_delete('yatra_saved_trips_lock_' . $userId, 'yatra');
43 + }
44 +
45 + /**
46 + * Remove a specific trip ID from every user that has it saved. Used by the
47 + * trip-deletion cleanup hook so orphan IDs don't accumulate forever.
48 + */
49 + public function removeTripFromAllUsers(int $tripId): int
50 + {
51 + if ($tripId <= 0) {
52 + return 0;
53 + }
54 +
55 + global $wpdb;
56 + $rows = $wpdb->get_results(
57 + $wpdb->prepare(
58 + "SELECT user_id, meta_value FROM {$wpdb->usermeta} WHERE meta_key = %s",
59 + self::META_KEY
60 + )
61 + );
62 + if (!$rows) {
63 + return 0;
64 + }
65 +
66 + $touched = 0;
67 + foreach ($rows as $row) {
68 + $raw = maybe_unserialize($row->meta_value);
69 + $ids = $this->normalizeSavedTripIdsFromMeta(is_array($raw) ? $raw : []);
70 + if (!in_array($tripId, $ids, true)) {
71 + continue;
72 + }
73 + $filtered = array_values(array_filter(
74 + $ids,
75 + static fn (int $id): bool => $id !== $tripId
76 + ));
77 + update_user_meta((int) $row->user_id, self::META_KEY, $filtered);
78 + $touched++;
79 + }
80 + return $touched;
81 + }
82 +
83 + /**
22 84 * Normalize stored meta to a list of trip IDs (handles legacy rows with trip_id keys).
23 85 *
24 86 * @param mixed $savedData
25 87 * @return list<int>
@@ -78,32 +140,40 @@
78 140 if ($tripId <= 0) {
79 141 return false;
80 142 }
81 143
82 - $savedData = get_user_meta($userId, self::META_KEY, true);
83 - $savedTripIds = $this->normalizeSavedTripIdsFromMeta(is_array($savedData) ? $savedData : []);
84 -
85 - if (in_array($tripId, $savedTripIds, true)) {
86 - return true;
87 - }
88 -
89 144 $tripRepository = new TripRepository();
90 145 $trip = $tripRepository->find($tripId);
91 -
92 146 if (!$trip) {
93 147 return false;
94 148 }
95 149
96 - $savedTripIds[] = $tripId;
97 - $savedTripIds = array_values(array_unique($savedTripIds));
150 + // Serialize concurrent writes per user so two tabs can't lose each
151 + // other's updates on the read-modify-write of the meta row. We still
152 + // proceed if the lock can't be acquired — better to risk a rare
153 + // overwrite than to block the user entirely.
154 + $this->acquireUserLock($userId);
155 + try {
156 + $savedData = get_user_meta($userId, self::META_KEY, true);
157 + $savedTripIds = $this->normalizeSavedTripIdsFromMeta(is_array($savedData) ? $savedData : []);
98 158
99 - $updated = update_user_meta($userId, self::META_KEY, $savedTripIds);
100 - if ($updated !== false) {
101 - return true;
159 + if (in_array($tripId, $savedTripIds, true)) {
160 + return true;
161 + }
162 +
163 + $savedTripIds[] = $tripId;
164 + $savedTripIds = array_values(array_unique($savedTripIds));
165 +
166 + $updated = update_user_meta($userId, self::META_KEY, $savedTripIds);
167 + if ($updated !== false) {
168 + return true;
169 + }
170 +
171 + // update_user_meta() returns false when the value is unchanged; treat as success if the trip is stored.
172 + return $this->isSaved($userId, $tripId);
173 + } finally {
174 + $this->releaseUserLock($userId);
102 175 }
103 -
104 - // update_user_meta() returns false when the value is unchanged; treat as success if the trip is stored.
105 - return $this->isSaved($userId, $tripId);
106 176 }
107 177
108 178 /**
109 179 * Remove saved trip
@@ -114,33 +184,42 @@
114 184 */
115 185 public function removeTrip(int $userId, int $tripId): bool
116 186 {
117 187 $tripId = (int) $tripId;
118 - if ($tripId <= 0 || !$this->isSaved($userId, $tripId)) {
188 + if ($tripId <= 0) {
119 189 return false;
120 190 }
121 191
122 - $savedData = get_user_meta($userId, self::META_KEY, true);
123 - if (!is_array($savedData) || $savedData === []) {
124 - return false;
125 - }
192 + $this->acquireUserLock($userId);
193 + try {
194 + $savedData = get_user_meta($userId, self::META_KEY, true);
195 + if (!is_array($savedData) || $savedData === []) {
196 + return false;
197 + }
126 198
127 - $before = $this->normalizeSavedTripIdsFromMeta($savedData);
128 - $savedTripIds = array_values(array_filter(
129 - $before,
130 - static fn (int $id): bool => $id !== $tripId
131 - ));
199 + $before = $this->normalizeSavedTripIdsFromMeta($savedData);
200 + if (!in_array($tripId, $before, true)) {
201 + return false;
202 + }
132 203
133 - if (count($savedTripIds) === count($before)) {
134 - return false;
135 - }
204 + $savedTripIds = array_values(array_filter(
205 + $before,
206 + static fn (int $id): bool => $id !== $tripId
207 + ));
136 208
137 - $updated = update_user_meta($userId, self::META_KEY, $savedTripIds);
138 - if ($updated !== false) {
139 - return true;
209 + if (count($savedTripIds) === count($before)) {
210 + return false;
211 + }
212 +
213 + $updated = update_user_meta($userId, self::META_KEY, $savedTripIds);
214 + if ($updated !== false) {
215 + return true;
216 + }
217 +
218 + return !$this->isSaved($userId, $tripId);
219 + } finally {
220 + $this->releaseUserLock($userId);
140 221 }
141 -
142 - return !$this->isSaved($userId, $tripId);
143 222 }
144 223
145 224 /**
146 225 * Get user's saved trips (fetches fresh data from database)
@@ -248,15 +327,28 @@
248 327
249 328 // Format duration using helper function
250 329 $durationDays = !empty($tripObj->duration_days) ? (int) $tripObj->duration_days : null;
251 330 $durationNights = !empty($tripObj->duration_nights) ? (int) $tripObj->duration_nights : null;
331 + // Hour-based day tours show "8 hours" instead of "1 day". 0 for every
332 + // existing (day-based) trip, so their wording is unchanged.
333 + $durationHours = !empty($tripObj->duration_hours) ? (int) $tripObj->duration_hours : 0;
252 334 $duration = '';
253 - if (!empty($durationDays)) {
335 + if ($durationHours > 0) {
336 + $duration = function_exists('yatra_format_duration')
337 + ? yatra_format_duration(0, null, $durationHours)
338 + : sprintf(
339 + /* translators: %d: number of hours. */
340 + _n('%d hour', '%d hours', $durationHours, 'yatra'),
341 + $durationHours
342 + );
343 + } elseif (!empty($durationDays)) {
254 344 if (function_exists('yatra_format_duration')) {
255 345 $duration = yatra_format_duration($durationDays, $durationNights);
256 346 } else {
347 + /* translators: %d: number of days. */
257 348 $duration = sprintf(__('%d Days', 'yatra'), $durationDays);
258 349 if (!empty($durationNights)) {
350 + /* translators: %d: number of nights. */
259 351 $duration .= ' / ' . sprintf(__('%d Nights', 'yatra'), $durationNights);
260 352 }
261 353 }
262 354 } else {