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
yatra / app / Controllers / AttributeController.php

AttributeController.php in Yatra – Travel Booking & Tour Operator Software trunk, at app/Controllers/AttributeController.php

995 lines 37.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\Controllers;
6
7 use WP_REST_Request;
8 use WP_REST_Response;
9 use WP_Error;
10 use Yatra\Services\AttributeService;
11
12 /**
13 * Attribute REST API Controller
14 *
15 * Handles REST API endpoints for trip attributes
16 *
17 * Endpoints:
18 * GET /yatra/v1/attributes - List attributes
19 * POST /yatra/v1/attributes - Create attribute
20 * GET /yatra/v1/attributes/{id} - Get single attribute
21 * PUT /yatra/v1/attributes/{id} - Update attribute
22 * DELETE /yatra/v1/attributes/{id} - Delete attribute
23 * POST /yatra/v1/attributes/orders - Update display orders
24 * GET /yatra/v1/attributes/search - Search attributes
25 * GET /yatra/v1/attributes/frontend - Get frontend attributes
26 * GET /yatra/v1/attributes/filterable - Get filterable attributes
27 * GET /yatra/v1/attributes/values/{id} - Get attribute values
28 * GET /yatra/v1/trips/{id}/attributes - Get trip attributes
29 * POST /yatra/v1/trips/{id}/attributes - Set trip attribute
30 * DELETE /yatra/v1/trips/{id}/attributes/{attr_id} - Remove trip attribute
31 */
32 class AttributeController extends BaseController
33 {
34 /**
35 * @var AttributeService
36 */
37 private $attributeService;
38
39 /**
40 * Constructor
41 */
42 public function __construct()
43 {
44 $this->rest_base = 'attributes';
45 $this->attributeService = new AttributeService();
46 }
47
48 /**
49 * Register REST API routes
50 */
51 public function register_routes(): void
52 {
53 if (defined('WP_DEBUG') && WP_DEBUG) {
54 }
55
56 // Standard CRUD routes
57 $this->registerCrudRoutes();
58
59 // Additional routes
60 register_rest_route($this->namespace, "/{$this->rest_base}/search", [
61 'methods' => 'GET',
62 'callback' => [$this, 'search'],
63 'permission_callback' => [$this, 'search_permissions_check'],
64 'args' => [
65 'q' => [
66 'required' => true,
67 'type' => 'string',
68 'minLength' => 2,
69 'sanitize_callback' => 'sanitize_text_field',
70 ],
71 ],
72 ]);
73
74 register_rest_route($this->namespace, "/{$this->rest_base}/frontend", [
75 'methods' => 'GET',
76 'callback' => [$this, 'get_frontend_attributes'],
77 'permission_callback' => '__return_true', // Public endpoint
78 ]);
79
80 register_rest_route($this->namespace, "/{$this->rest_base}/filterable", [
81 'methods' => 'GET',
82 'callback' => [$this, 'get_filterable_attributes'],
83 'permission_callback' => '__return_true', // Public endpoint
84 ]);
85
86 // Stats endpoint
87 register_rest_route($this->namespace, "/{$this->rest_base}/stats", [
88 'methods' => 'GET',
89 'callback' => [$this, 'getStats'],
90 'permission_callback' => [$this, 'get_permissions_check'],
91 ]);
92
93 register_rest_route($this->namespace, "/{$this->rest_base}/values/(?P<id>\d+)", [
94 'methods' => 'GET',
95 'callback' => [$this, 'get_attribute_values'],
96 'permission_callback' => '__return_true', // Public endpoint
97 'args' => [
98 'id' => [
99 'required' => true,
100 'type' => 'integer',
101 'validate_callback' => function($param) {
102 return is_numeric($param) && $param > 0;
103 },
104 ],
105 ],
106 ]);
107
108 // Bulk operations
109 register_rest_route($this->namespace, "/{$this->rest_base}/bulk", [
110 'methods' => \WP_REST_Server::CREATABLE,
111 'callback' => [$this, 'bulkAction'],
112 'permission_callback' => [$this, 'check_permission'],
113 ]);
114
115 register_rest_route($this->namespace, "/{$this->rest_base}/orders", [
116 'methods' => 'POST',
117 'callback' => [$this, 'update_display_orders'],
118 'permission_callback' => [$this, 'check_permission'],
119 'args' => [
120 'orders' => [
121 'required' => true,
122 'type' => 'array',
123 'validate_callback' => function($param) {
124 return is_array($param) && !empty($param);
125 },
126 ],
127 ],
128 ]);
129
130 register_rest_route($this->namespace, "/{$this->rest_base}/check-slug", [
131 'methods' => 'GET',
132 'callback' => [$this, 'check_slug'],
133 'permission_callback' => [$this, 'get_permissions_check'],
134 'args' => [
135 'slug' => [
136 'required' => true,
137 'type' => 'string',
138 'sanitize_callback' => 'sanitize_text_field',
139 ],
140 'exclude_id' => [
141 'required' => false,
142 'type' => 'integer',
143 'validate_callback' => function($param) {
144 return is_numeric($param) && $param > 0;
145 },
146 ],
147 ],
148 ]);
149
150 // Trip attribute routes
151 register_rest_route($this->namespace, "/trips/(?P<trip_id>\d+)/attributes", [
152 'methods' => 'GET',
153 'callback' => [$this, 'get_trip_attributes'],
154 'permission_callback' => '__return_true', // Public endpoint
155 'args' => [
156 'trip_id' => [
157 'required' => true,
158 'type' => 'integer',
159 'validate_callback' => function($param) {
160 return is_numeric($param) && $param > 0;
161 },
162 ],
163 ],
164 ]);
165
166 register_rest_route($this->namespace, "/trips/(?P<trip_id>\d+)/attributes", [
167 'methods' => 'POST',
168 'callback' => [$this, 'set_trip_attribute'],
169 'permission_callback' => [$this, 'update_permissions_check'],
170 'args' => [
171 'trip_id' => [
172 'required' => true,
173 'type' => 'integer',
174 'validate_callback' => function($param) {
175 return is_numeric($param) && $param > 0;
176 },
177 ],
178 'attribute_id' => [
179 'required' => true,
180 'type' => 'integer',
181 'validate_callback' => function($param) {
182 return is_numeric($param) && $param > 0;
183 },
184 ],
185 'value' => [
186 'required' => true,
187 'type' => 'string',
188 'sanitize_callback' => 'wp_kses_post',
189 ],
190 ],
191 ]);
192
193 register_rest_route($this->namespace, "/trips/(?P<trip_id>\d+)/attributes/(?P<attribute_id>\d+)", [
194 'methods' => 'DELETE',
195 'callback' => [$this, 'remove_trip_attribute'],
196 'permission_callback' => [$this, 'update_permissions_check'],
197 'args' => [
198 'trip_id' => [
199 'required' => true,
200 'type' => 'integer',
201 'validate_callback' => function($param) {
202 return is_numeric($param) && $param > 0;
203 },
204 ],
205 'attribute_id' => [
206 'required' => true,
207 'type' => 'integer',
208 'validate_callback' => function($param) {
209 return is_numeric($param) && $param > 0;
210 },
211 ],
212 ],
213 ]);
214 }
215
216 /**
217 * Get all attributes
218 */
219 public function get_items(WP_REST_Request $request): WP_REST_Response
220 {
221 try {
222 $params = $request->get_params();
223
224 // Get pagination parameters
225 $page = isset($params['page']) ? (int) $params['page'] : 1;
226 $perPage = isset($params['per_page']) ? (int) $params['per_page'] : 10;
227
228 // Build filters
229 $filters = [];
230 if (!empty($params['status'])) {
231 $filters['status'] = $params['status'];
232 }
233 if (!empty($params['field_type'])) {
234 $filters['field_type'] = $params['field_type'];
235 }
236 if (!empty($params['show_on_frontend'])) {
237 $filters['show_on_frontend'] = $params['show_on_frontend'];
238 }
239 if (!empty($params['show_in_filters'])) {
240 $filters['show_in_filters'] = $params['show_in_filters'];
241 }
242
243 // Add search filter
244 if (!empty($params['search'])) {
245 $filters['search'] = $params['search'];
246 }
247
248 // Add sorting
249 if (!empty($params['orderby'])) {
250 $filters['orderby'] = $params['orderby'];
251 }
252 if (!empty($params['order'])) {
253 $filters['order'] = $params['order'];
254 }
255
256 // Get paginated results
257 $result = $this->attributeService->paginate($page, $perPage, $filters);
258 $total = $this->attributeService->count($filters);
259
260 // Process icon fields and metadata for all attributes
261 foreach ($result as $attribute) {
262 // Parse metadata JSON and extract attribute properties
263 if (!empty($attribute->metadata)) {
264 $metadata = json_decode($attribute->metadata, true);
265 if (is_array($metadata)) {
266 // Add metadata fields as properties to the attribute object
267 $attribute->field_type = $metadata['field_type'] ?? null;
268 $attribute->required = $metadata['required'] ?? false;
269 $attribute->show_on_frontend = $metadata['show_on_frontend'] ?? false;
270 $attribute->show_in_filters = $metadata['show_in_filters'] ?? false;
271 $attribute->filter_type = $metadata['filter_type'] ?? null;
272 $attribute->searchable = $metadata['searchable'] ?? false;
273 $attribute->display_order = $metadata['display_order'] ?? 0;
274 $attribute->default_value = $metadata['default_value'] ?? null;
275 $attribute->placeholder = $metadata['placeholder'] ?? null;
276 $attribute->field_options = $metadata['field_options'] ?? null;
277 $attribute->validation_rules = $metadata['validation_rules'] ?? null;
278 }
279 }
280
281 if (!empty($attribute->icon)) {
282 $icon_data = maybe_unserialize($attribute->icon);
283 if (is_array($icon_data)) {
284 // Resolve image URLs for image type icons
285 if ($icon_data['type'] === 'image' && !empty($icon_data['value'])) {
286 $value = $icon_data['value'];
287 $image_url = '';
288
289 if (is_numeric($value)) {
290 $maybe_url = wp_get_attachment_image_url((int) $value, 'large');
291 if (!empty($maybe_url)) {
292 $image_url = $maybe_url;
293 }
294 } elseif (is_string($value) && filter_var($value, FILTER_VALIDATE_URL)) {
295 $image_url = $value;
296 }
297
298 $icon_data['value'] = $image_url;
299 }
300 $attribute->icon = $icon_data;
301 } else {
302 // Handle legacy string format
303 $attribute->icon = [
304 'type' => 'icon',
305 'value' => $attribute->icon
306 ];
307 }
308 } else {
309 $attribute->icon = null;
310 }
311 }
312
313 return $this->paginated_response($result, $total, $page, $perPage);
314
315 } catch (\Exception $e) {
316 return $this->error_response('Failed to retrieve attributes: ' . $e->getMessage(), 500);
317 }
318 }
319
320 /**
321 * Create attribute
322 */
323 public function create_item(WP_REST_Request $request)
324 {
325 try {
326 $data = $this->prepare_item_for_database($request);
327
328 // Debug logging for icon data
329 // Handle slug conflicts by generating unique slug if needed
330 $originalSlug = $data['slug'];
331 $slug = $originalSlug;
332 $counter = 1;
333
334 while ($this->attributeService->slugExists($slug)) {
335 $slug = $originalSlug . '-' . $counter;
336 $counter++;
337 }
338
339 $data['slug'] = $slug;
340
341 $attributeId = $this->attributeService->createAttribute($data);
342
343 if ($attributeId) {
344 $attribute = $this->attributeService->getById($attributeId);
345
346 // Parse metadata JSON and extract attribute properties
347 if (!empty($attribute->metadata)) {
348 $metadata = json_decode($attribute->metadata, true);
349 if (is_array($metadata)) {
350 // Add metadata fields as properties to the attribute object
351 $attribute->field_type = $metadata['field_type'] ?? null;
352 $attribute->required = $metadata['required'] ?? false;
353 $attribute->show_on_frontend = $metadata['show_on_frontend'] ?? false;
354 $attribute->show_in_filters = $metadata['show_in_filters'] ?? false;
355 $attribute->filter_type = $metadata['filter_type'] ?? null;
356 $attribute->searchable = $metadata['searchable'] ?? false;
357 $attribute->display_order = $metadata['display_order'] ?? 0;
358 $attribute->default_value = $metadata['default_value'] ?? null;
359 $attribute->placeholder = $metadata['placeholder'] ?? null;
360 $attribute->field_options = $metadata['field_options'] ?? null;
361 $attribute->validation_rules = $metadata['validation_rules'] ?? null;
362 }
363 }
364
365 $this->attachDecodedIconToAttribute($attribute);
366
367 return $this->success_response($attribute, 201);
368 }
369
370 return $this->error_response('Failed to create attribute', 500);
371
372 } catch (\Exception $e) {
373 return $this->error_response($e->getMessage(), 400);
374 }
375 }
376
377 /**
378 * Get single attribute
379 */
380 public function get_item(WP_REST_Request $request)
381 {
382 try {
383 $id = (int) $request->get_param('id');
384
385 $attribute = $this->attributeService->getById($id);
386
387 if (!$attribute) {
388 return $this->error_response('Attribute not found', 404);
389 }
390
391 // Parse metadata JSON and extract attribute properties
392 if (!empty($attribute->metadata)) {
393 $metadata = json_decode($attribute->metadata, true);
394 if (is_array($metadata)) {
395 // Add metadata fields as properties to the attribute object
396 $attribute->field_type = $metadata['field_type'] ?? null;
397 $attribute->required = $metadata['required'] ?? false;
398 $attribute->show_on_frontend = $metadata['show_on_frontend'] ?? false;
399 $attribute->show_in_filters = $metadata['show_in_filters'] ?? false;
400 $attribute->filter_type = $metadata['filter_type'] ?? null;
401 $attribute->searchable = $metadata['searchable'] ?? false;
402 $attribute->display_order = $metadata['display_order'] ?? 0;
403 $attribute->default_value = $metadata['default_value'] ?? null;
404 $attribute->placeholder = $metadata['placeholder'] ?? null;
405 $attribute->field_options = $metadata['field_options'] ?? null;
406 $attribute->validation_rules = $metadata['validation_rules'] ?? null;
407 }
408 }
409
410 $this->attachDecodedIconToAttribute($attribute);
411
412 return $this->success_response($attribute);
413
414 } catch (\Exception $e) {
415 return $this->error_response('Failed to retrieve attribute: ' . $e->getMessage(), 500);
416 }
417 }
418
419 /**
420 * Update attribute
421 */
422 public function update_item(WP_REST_Request $request)
423 {
424 try {
425 $id = (int) $request->get_param('id');
426 $data = $this->prepare_item_for_database($request);
427
428 $result = $this->attributeService->updateAttribute($id, $data);
429
430 if ($result) {
431 $attribute = $this->attributeService->getById($id);
432
433 // Parse metadata JSON and extract attribute properties
434 if (!empty($attribute->metadata)) {
435 $metadata = json_decode($attribute->metadata, true);
436 if (is_array($metadata)) {
437 // Add metadata fields as properties to the attribute object
438 $attribute->field_type = $metadata['field_type'] ?? null;
439 $attribute->required = $metadata['required'] ?? false;
440 $attribute->show_on_frontend = $metadata['show_on_frontend'] ?? false;
441 $attribute->show_in_filters = $metadata['show_in_filters'] ?? false;
442 $attribute->filter_type = $metadata['filter_type'] ?? null;
443 $attribute->searchable = $metadata['searchable'] ?? false;
444 $attribute->display_order = $metadata['display_order'] ?? 0;
445 $attribute->default_value = $metadata['default_value'] ?? null;
446 $attribute->placeholder = $metadata['placeholder'] ?? null;
447 $attribute->field_options = $metadata['field_options'] ?? null;
448 $attribute->validation_rules = $metadata['validation_rules'] ?? null;
449 }
450 }
451
452 return $this->success_response($attribute);
453 }
454
455 return $this->error_response('Failed to update attribute', 500);
456
457 } catch (\Exception $e) {
458 return $this->error_response($e->getMessage(), 400);
459 }
460 }
461
462 /**
463 * Delete attribute
464 */
465 public function delete_item(WP_REST_Request $request)
466 {
467 try {
468 $id = (int) $request->get_param('id');
469
470 $result = $this->attributeService->deleteAttribute($id);
471
472 if ($result) {
473 return $this->success_response(['deleted' => true]);
474 }
475
476 return $this->error_response('Failed to delete attribute', 500);
477
478 } catch (\Exception $e) {
479 return $this->error_response($e->getMessage(), 400);
480 }
481 }
482
483 /**
484 * Handle bulk operations
485 */
486 public function bulkAction(WP_REST_Request $request): WP_REST_Response
487 {
488 try {
489 $action = sanitize_text_field((string) $request->get_param('action'));
490 $ids = $request->get_param('ids');
491
492 if (empty($action)) {
493 return $this->error_response(__('Action is required', 'yatra'), 400);
494 }
495
496 if (empty($ids) || !is_array($ids)) {
497 return $this->error_response(__('No attributes selected', 'yatra'), 400);
498 }
499
500 switch ($action) {
501 case 'trash':
502 $result = $this->attributeService->bulkUpdateStatus($ids, 'trash');
503 break;
504 case 'publish':
505 case 'restore':
506 $result = $this->attributeService->bulkUpdateStatus($ids, 'publish');
507 break;
508 case 'draft':
509 $result = $this->attributeService->bulkUpdateStatus($ids, 'draft');
510 break;
511 case 'delete':
512 $result = $this->attributeService->bulkDelete($ids);
513 break;
514 default:
515 return $this->error_response(__('Invalid action', 'yatra'), 400);
516 }
517
518 return $this->success_response($result);
519
520 } catch (\InvalidArgumentException $e) {
521 return $this->error_response($e->getMessage(), 400);
522 } catch (\Exception $e) {
523 return $this->error_response($e->getMessage(), 500);
524 }
525 }
526
527 /**
528 * Get attribute values
529 */
530 public function get_attribute_values(WP_REST_Request $request)
531 {
532 try {
533 $id = (int) $request->get_param('id');
534
535 $values = $this->attributeService->getAttributeValues($id);
536
537 return $this->success_response($values);
538
539 } catch (\Exception $e) {
540 return $this->error_response('Failed to retrieve attribute values: ' . $e->getMessage(), 500);
541 }
542 }
543
544 /**
545 * Update display orders
546 */
547 public function update_orders(WP_REST_Request $request)
548 {
549 try {
550 $orders = $request->get_param('orders');
551
552 $result = $this->attributeService->updateDisplayOrders($orders);
553
554 if ($result) {
555 return $this->success_response(['updated' => true]);
556 }
557
558 return $this->error_response('Failed to update display orders', 500);
559
560 } catch (\Exception $e) {
561 return $this->error_response($e->getMessage(), 400);
562 }
563 }
564
565 /**
566 * Update display orders
567 */
568 public function update_display_orders(WP_REST_Request $request)
569 {
570 try {
571 $orders = $request->get_param('orders');
572
573 $result = $this->attributeService->updateDisplayOrders($orders);
574
575 if ($result) {
576 return $this->success_response(['message' => 'Display orders updated successfully']);
577 }
578
579 return $this->error_response('Failed to update display orders', 500);
580
581 } catch (\Exception $e) {
582 return $this->error_response($e->getMessage(), 400);
583 }
584 }
585
586 /**
587 * Check if slug exists and suggest unique slug if needed
588 */
589 public function check_slug(WP_REST_Request $request)
590 {
591 try {
592 $slug = sanitize_title($request->get_param('slug'));
593 $excludeId = $request->get_param('exclude_id');
594
595 if (empty($slug)) {
596 return $this->error_response('Slug is required', 400);
597 }
598
599 // Check if slug exists (excluding current attribute if editing)
600 $exists = $this->attributeService->slugExists($slug, $excludeId);
601
602 if (!$exists) {
603 return $this->success_response([
604 'exists' => false,
605 'suggested_slug' => $slug
606 ]);
607 }
608
609 // Generate unique slug
610 $originalSlug = $slug;
611 $counter = 1;
612
613 while ($this->attributeService->slugExists($slug, $excludeId)) {
614 $slug = $originalSlug . '-' . $counter;
615 $counter++;
616
617 // Prevent infinite loop
618 if ($counter > 100) {
619 break;
620 }
621 }
622
623 return $this->success_response([
624 'exists' => true,
625 'suggested_slug' => $slug
626 ]);
627
628 } catch (\Exception $e) {
629 return $this->error_response($e->getMessage(), 400);
630 }
631 }
632
633 /**
634 * Get trip attributes
635 */
636 public function get_trip_attributes(WP_REST_Request $request): WP_REST_Response
637 {
638 try {
639 $tripId = (int) $request->get_param('trip_id');
640
641 $attributes = $this->attributeService->getTripAttributes($tripId);
642
643 return $this->success_response($attributes);
644
645 } catch (\Exception $e) {
646 return $this->error_response('Failed to retrieve trip attributes: ' . $e->getMessage(), 500);
647 }
648 }
649
650 /**
651 * Set trip attribute
652 */
653 public function set_trip_attribute(WP_REST_Request $request)
654 {
655 try {
656 $tripId = (int) $request->get_param('trip_id');
657 $attributeId = (int) $request->get_param('attribute_id');
658 $value = $request->get_param('value');
659
660 $result = $this->attributeService->setTripAttribute($tripId, $attributeId, $value);
661
662 if ($result) {
663 return $this->success_response(['set' => true]);
664 }
665
666 return $this->error_response('Failed to set trip attribute', 500);
667
668 } catch (\Exception $e) {
669 return $this->error_response($e->getMessage(), 400);
670 }
671 }
672
673 /**
674 * Remove trip attribute
675 */
676 public function remove_trip_attribute(WP_REST_Request $request)
677 {
678 try {
679 $tripId = (int) $request->get_param('trip_id');
680 $attributeId = (int) $request->get_param('attribute_id');
681
682 $result = $this->attributeService->removeTripAttribute($tripId, $attributeId);
683
684 if ($result) {
685 return $this->success_response(['removed' => true]);
686 }
687
688 return $this->error_response('Failed to remove trip attribute', 500);
689
690 } catch (\Exception $e) {
691 return $this->error_response('Failed to remove trip attribute: ' . $e->getMessage(), 500);
692 }
693 }
694
695 /**
696 * Prepare item for database
697 */
698 private function prepare_item_for_database(WP_REST_Request $request): array
699 {
700 $data = [];
701
702 // Basic fields
703 if ($request->has_param('name')) {
704 $data['name'] = sanitize_text_field($request->get_param('name'));
705 }
706
707 if ($request->has_param('slug')) {
708 $data['slug'] = sanitize_title($request->get_param('slug'));
709 }
710
711 if ($request->has_param('description')) {
712 $data['description'] = wp_kses_post($request->get_param('description'));
713 }
714
715 // Handle icon field
716 if ($request->has_param('icon')) {
717 $icon = $request->get_param('icon');
718 if (is_array($icon)) {
719 $data['icon'] = function_exists('yatra_normalize_icon_picker_for_storage')
720 ? yatra_normalize_icon_picker_for_storage($icon)
721 : [
722 'type' => isset($icon['type']) && in_array($icon['type'], ['icon', 'image'], true)
723 ? $icon['type']
724 : 'icon',
725 'value' => isset($icon['value'])
726 ? sanitize_text_field((string) $icon['value'])
727 : '',
728 ];
729 } elseif (is_string($icon)) {
730 // Handle legacy string format
731 $data['icon'] = sanitize_text_field($icon);
732 }
733 }
734
735 if ($request->has_param('field_type')) {
736 $data['field_type'] = sanitize_text_field($request->get_param('field_type'));
737 }
738
739 if ($request->has_param('field_options')) {
740 $options = $request->get_param('field_options');
741 if (is_string($options)) {
742 $decoded = json_decode(trim($options), true);
743 $options = is_array($decoded) ? $decoded : [];
744 }
745 if (is_array($options)) {
746 $data['field_options'] = array_values(array_filter(array_map(static function ($row) {
747 if (!is_array($row)) {
748 return null;
749 }
750 $label = isset($row['label']) ? sanitize_text_field((string) $row['label']) : '';
751 $value = isset($row['value']) ? sanitize_text_field((string) $row['value']) : '';
752 if ($label === '' && $value === '') {
753 return null;
754 }
755 if ($value === '' && $label !== '') {
756 $value = sanitize_title($label);
757 }
758 return ['label' => $label, 'value' => $value];
759 }, $options)));
760 }
761 }
762
763 if ($request->has_param('default_value')) {
764 $data['default_value'] = sanitize_text_field($request->get_param('default_value'));
765 }
766
767 if ($request->has_param('placeholder')) {
768 $data['placeholder'] = sanitize_text_field($request->get_param('placeholder'));
769 }
770
771 if ($request->has_param('required')) {
772 $data['required'] = (bool) $request->get_param('required');
773 }
774
775 if ($request->has_param('validation_rules')) {
776 $rules = $request->get_param('validation_rules');
777 if (is_array($rules)) {
778 $data['validation_rules'] = $rules;
779 }
780 }
781
782 if ($request->has_param('display_order')) {
783 $data['display_order'] = (int) $request->get_param('display_order');
784 }
785
786 if ($request->has_param('show_on_frontend')) {
787 $data['show_on_frontend'] = (bool) $request->get_param('show_on_frontend');
788 }
789
790 if ($request->has_param('show_in_filters')) {
791 $data['show_in_filters'] = (bool) $request->get_param('show_in_filters');
792 }
793
794 if ($request->has_param('filter_type')) {
795 $data['filter_type'] = sanitize_text_field($request->get_param('filter_type'));
796 }
797
798 if ($request->has_param('searchable')) {
799 $data['searchable'] = (bool) $request->get_param('searchable');
800 }
801
802 if ($request->has_param('status')) {
803 $raw = $request->get_param('status');
804 $s = is_string($raw) ? strtolower(trim(sanitize_text_field($raw))) : '';
805 if (in_array($s, ['publish', 'draft', 'trash'], true)) {
806 $data['status'] = $s;
807 }
808 }
809
810 return $data;
811 }
812
813 /**
814 * Granular permission checks. Trip attributes are a trip-taxonomy
815 * concept — they classify trips for filtering / display — so the
816 * Team module's `yatra_manage_trip_taxonomies` cap is the right
817 * gate for write operations, and `yatra_view_trips` for reads.
818 * WP administrators pass every cap via the Team module's admin-
819 * fallback filter so no explicit `manage_options` check is needed.
820 */
821 public function get_permissions_check(): bool
822 {
823 return current_user_can('yatra_view_trips');
824 }
825
826 public function check_permission(?WP_REST_Request $request = null): bool
827 {
828 return current_user_can('yatra_manage_trip_taxonomies');
829 }
830
831 public function search_permissions_check(): bool
832 {
833 return current_user_can('yatra_view_trips');
834 }
835
836 public function update_permissions_check(): bool
837 {
838 return current_user_can('yatra_manage_trip_taxonomies');
839 }
840
841 /**
842 * Get item schema
843 */
844 public function get_item_schema(): array
845 {
846 return [
847 '$schema' => 'http://json-schema.org/draft-04/schema#',
848 'title' => 'attribute',
849 'type' => 'object',
850 'properties' => [
851 'id' => [
852 'description' => 'Unique identifier for the attribute.',
853 'type' => 'integer',
854 'readonly' => true,
855 ],
856 'name' => [
857 'description' => 'Attribute name.',
858 'type' => 'string',
859 'required' => true,
860 ],
861 'slug' => [
862 'description' => 'URL-friendly identifier.',
863 'type' => 'string',
864 ],
865 'description' => [
866 'description' => 'Attribute description.',
867 'type' => 'string',
868 ],
869 'field_type' => [
870 'description' => 'Form field type.',
871 'type' => 'string',
872 'enum' => ['text_field', 'number', 'email', 'url', 'textarea', 'select', 'radio', 'checkbox', 'date', 'time', 'color', 'file'],
873 'default' => 'text_field',
874 ],
875 'field_options' => [
876 'description' => 'Options for select/radio/checkbox fields.',
877 'type' => 'array',
878 ],
879 'default_value' => [
880 'description' => 'Default value.',
881 'type' => 'string',
882 ],
883 'placeholder' => [
884 'description' => 'Field placeholder text.',
885 'type' => 'string',
886 ],
887 'required' => [
888 'description' => 'Whether attribute is required.',
889 'type' => 'boolean',
890 'default' => false,
891 ],
892 'validation_rules' => [
893 'description' => 'Validation rules.',
894 'type' => 'array',
895 ],
896 'display_order' => [
897 'description' => 'Display order.',
898 'type' => 'integer',
899 'default' => 0,
900 ],
901 'show_on_frontend' => [
902 'description' => 'Show on trip pages.',
903 'type' => 'boolean',
904 'default' => true,
905 ],
906 'show_in_filters' => [
907 'description' => 'Show in trip listing filters.',
908 'type' => 'boolean',
909 'default' => false,
910 ],
911 'filter_type' => [
912 'description' => 'How to filter this attribute.',
913 'type' => 'string',
914 'enum' => ['exact', 'partial', 'range', 'dropdown'],
915 'default' => 'exact',
916 ],
917 'searchable' => [
918 'description' => 'Include in search index.',
919 'type' => 'boolean',
920 'default' => false,
921 ],
922 'status' => [
923 'description' => 'Attribute status.',
924 'type' => 'string',
925 'enum' => ['publish', 'draft', 'trash'],
926 'default' => 'publish',
927 ],
928 'created_at' => [
929 'description' => 'Creation date.',
930 'type' => 'string',
931 'format' => 'date-time',
932 'readonly' => true,
933 ],
934 'updated_at' => [
935 'description' => 'Last updated date.',
936 'type' => 'string',
937 'format' => 'date-time',
938 'readonly' => true,
939 ],
940 ],
941 ];
942 }
943
944 /**
945 * Get attribute statistics
946 * GET /attributes/stats
947 */
948 public function getStats(WP_REST_Request $request)
949 {
950 try {
951 $stats = $this->attributeService->getStatusCounts();
952 return $this->success_response($stats);
953 } catch (\Exception $e) {
954 return $this->error_response($e->getMessage(), 500);
955 }
956 }
957
958 /**
959 * Replace raw DB icon (serialized array or legacy string) with REST JSON (preserves Font Awesome provider).
960 *
961 * @param object $attribute Row from AttributeService::getById()
962 */
963 private function attachDecodedIconToAttribute(object $attribute): void
964 {
965 if (!empty($attribute->icon)) {
966 $icon_data = maybe_unserialize($attribute->icon);
967 if (is_array($icon_data)) {
968 if ($icon_data['type'] === 'image' && !empty($icon_data['value'])) {
969 $value = $icon_data['value'];
970 $image_url = '';
971
972 if (is_numeric($value)) {
973 $maybe_url = wp_get_attachment_image_url((int) $value, 'large');
974 if (!empty($maybe_url)) {
975 $image_url = $maybe_url;
976 }
977 } elseif (is_string($value) && filter_var($value, FILTER_VALIDATE_URL)) {
978 $image_url = $value;
979 }
980
981 $icon_data['value'] = $image_url;
982 }
983 $attribute->icon = $icon_data;
984 } else {
985 $attribute->icon = [
986 'type' => 'icon',
987 'value' => (string) $attribute->icon,
988 ];
989 }
990 } else {
991 $attribute->icon = null;
992 }
993 }
994 }
995