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
← All changes | app/Controllers/CustomerController.php +245 -19 3.0.4trunk View file →
@@ -61,8 +61,48 @@
61 61 'permission_callback' => [$this, 'checkCustomerPermission'],
62 62 ],
63 63 ]);
64 64
65 + // Change current customer's account password
66 + register_rest_route($namespace, '/' . $base . '/me/password', [
67 + [
68 + 'methods' => \WP_REST_Server::EDITABLE,
69 + 'callback' => [$this, 'updateMyPassword'],
70 + 'permission_callback' => [$this, 'checkCustomerPermission'],
71 + ],
72 + ]);
73 +
74 + // Request an account email change (WordPress pending-change pattern:
75 + // a confirmation link is emailed to the new address; nothing changes yet).
76 + // The confirmation link itself is a normal front-end URL handled by
77 + // AccountPageHandler — NOT a REST route — because a browser GET to REST
78 + // carries no nonce and would be treated as anonymous.
79 + register_rest_route($namespace, '/' . $base . '/me/email', [
80 + [
81 + 'methods' => \WP_REST_Server::CREATABLE,
82 + 'callback' => [$this, 'updateMyEmail'],
83 + 'permission_callback' => [$this, 'checkCustomerPermission'],
84 + ],
85 + ]);
86 +
87 + // Re-send the confirmation email for an already-pending email change.
88 + register_rest_route($namespace, '/' . $base . '/me/email/resend', [
89 + [
90 + 'methods' => \WP_REST_Server::CREATABLE,
91 + 'callback' => [$this, 'resendMyEmailConfirmation'],
92 + 'permission_callback' => [$this, 'checkCustomerPermission'],
93 + ],
94 + ]);
95 +
96 + // Cancel (dismiss) a pending email change — deletes the stored token.
97 + register_rest_route($namespace, '/' . $base . '/me/email', [
98 + [
99 + 'methods' => \WP_REST_Server::DELETABLE,
100 + 'callback' => [$this, 'cancelMyEmailChange'],
101 + 'permission_callback' => [$this, 'checkCustomerPermission'],
102 + ],
103 + ]);
104 +
65 105 // Current customer's bookings
66 106 register_rest_route($namespace, '/' . $base . '/my-bookings', [
67 107 [
68 108 'methods' => \WP_REST_Server::READABLE,
@@ -116,71 +156,74 @@
116 156 // =====================
117 157 // ADMIN ROUTES
118 158 // =====================
119 159
120 - // List & Create customers
160 + // List + create customers. List gates on view, create gates
161 + // on edit (creating a customer is a write).
121 162 register_rest_route($namespace, '/' . $base, [
122 163 [
123 164 'methods' => \WP_REST_Server::READABLE,
124 165 'callback' => [$this, 'getCustomers'],
125 - 'permission_callback' => [$this, 'checkAdminPermission'],
166 + 'permission_callback' => [$this, 'checkCanView'],
126 167 ],
127 168 [
128 169 'methods' => \WP_REST_Server::CREATABLE,
129 170 'callback' => [$this, 'createCustomer'],
130 - 'permission_callback' => [$this, 'checkAdminPermission'],
171 + 'permission_callback' => [$this, 'checkCanEdit'],
131 172 ],
132 173 ]);
133 174
134 - // Single customer operations
175 + // Single customer — read / update / delete with distinct caps.
135 176 register_rest_route($namespace, '/' . $base . '/(?P<id>\d+)', [
136 177 [
137 178 'methods' => \WP_REST_Server::READABLE,
138 179 'callback' => [$this, 'getCustomer'],
139 - 'permission_callback' => [$this, 'checkAdminPermission'],
180 + 'permission_callback' => [$this, 'checkCanView'],
140 181 ],
141 182 [
142 183 'methods' => \WP_REST_Server::EDITABLE,
143 184 'callback' => [$this, 'updateCustomer'],
144 - 'permission_callback' => [$this, 'checkAdminPermission'],
185 + 'permission_callback' => [$this, 'checkCanEdit'],
145 186 ],
146 187 [
147 188 'methods' => \WP_REST_Server::DELETABLE,
148 189 'callback' => [$this, 'deleteCustomer'],
149 - 'permission_callback' => [$this, 'checkAdminPermission'],
190 + 'permission_callback' => [$this, 'checkCanEdit'],
150 191 ],
151 192 ]);
152 193
153 - // Customer's bookings (admin)
194 + // Customer bookings list — view cap.
154 195 register_rest_route($namespace, '/' . $base . '/(?P<id>\d+)/bookings', [
155 196 [
156 197 'methods' => \WP_REST_Server::READABLE,
157 198 'callback' => [$this, 'getCustomerBookings'],
158 - 'permission_callback' => [$this, 'checkAdminPermission'],
199 + 'permission_callback' => [$this, 'checkCanView'],
159 200 ],
160 201 ]);
161 202
162 - // Merge customers
203 + // Merge customers — destructive write. Edit cap.
163 204 register_rest_route($namespace, '/' . $base . '/merge', [
164 205 [
165 206 'methods' => \WP_REST_Server::CREATABLE,
166 207 'callback' => [$this, 'mergeCustomers'],
167 - 'permission_callback' => [$this, 'checkAdminPermission'],
208 + 'permission_callback' => [$this, 'checkCanEdit'],
168 209 ],
169 210 ]);
170 211
171 - // Customer statistics
212 + // Customer statistics — view cap.
172 213 register_rest_route($namespace, '/' . $base . '/stats', [
173 214 [
174 215 'methods' => \WP_REST_Server::READABLE,
175 216 'callback' => [$this, 'getCustomerStats'],
176 - 'permission_callback' => [$this, 'checkAdminPermission'],
217 + 'permission_callback' => [$this, 'checkCanView'],
177 218 ],
178 219 ]);
179 220 }
180 221
181 222 /**
182 - * Check if user is logged in
223 + * Check if user is logged in (for /me, /my-bookings etc. — these
224 + * are customer-facing endpoints that work for any logged-in WP
225 + * user, not just team members).
183 226 */
184 227 public function checkCustomerPermission(): bool
185 228 {
186 229 return is_user_logged_in();
@@ -186,13 +229,33 @@
186 229 return is_user_logged_in();
187 230 }
188 231
189 232 /**
190 - * Check admin permission
233 + * Granular admin-side permission checks. WP administrators pass
234 + * every cap via the Team module's admin-fallback filter, so an
235 + * explicit `manage_options` check isn't needed here.
191 236 */
237 + public function checkCanView(): bool
238 + {
239 + return current_user_can('yatra_view_customers');
240 + }
241 +
242 + public function checkCanEdit(): bool
243 + {
244 + return current_user_can('yatra_edit_customers');
245 + }
246 +
247 + /**
248 + * @deprecated Kept for any external code referencing the old
249 + * method name. The previous implementation OR-ed
250 + * `yatra_manage_customers` which was never registered anywhere
251 + * — that arm has been removed because it was always false in
252 + * practice. Routes to view-only — admin users still pass via
253 + * the admin-fallback layer.
254 + */
192 255 public function checkAdminPermission(): bool
193 256 {
194 - return current_user_can('manage_options') || current_user_can('yatra_manage_customers');
257 + return $this->checkCanView();
195 258 }
196 259
197 260 // =========================================================================
198 261 // FRONTEND ENDPOINTS (Current User)
@@ -228,15 +291,47 @@
228 291 try {
229 292 $userId = get_current_user_id();
230 293 $data = $request->get_json_params();
231 294
295 + // Email is the account login and is intentionally NOT editable from
296 + // the account profile. Strip it server-side so it can never be
297 + // changed via this endpoint, regardless of what the client sends.
298 + if (is_array($data)) {
299 + unset($data['email'], $data['user_email']);
300 + }
301 +
232 302 Logger::apiRequest('/customers/me', 'PUT', $data);
233 303
234 304 $customer = $this->customerService->getCustomerByUserId($userId);
235 305
236 306 if (!$customer) {
237 - Logger::warning("Customer profile not found for user", ['user_id' => $userId]);
238 - return $this->not_found(__('Customer profile not found', 'yatra'));
307 + // No customer record yet (e.g. registered but never booked).
308 + // Create one linked to this user so the profile persists instead
309 + // of failing. Email stays the account login (never client-set).
310 + $wpUser = get_user_by('id', $userId);
311 + if (!$wpUser) {
312 + return $this->not_found(__('Customer profile not found', 'yatra'));
313 + }
314 +
315 + $createData = CustomerValidator::sanitize($data);
316 + $createData['user_id'] = $userId;
317 + $createData['email'] = $wpUser->user_email;
318 + if (empty($createData['first_name'])) {
319 + $createData['first_name'] = $wpUser->first_name !== ''
320 + ? $wpUser->first_name
321 + : ($wpUser->display_name !== '' ? $wpUser->display_name : $wpUser->user_login);
322 + }
323 + if (empty($createData['last_name'])) {
324 + $createData['last_name'] = (string) $wpUser->last_name;
325 + }
326 +
327 + $createResult = $this->customerService->createCustomer($createData);
328 + if (empty($createResult['success'])) {
329 + Logger::warning('Could not create customer profile on update', ['user_id' => $userId, 'result' => $createResult]);
330 + return $this->error_response($createResult['message'] ?? __('Failed to save profile.', 'yatra'), 400);
331 + }
332 +
333 + return $this->success_response($this->customerService->getAccountProfileForUser($userId));
239 334 }
240 335
241 336 $customerId = (int) $customer['id'];
242 337
@@ -251,9 +346,15 @@
251 346 return $this->error_response($result['message'] ?? 'Failed to update customer profile', 400);
252 347 }
253 348
254 349 Logger::info("Customer profile updated successfully", ['customer_id' => $customerId, 'user_id' => $userId]);
255 - return $this->success_response($result['data']);
350 +
351 + // updateCustomer() returns success/message only — return the fresh
352 + // profile so the response carries the updated values (and avoids an
353 + // "undefined key data" warning).
354 + return $this->success_response(
355 + $this->customerService->getAccountProfileForUser($userId)
356 + );
256 357
257 358 } catch (\Exception $e) {
258 359 Logger::error("Failed to update customer profile", ['user_id' => $userId ?? 0, 'data' => $data ?? [], 'error' => $e->getMessage()]);
259 360 return $this->handle_exception($e);
@@ -260,8 +361,104 @@
260 361 }
261 362 }
262 363
263 364 /**
365 + * PUT /customers/me/password - Change the current customer's account password.
366 + *
367 + * Requires the correct current password, then sets the new one and refreshes
368 + * the auth cookie so the customer stays logged in (wp_set_password otherwise
369 + * invalidates the current session).
370 + */
371 + public function updateMyPassword(WP_REST_Request $request)
372 + {
373 + $userId = get_current_user_id();
374 + if ($userId <= 0) {
375 + return $this->error_response(__('Authentication required.', 'yatra'), 401);
376 + }
377 +
378 + $data = $request->get_json_params();
379 + $current = isset($data['current_password']) ? (string) $data['current_password'] : '';
380 + $newPassword = isset($data['new_password']) ? (string) $data['new_password'] : '';
381 +
382 + if ($current === '' || $newPassword === '') {
383 + return $this->error_response(__('Current and new password are required.', 'yatra'), 400);
384 + }
385 +
386 + $user = get_user_by('id', $userId);
387 + if (!$user || !wp_check_password($current, $user->user_pass, $userId)) {
388 + return $this->error_response(__('Your current password is incorrect.', 'yatra'), 400);
389 + }
390 +
391 + wp_set_password($newPassword, $userId);
392 +
393 + // wp_set_password() invalidates the session token / logs the user out.
394 + // Re-establish the current session so the account page stays authenticated.
395 + wp_set_current_user($userId);
396 + wp_set_auth_cookie($userId, true);
397 +
398 + Logger::info('Customer changed account password', ['user_id' => $userId]);
399 +
400 + return $this->success_response(['updated' => true]);
401 + }
402 +
403 + /**
404 + * POST /customers/me/email - Request a change to the account login email.
405 + *
406 + * Follows WordPress core's pending-change pattern: the email is NOT changed
407 + * here. A confirmation link is emailed to the NEW address; the change only
408 + * applies once the customer clicks it (confirmed by AccountPageHandler).
409 + */
410 + public function updateMyEmail(WP_REST_Request $request)
411 + {
412 + $userId = get_current_user_id();
413 + if ($userId <= 0) {
414 + return $this->error_response(__('Authentication required.', 'yatra'), 401);
415 + }
416 +
417 + $data = $request->get_json_params();
418 + $newEmail = '';
419 + if (is_array($data)) {
420 + $newEmail = (string) ($data['email'] ?? $data['new_email'] ?? '');
421 + }
422 +
423 + $result = $this->customerService->requestEmailChange($userId, $newEmail);
424 +
425 + return new WP_REST_Response($result, empty($result['success']) ? 400 : 200);
426 + }
427 +
428 + /**
429 + * POST /customers/me/email/resend - Re-send the confirmation link for a
430 + * pending email change (reuses the existing token; nothing else changes).
431 + */
432 + public function resendMyEmailConfirmation(WP_REST_Request $request)
433 + {
434 + $userId = get_current_user_id();
435 + if ($userId <= 0) {
436 + return $this->error_response(__('Authentication required.', 'yatra'), 401);
437 + }
438 +
439 + $result = $this->customerService->resendEmailChangeConfirmation($userId);
440 +
441 + return new WP_REST_Response($result, empty($result['success']) ? 400 : 200);
442 + }
443 +
444 + /**
445 + * DELETE /customers/me/email - Cancel (dismiss) a pending email change,
446 + * discarding the stored token so the emailed link stops working.
447 + */
448 + public function cancelMyEmailChange(WP_REST_Request $request)
449 + {
450 + $userId = get_current_user_id();
451 + if ($userId <= 0) {
452 + return $this->error_response(__('Authentication required.', 'yatra'), 401);
453 + }
454 +
455 + $result = $this->customerService->cancelEmailChange($userId);
456 +
457 + return new WP_REST_Response($result, empty($result['success']) ? 400 : 200);
458 + }
459 +
460 + /**
264 461 * GET /customers/my-bookings - Get current customer's bookings
265 462 */
266 463 public function getMyBookings(WP_REST_Request $request): WP_REST_Response
267 464 {
@@ -427,10 +624,25 @@
427 624 public function createCustomer(WP_REST_Request $request): WP_REST_Response
428 625 {
429 626 $data = $request->get_json_params();
430 627
628 + // Creating a WordPress login account is a higher-privilege action than
629 + // adding a CRM record, so it needs the WP user-creation capability. A
630 + // staffer who can manage customers but not create users simply gets a
631 + // CRM-only record — the request still succeeds.
632 + if (!empty($data['create_account']) && !current_user_can('create_users')) {
633 + unset($data['create_account']);
634 + }
635 +
431 636 $result = $this->customerService->createCustomer($data);
432 637
638 + // A confirmation request (email already has a login) is not an error — the
639 + // client shows a prompt and re-submits with confirm_link_existing. Return
640 + // 200 so it isn't treated as a failed request.
641 + if (!empty($result['needs_link_confirmation'])) {
642 + return new WP_REST_Response($result, 200);
643 + }
644 +
433 645 if (!$result['success']) {
434 646 return new WP_REST_Response($result, 400);
435 647 }
436 648
@@ -444,9 +656,23 @@
444 656 {
445 657 $id = (int) $request->get_param('id');
446 658 $data = $request->get_json_params();
447 659
660 + // Adding a login account from the edit form is the same higher-privilege
661 + // action as on create, so it needs the WP user-creation capability. A
662 + // staffer who can edit customers but not create users just saves the edit
663 + // without an account being made.
664 + if (is_array($data) && !empty($data['create_account']) && !current_user_can('create_users')) {
665 + unset($data['create_account']);
666 + }
667 +
448 668 $result = $this->customerService->updateCustomer($id, $data);
669 +
670 + // Confirmation request (email already has a login) — not an error; 200 so
671 + // the client can prompt and re-submit with confirm_link_existing.
672 + if (!empty($result['needs_link_confirmation'])) {
673 + return new WP_REST_Response($result, 200);
674 + }
449 675
450 676 if (!$result['success']) {
451 677 return new WP_REST_Response($result, 400);
452 678 }