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/ReviewController.php +153 -15 3.0.3trunk View file →
@@ -44,60 +44,75 @@
44 44 // =====================
45 45 // ADMIN ROUTES
46 46 // =====================
47 47
48 - // List reviews
48 + // List reviews + create review (admin). View cap for list,
49 + // edit cap for create.
49 50 register_rest_route($namespace, '/' . $base, [
50 51 [
51 52 'methods' => \WP_REST_Server::READABLE,
52 53 'callback' => [$this, 'getReviews'],
53 - 'permission_callback' => [$this, 'checkAdminPermission'],
54 + 'permission_callback' => [$this, 'checkCanView'],
54 55 ],
56 + [
57 + // The admin "Add New Review" form (resources/js/pages/ReviewForm.tsx)
58 + // POSTs here. Distinct from the public-facing
59 + // `POST /trips/{trip_id}/reviews` route below: the admin path
60 + // bypasses the "already reviewed this trip" gate, accepts
61 + // any DB-valid status (incl. spam/trash post-3.0.5 migration),
62 + // and stamps `created_by` with the current admin user id.
63 + 'methods' => \WP_REST_Server::CREATABLE,
64 + 'callback' => [$this, 'createReview'],
65 + 'permission_callback' => [$this, 'checkCanEdit'],
66 + ],
55 67 ]);
56 68
57 - // Bulk actions
69 + // Bulk actions — moderation operations. Manage cap covers
70 + // approval workflows; delete actions inside the bulk handler
71 + // should re-check the delete cap.
58 72 register_rest_route($namespace, '/' . $base . '/bulk', [
59 73 [
60 74 'methods' => \WP_REST_Server::EDITABLE,
61 75 'callback' => [$this, 'bulkAction'],
62 - 'permission_callback' => [$this, 'checkAdminPermission'],
76 + 'permission_callback' => [$this, 'checkCanManage'],
63 77 ],
64 78 ]);
65 79
66 - // Get single review
80 + // Single review — read / update / delete with distinct caps.
67 81 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)', [
68 82 [
69 83 'methods' => \WP_REST_Server::READABLE,
70 84 'callback' => [$this, 'getReview'],
71 - 'permission_callback' => [$this, 'checkAdminPermission'],
85 + 'permission_callback' => [$this, 'checkCanView'],
72 86 ],
73 87 [
74 88 'methods' => \WP_REST_Server::EDITABLE,
75 89 'callback' => [$this, 'updateReview'],
76 - 'permission_callback' => [$this, 'checkAdminPermission'],
90 + 'permission_callback' => [$this, 'checkCanEdit'],
77 91 ],
78 92 [
79 93 'methods' => \WP_REST_Server::DELETABLE,
80 94 'callback' => [$this, 'deleteReview'],
81 - 'permission_callback' => [$this, 'checkAdminPermission'],
95 + 'permission_callback' => [$this, 'checkCanDelete'],
82 96 ],
83 97 ]);
84 98
85 - // Update review status
99 + // Update review status — moderation action. Manage cap (held
100 + // by Owner, Manager, Marketing).
86 101 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)/status', [
87 102 [
88 103 'methods' => \WP_REST_Server::EDITABLE,
89 104 'callback' => [$this, 'updateStatus'],
90 - 'permission_callback' => [$this, 'checkAdminPermission'],
105 + 'permission_callback' => [$this, 'checkCanManage'],
91 106 ],
92 107 ]);
93 108
94 - // Stats
109 + // Stats — view cap.
95 110 register_rest_route($namespace, '/' . $base . '/stats', [
96 111 [
97 112 'methods' => \WP_REST_Server::READABLE,
98 113 'callback' => [$this, 'getStats'],
99 - 'permission_callback' => [$this, 'checkAdminPermission'],
114 + 'permission_callback' => [$this, 'checkCanView'],
100 115 ],
101 116 ]);
102 117
103 118 // =====================
@@ -141,13 +156,46 @@
141 156 ]);
142 157 }
143 158
144 159 /**
145 - * Check admin permission
160 + * Granular admin-side permission checks. WP administrators pass
161 + * every cap via the Team module's admin-fallback filter, so an
162 + * explicit `manage_options` check isn't needed here.
146 163 */
164 + public function checkCanView(): bool
165 + {
166 + return current_user_can('yatra_view_reviews');
167 + }
168 +
169 + public function checkCanEdit(): bool
170 + {
171 + return current_user_can('yatra_edit_reviews');
172 + }
173 +
174 + public function checkCanManage(): bool
175 + {
176 + // Moderation cap — approve / spam / trash. Distinct from
177 + // edit so a Marketing role can moderate without being able
178 + // to edit the review body itself.
179 + return current_user_can('yatra_manage_reviews');
180 + }
181 +
182 + public function checkCanDelete(): bool
183 + {
184 + return current_user_can('yatra_delete_reviews');
185 + }
186 +
187 + /**
188 + * @deprecated Kept for any external code referencing the old
189 + * method name. Old implementation only checked `manage_options`,
190 + * so non-admin Yatra-role users were locked out of every
191 + * endpoint. New behaviour routes to the view cap which is more
192 + * permissive for legitimate team members; admin users still pass
193 + * via the admin-fallback layer.
194 + */
147 195 public function checkAdminPermission(): bool
148 196 {
149 - return current_user_can('manage_options');
197 + return $this->checkCanView();
150 198 }
151 199
152 200 /**
153 201 * Check if user is logged in
@@ -252,9 +300,9 @@
252 300 */
253 301 public function updateReview(WP_REST_Request $request): WP_REST_Response
254 302 {
255 303 $id = (int) $request->get_param('id');
256 - $data = $request->get_json_params();
304 + $data = $this->mapAdminReviewPayload($request->get_json_params() ?? []);
257 305
258 306 $result = $this->reviewService->updateReview($id, $data);
259 307
260 308 if (!$result['success']) {
@@ -261,8 +309,98 @@
261 309 return new WP_REST_Response($result, 400);
262 310 }
263 311
264 312 return new WP_REST_Response($result);
313 + }
314 +
315 + /**
316 + * POST /reviews — Admin "Add New Review" endpoint.
317 + *
318 + * The admin React form posts the operator-curated fields here. This
319 + * path differs from `submitReview` (which the public review form uses)
320 + * in three ways:
321 + *
322 + * 1. No "already reviewed this trip" gate — admins should be able to
323 + * enter reviews on behalf of customers without tripping the
324 + * duplicate-prevention guard.
325 + * 2. Honours the operator-supplied `status` rather than deriving it
326 + * from the `reviews.auto_approve` setting — the admin is the
327 + * authority for whether the row is pending / approved / spam etc.
328 + * 3. Stamps `created_by` with the current admin user id so audits
329 + * can attribute who added the review.
330 + */
331 + public function createReview(WP_REST_Request $request): WP_REST_Response
332 + {
333 + $data = $this->mapAdminReviewPayload($request->get_json_params() ?? []);
334 +
335 + // created_by is set here (controller) rather than in the service
336 + // so the service stays input-agnostic — service methods may also
337 + // be called from CLI / cron / tests where there's no current user.
338 + $data['created_by'] = get_current_user_id() ?: null;
339 +
340 + $result = $this->reviewService->createReviewAsAdmin($data);
341 +
342 + if (!$result['success']) {
343 + return new WP_REST_Response($result, 400);
344 + }
345 +
346 + return new WP_REST_Response($result, 201);
347 + }
348 +
349 + /**
350 + * Translate the admin form's payload shape into the field names the
351 + * service + repository expect.
352 + *
353 + * The admin React form (resources/js/pages/ReviewForm.tsx) ships:
354 + * - customer_name, customer_email, comment, verified, status
355 + *
356 + * The DB columns (and {@see ReviewRepository::prepareReviewData()})
357 + * speak:
358 + * - author_name, author_email, content, status
359 + * (no `verified` column exists yet — silently dropped)
360 + *
361 + * Doing this map at the controller layer keeps the service free of
362 + * UI-specific aliases, and means future UIs can either send the
363 + * legacy alias names or the canonical names with no double-mapping.
364 + *
365 + * @param array<string, mixed> $payload Raw JSON from the request.
366 + * @return array<string, mixed> Canonical, service-ready payload.
367 + */
368 + private function mapAdminReviewPayload(array $payload): array
369 + {
370 + // Field aliases: admin-side name → canonical DB-column name.
371 + $aliases = [
372 + 'customer_name' => 'author_name',
373 + 'customer_email' => 'author_email',
374 + 'comment' => 'content',
375 + ];
376 +
377 + foreach ($aliases as $from => $to) {
378 + if (array_key_exists($from, $payload) && !array_key_exists($to, $payload)) {
379 + $payload[$to] = $payload[$from];
380 + }
381 + // Don't unset the alias — leaving both is harmless because
382 + // prepareReviewData ignores unknown keys, and it keeps the
383 + // payload introspectable in logs.
384 + }
385 +
386 + // `verified` has no column in wp_yatra_reviews yet. Drop it
387 + // explicitly so a future log of the payload doesn't suggest the
388 + // value was honoured.
389 + if (array_key_exists('verified', $payload)) {
390 + unset($payload['verified']);
391 + }
392 +
393 + // Clamp status to the actual enum. Anything else gets coerced to
394 + // 'pending' so we never write '' (the silent-truncation pit that
395 + // motivated the Upgrade_3_0_5 migration in the first place).
396 + if (array_key_exists('status', $payload)) {
397 + $allowed = ['pending', 'approved', 'rejected', 'spam', 'trash'];
398 + $status = is_string($payload['status']) ? $payload['status'] : '';
399 + $payload['status'] = in_array($status, $allowed, true) ? $status : 'pending';
400 + }
401 +
402 + return $payload;
265 403 }
266 404
267 405 /**
268 406 * PUT /reviews/bulk - Bulk actions