PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.9
Yatra – Travel Booking & Tour Operator Software v3.0.2.9
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 2.0.10 All 83 releases
yatra / app / Controllers / AttributeController.php

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

994 lines 36.9 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 return $this->success_response($attribute, 201);
366 }
367
368 return $this->error_response('Failed to create attribute', 500);
369
370 } catch (\Exception $e) {
371 return $this->error_response($e->getMessage(), 400);
372 }
373 }
374
375 /**
376 * Get single attribute
377 */
378 public function get_item(WP_REST_Request $request)
379 {
380 try {
381 $id = (int) $request->get_param('id');
382
383 $attribute = $this->attributeService->getById($id);
384
385 if (!$attribute) {
386 return $this->error_response('Attribute not found', 404);
387 }
388
389 // Parse metadata JSON and extract attribute properties
390 if (!empty($attribute->metadata)) {
391 $metadata = json_decode($attribute->metadata, true);
392 if (is_array($metadata)) {
393 // Add metadata fields as properties to the attribute object
394 $attribute->field_type = $metadata['field_type'] ?? null;
395 $attribute->required = $metadata['required'] ?? false;
396 $attribute->show_on_frontend = $metadata['show_on_frontend'] ?? false;
397 $attribute->show_in_filters = $metadata['show_in_filters'] ?? false;
398 $attribute->filter_type = $metadata['filter_type'] ?? null;
399 $attribute->searchable = $metadata['searchable'] ?? false;
400 $attribute->display_order = $metadata['display_order'] ?? 0;
401 $attribute->default_value = $metadata['default_value'] ?? null;
402 $attribute->placeholder = $metadata['placeholder'] ?? null;
403 $attribute->field_options = $metadata['field_options'] ?? null;
404 $attribute->validation_rules = $metadata['validation_rules'] ?? null;
405 }
406 }
407
408 // Process icon field - unserialize if it's serialized
409 if (!empty($attribute->icon)) {
410 $icon_data = maybe_unserialize($attribute->icon);
411 if (is_array($icon_data)) {
412 // Resolve image URLs for image type icons
413 if ($icon_data['type'] === 'image' && !empty($icon_data['value'])) {
414 $value = $icon_data['value'];
415 $image_url = '';
416
417 if (is_numeric($value)) {
418 $maybe_url = wp_get_attachment_image_url((int) $value, 'large');
419 if (!empty($maybe_url)) {
420 $image_url = $maybe_url;
421 }
422 } elseif (is_string($value) && filter_var($value, FILTER_VALIDATE_URL)) {
423 $image_url = $value;
424 }
425
426 $icon_data['value'] = $image_url;
427 }
428 $attribute->icon = $icon_data;
429 } else {
430 // Handle legacy string format
431 $attribute->icon = [
432 'type' => 'icon',
433 'value' => $attribute->icon
434 ];
435 }
436 } else {
437 $attribute->icon = null;
438 }
439
440 return $this->success_response($attribute);
441
442 } catch (\Exception $e) {
443 return $this->error_response('Failed to retrieve attribute: ' . $e->getMessage(), 500);
444 }
445 }
446
447 /**
448 * Update attribute
449 */
450 public function update_item(WP_REST_Request $request)
451 {
452 try {
453 $id = (int) $request->get_param('id');
454 $data = $this->prepare_item_for_database($request);
455
456 $result = $this->attributeService->updateAttribute($id, $data);
457
458 if ($result) {
459 $attribute = $this->attributeService->getById($id);
460
461 // Parse metadata JSON and extract attribute properties
462 if (!empty($attribute->metadata)) {
463 $metadata = json_decode($attribute->metadata, true);
464 if (is_array($metadata)) {
465 // Add metadata fields as properties to the attribute object
466 $attribute->field_type = $metadata['field_type'] ?? null;
467 $attribute->required = $metadata['required'] ?? false;
468 $attribute->show_on_frontend = $metadata['show_on_frontend'] ?? false;
469 $attribute->show_in_filters = $metadata['show_in_filters'] ?? false;
470 $attribute->filter_type = $metadata['filter_type'] ?? null;
471 $attribute->searchable = $metadata['searchable'] ?? false;
472 $attribute->display_order = $metadata['display_order'] ?? 0;
473 $attribute->default_value = $metadata['default_value'] ?? null;
474 $attribute->placeholder = $metadata['placeholder'] ?? null;
475 $attribute->field_options = $metadata['field_options'] ?? null;
476 $attribute->validation_rules = $metadata['validation_rules'] ?? null;
477 }
478 }
479
480 return $this->success_response($attribute);
481 }
482
483 return $this->error_response('Failed to update attribute', 500);
484
485 } catch (\Exception $e) {
486 return $this->error_response($e->getMessage(), 400);
487 }
488 }
489
490 /**
491 * Delete attribute
492 */
493 public function delete_item(WP_REST_Request $request)
494 {
495 try {
496 $id = (int) $request->get_param('id');
497
498 $result = $this->attributeService->deleteAttribute($id);
499
500 if ($result) {
501 return $this->success_response(['deleted' => true]);
502 }
503
504 return $this->error_response('Failed to delete attribute', 500);
505
506 } catch (\Exception $e) {
507 return $this->error_response($e->getMessage(), 400);
508 }
509 }
510
511 /**
512 * Handle bulk operations
513 */
514 public function bulkAction(WP_REST_Request $request): WP_REST_Response
515 {
516 try {
517 $action = sanitize_text_field((string) $request->get_param('action'));
518 $ids = $request->get_param('ids');
519
520 if (empty($action)) {
521 return $this->error_response(__('Action is required', 'yatra'), 400);
522 }
523
524 if (empty($ids) || !is_array($ids)) {
525 return $this->error_response(__('No attributes selected', 'yatra'), 400);
526 }
527
528 switch ($action) {
529 case 'trash':
530 $result = $this->attributeService->bulkUpdateStatus($ids, 'trash');
531 break;
532 case 'publish':
533 case 'restore':
534 $result = $this->attributeService->bulkUpdateStatus($ids, 'publish');
535 break;
536 case 'draft':
537 $result = $this->attributeService->bulkUpdateStatus($ids, 'draft');
538 break;
539 case 'delete':
540 $result = $this->attributeService->bulkDelete($ids);
541 break;
542 default:
543 return $this->error_response(__('Invalid action', 'yatra'), 400);
544 }
545
546 return $this->success_response($result);
547
548 } catch (\InvalidArgumentException $e) {
549 return $this->error_response($e->getMessage(), 400);
550 } catch (\Exception $e) {
551 return $this->error_response($e->getMessage(), 500);
552 }
553 }
554
555 /**
556 * Get attribute values
557 */
558 public function get_attribute_values(WP_REST_Request $request)
559 {
560 try {
561 $id = (int) $request->get_param('id');
562
563 $values = $this->attributeService->getAttributeValues($id);
564
565 return $this->success_response($values);
566
567 } catch (\Exception $e) {
568 return $this->error_response('Failed to retrieve attribute values: ' . $e->getMessage(), 500);
569 }
570 }
571
572 /**
573 * Update display orders
574 */
575 public function update_orders(WP_REST_Request $request)
576 {
577 try {
578 $orders = $request->get_param('orders');
579
580 $result = $this->attributeService->updateDisplayOrders($orders);
581
582 if ($result) {
583 return $this->success_response(['updated' => true]);
584 }
585
586 return $this->error_response('Failed to update display orders', 500);
587
588 } catch (\Exception $e) {
589 return $this->error_response($e->getMessage(), 400);
590 }
591 }
592
593 /**
594 * Update display orders
595 */
596 public function update_display_orders(WP_REST_Request $request)
597 {
598 try {
599 $orders = $request->get_param('orders');
600
601 $result = $this->attributeService->updateDisplayOrders($orders);
602
603 if ($result) {
604 return $this->success_response(['message' => 'Display orders updated successfully']);
605 }
606
607 return $this->error_response('Failed to update display orders', 500);
608
609 } catch (\Exception $e) {
610 return $this->error_response($e->getMessage(), 400);
611 }
612 }
613
614 /**
615 * Check if slug exists and suggest unique slug if needed
616 */
617 public function check_slug(WP_REST_Request $request)
618 {
619 try {
620 $slug = sanitize_title($request->get_param('slug'));
621 $excludeId = $request->get_param('exclude_id');
622
623 if (empty($slug)) {
624 return $this->error_response('Slug is required', 400);
625 }
626
627 // Check if slug exists (excluding current attribute if editing)
628 $exists = $this->attributeService->slugExists($slug, $excludeId);
629
630 if (!$exists) {
631 return $this->success_response([
632 'exists' => false,
633 'suggested_slug' => $slug
634 ]);
635 }
636
637 // Generate unique slug
638 $originalSlug = $slug;
639 $counter = 1;
640
641 while ($this->attributeService->slugExists($slug, $excludeId)) {
642 $slug = $originalSlug . '-' . $counter;
643 $counter++;
644
645 // Prevent infinite loop
646 if ($counter > 100) {
647 break;
648 }
649 }
650
651 return $this->success_response([
652 'exists' => true,
653 'suggested_slug' => $slug
654 ]);
655
656 } catch (\Exception $e) {
657 return $this->error_response($e->getMessage(), 400);
658 }
659 }
660
661 /**
662 * Get trip attributes
663 */
664 public function get_trip_attributes(WP_REST_Request $request): WP_REST_Response
665 {
666 try {
667 $tripId = (int) $request->get_param('trip_id');
668
669 $attributes = $this->attributeService->getTripAttributes($tripId);
670
671 return $this->success_response($attributes);
672
673 } catch (\Exception $e) {
674 return $this->error_response('Failed to retrieve trip attributes: ' . $e->getMessage(), 500);
675 }
676 }
677
678 /**
679 * Set trip attribute
680 */
681 public function set_trip_attribute(WP_REST_Request $request)
682 {
683 try {
684 $tripId = (int) $request->get_param('trip_id');
685 $attributeId = (int) $request->get_param('attribute_id');
686 $value = $request->get_param('value');
687
688 $result = $this->attributeService->setTripAttribute($tripId, $attributeId, $value);
689
690 if ($result) {
691 return $this->success_response(['set' => true]);
692 }
693
694 return $this->error_response('Failed to set trip attribute', 500);
695
696 } catch (\Exception $e) {
697 return $this->error_response($e->getMessage(), 400);
698 }
699 }
700
701 /**
702 * Remove trip attribute
703 */
704 public function remove_trip_attribute(WP_REST_Request $request)
705 {
706 try {
707 $tripId = (int) $request->get_param('trip_id');
708 $attributeId = (int) $request->get_param('attribute_id');
709
710 $result = $this->attributeService->removeTripAttribute($tripId, $attributeId);
711
712 if ($result) {
713 return $this->success_response(['removed' => true]);
714 }
715
716 return $this->error_response('Failed to remove trip attribute', 500);
717
718 } catch (\Exception $e) {
719 return $this->error_response('Failed to remove trip attribute: ' . $e->getMessage(), 500);
720 }
721 }
722
723 /**
724 * Prepare item for database
725 */
726 private function prepare_item_for_database(WP_REST_Request $request): array
727 {
728 $data = [];
729
730 // Basic fields
731 if ($request->has_param('name')) {
732 $data['name'] = sanitize_text_field($request->get_param('name'));
733 }
734
735 if ($request->has_param('slug')) {
736 $data['slug'] = sanitize_title($request->get_param('slug'));
737 }
738
739 if ($request->has_param('description')) {
740 $data['description'] = wp_kses_post($request->get_param('description'));
741 }
742
743 // Handle icon field
744 if ($request->has_param('icon')) {
745 $icon = $request->get_param('icon');
746 if (is_array($icon)) {
747 // Sanitize icon array
748 $data['icon'] = [
749 'type' => isset($icon['type']) && in_array($icon['type'], ['icon', 'image'], true)
750 ? $icon['type']
751 : 'icon',
752 'value' => isset($icon['value'])
753 ? sanitize_text_field($icon['value'])
754 : '',
755 ];
756 } elseif (is_string($icon)) {
757 // Handle legacy string format
758 $data['icon'] = sanitize_text_field($icon);
759 }
760 }
761
762 if ($request->has_param('field_type')) {
763 $data['field_type'] = sanitize_text_field($request->get_param('field_type'));
764 }
765
766 if ($request->has_param('field_options')) {
767 $options = $request->get_param('field_options');
768 if (is_string($options)) {
769 $decoded = json_decode(trim($options), true);
770 $options = is_array($decoded) ? $decoded : [];
771 }
772 if (is_array($options)) {
773 $data['field_options'] = array_values(array_filter(array_map(static function ($row) {
774 if (!is_array($row)) {
775 return null;
776 }
777 $label = isset($row['label']) ? sanitize_text_field((string) $row['label']) : '';
778 $value = isset($row['value']) ? sanitize_text_field((string) $row['value']) : '';
779 if ($label === '' && $value === '') {
780 return null;
781 }
782 if ($value === '' && $label !== '') {
783 $value = sanitize_title($label);
784 }
785 return ['label' => $label, 'value' => $value];
786 }, $options)));
787 }
788 }
789
790 if ($request->has_param('default_value')) {
791 $data['default_value'] = sanitize_text_field($request->get_param('default_value'));
792 }
793
794 if ($request->has_param('placeholder')) {
795 $data['placeholder'] = sanitize_text_field($request->get_param('placeholder'));
796 }
797
798 if ($request->has_param('required')) {
799 $data['required'] = (bool) $request->get_param('required');
800 }
801
802 if ($request->has_param('validation_rules')) {
803 $rules = $request->get_param('validation_rules');
804 if (is_array($rules)) {
805 $data['validation_rules'] = $rules;
806 }
807 }
808
809 if ($request->has_param('display_order')) {
810 $data['display_order'] = (int) $request->get_param('display_order');
811 }
812
813 if ($request->has_param('show_on_frontend')) {
814 $data['show_on_frontend'] = (bool) $request->get_param('show_on_frontend');
815 }
816
817 if ($request->has_param('show_in_filters')) {
818 $data['show_in_filters'] = (bool) $request->get_param('show_in_filters');
819 }
820
821 if ($request->has_param('filter_type')) {
822 $data['filter_type'] = sanitize_text_field($request->get_param('filter_type'));
823 }
824
825 if ($request->has_param('searchable')) {
826 $data['searchable'] = (bool) $request->get_param('searchable');
827 }
828
829 if ($request->has_param('status')) {
830 $raw = $request->get_param('status');
831 $s = is_string($raw) ? strtolower(trim(sanitize_text_field($raw))) : '';
832 if (in_array($s, ['publish', 'draft', 'trash'], true)) {
833 $data['status'] = $s;
834 }
835 }
836
837 return $data;
838 }
839
840 /**
841 * Check permissions for read operations
842 */
843 public function get_permissions_check(): bool
844 {
845 return current_user_can('manage_options');
846 }
847
848 /**
849 * Check permissions for create/update/delete operations
850 */
851 public function check_permission(?WP_REST_Request $request = null): bool
852 {
853 $hasPermission = current_user_can('manage_options');
854
855 if (defined('WP_DEBUG') && WP_DEBUG) {
856 }
857
858 return $hasPermission;
859 }
860
861 /**
862 * Check permissions for search operations
863 */
864 public function search_permissions_check(): bool
865 {
866 return current_user_can('manage_options');
867 }
868
869 /**
870 * Check permissions for update operations
871 */
872 public function update_permissions_check(): bool
873 {
874 return current_user_can('manage_options');
875 }
876
877 /**
878 * Get item schema
879 */
880 public function get_item_schema(): array
881 {
882 return [
883 '$schema' => 'http://json-schema.org/draft-04/schema#',
884 'title' => 'attribute',
885 'type' => 'object',
886 'properties' => [
887 'id' => [
888 'description' => 'Unique identifier for the attribute.',
889 'type' => 'integer',
890 'readonly' => true,
891 ],
892 'name' => [
893 'description' => 'Attribute name.',
894 'type' => 'string',
895 'required' => true,
896 ],
897 'slug' => [
898 'description' => 'URL-friendly identifier.',
899 'type' => 'string',
900 ],
901 'description' => [
902 'description' => 'Attribute description.',
903 'type' => 'string',
904 ],
905 'field_type' => [
906 'description' => 'Form field type.',
907 'type' => 'string',
908 'enum' => ['text_field', 'number', 'email', 'url', 'textarea', 'select', 'radio', 'checkbox', 'date', 'time', 'color', 'file'],
909 'default' => 'text_field',
910 ],
911 'field_options' => [
912 'description' => 'Options for select/radio/checkbox fields.',
913 'type' => 'array',
914 ],
915 'default_value' => [
916 'description' => 'Default value.',
917 'type' => 'string',
918 ],
919 'placeholder' => [
920 'description' => 'Field placeholder text.',
921 'type' => 'string',
922 ],
923 'required' => [
924 'description' => 'Whether attribute is required.',
925 'type' => 'boolean',
926 'default' => false,
927 ],
928 'validation_rules' => [
929 'description' => 'Validation rules.',
930 'type' => 'array',
931 ],
932 'display_order' => [
933 'description' => 'Display order.',
934 'type' => 'integer',
935 'default' => 0,
936 ],
937 'show_on_frontend' => [
938 'description' => 'Show on trip pages.',
939 'type' => 'boolean',
940 'default' => true,
941 ],
942 'show_in_filters' => [
943 'description' => 'Show in trip listing filters.',
944 'type' => 'boolean',
945 'default' => false,
946 ],
947 'filter_type' => [
948 'description' => 'How to filter this attribute.',
949 'type' => 'string',
950 'enum' => ['exact', 'partial', 'range', 'dropdown'],
951 'default' => 'exact',
952 ],
953 'searchable' => [
954 'description' => 'Include in search index.',
955 'type' => 'boolean',
956 'default' => false,
957 ],
958 'status' => [
959 'description' => 'Attribute status.',
960 'type' => 'string',
961 'enum' => ['publish', 'draft', 'trash'],
962 'default' => 'publish',
963 ],
964 'created_at' => [
965 'description' => 'Creation date.',
966 'type' => 'string',
967 'format' => 'date-time',
968 'readonly' => true,
969 ],
970 'updated_at' => [
971 'description' => 'Last updated date.',
972 'type' => 'string',
973 'format' => 'date-time',
974 'readonly' => true,
975 ],
976 ],
977 ];
978 }
979
980 /**
981 * Get attribute statistics
982 * GET /attributes/stats
983 */
984 public function getStats(WP_REST_Request $request)
985 {
986 try {
987 $stats = $this->attributeService->getStatusCounts();
988 return $this->success_response($stats);
989 } catch (\Exception $e) {
990 return $this->error_response($e->getMessage(), 500);
991 }
992 }
993 }
994