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 / BaseController.php

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

432 lines 12.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\Exceptions\YatraException;
11 use Yatra\Exceptions\ValidationException;
12
13 /**
14 * Base REST API Controller
15 *
16 * Provides common functionality for all REST controllers including:
17 * - Standard CRUD route registration
18 * - Pagination arguments
19 * - Response helpers
20 * - Permission checking
21 *
22 * @package Yatra\Controllers
23 */
24 abstract class BaseController
25 {
26 /**
27 * API namespace
28 */
29 protected string $namespace = 'yatra/v1';
30
31 /**
32 * Resource base (e.g., 'trips', 'activities')
33 * Override in child classes
34 */
35 protected string $rest_base = '';
36
37 /**
38 * Register REST API routes
39 * Override in child classes or use registerCrudRoutes()
40 */
41 abstract public function register_routes(): void;
42
43 /**
44 * Register standard CRUD routes
45 *
46 * Registers:
47 * - GET /{base} - List items
48 * - POST /{base} - Create item
49 * - GET /{base}/{id} - Get single item
50 * - PUT /{base}/{id} - Update item
51 * - DELETE /{base}/{id} - Delete item
52 *
53 * @param array $additionalArgs Additional args for collection route
54 * @param array $options Options: ['list', 'create', 'read', 'update', 'delete']
55 */
56 protected function registerCrudRoutes(array $additionalArgs = [], array $options = []): void
57 {
58 $base = $this->rest_base;
59
60 if (empty($base)) {
61 return;
62 }
63
64 // Default options - all CRUD operations enabled
65 $defaults = [
66 'list' => true,
67 'create' => true,
68 'read' => true,
69 'update' => true,
70 'delete' => true,
71 ];
72 $options = array_merge($defaults, $options);
73
74 // Collection routes
75 $collectionRoutes = [];
76
77 if ($options['list']) {
78 $collectionRoutes[] = [
79 'methods' => \WP_REST_Server::READABLE,
80 'callback' => [$this, 'get_items'],
81 'permission_callback' => [$this, 'check_permission'],
82 'args' => array_merge($this->getPaginationArgs(), $additionalArgs),
83 ];
84 }
85
86 if ($options['create']) {
87 $collectionRoutes[] = [
88 'methods' => \WP_REST_Server::CREATABLE,
89 'callback' => [$this, 'create_item'],
90 'permission_callback' => [$this, 'check_permission'],
91 ];
92 }
93
94 if (!empty($collectionRoutes)) {
95 register_rest_route($this->namespace, '/' . $base, $collectionRoutes);
96 }
97
98 // Single item routes
99 $itemRoutes = [];
100
101 if ($options['read']) {
102 $itemRoutes[] = [
103 'methods' => \WP_REST_Server::READABLE,
104 'callback' => [$this, 'get_item'],
105 'permission_callback' => [$this, 'check_permission'],
106 ];
107 }
108
109 if ($options['update']) {
110 $itemRoutes[] = [
111 'methods' => \WP_REST_Server::EDITABLE,
112 'callback' => [$this, 'update_item'],
113 'permission_callback' => [$this, 'check_permission'],
114 ];
115 }
116
117 if ($options['delete']) {
118 $itemRoutes[] = [
119 'methods' => \WP_REST_Server::DELETABLE,
120 'callback' => [$this, 'delete_item'],
121 'permission_callback' => [$this, 'check_permission'],
122 ];
123 }
124
125 if (!empty($itemRoutes)) {
126 register_rest_route($this->namespace, '/' . $base . '/(?P<id>[\d]+)', $itemRoutes);
127 }
128 }
129
130 /**
131 * Register a custom route
132 *
133 * @param string $route Route path (appended to namespace)
134 * @param array $args Route arguments
135 */
136 protected function registerRoute(string $route, array $args): void
137 {
138 register_rest_route($this->namespace, $route, $args);
139 }
140
141 /**
142 * Get standard pagination arguments
143 */
144 protected function getPaginationArgs(): array
145 {
146 return [
147 'page' => [
148 'default' => 1,
149 'sanitize_callback' => 'absint',
150 'validate_callback' => function($value) {
151 return is_numeric($value) && $value > 0;
152 },
153 ],
154 'per_page' => [
155 'default' => 10,
156 'sanitize_callback' => 'absint',
157 'validate_callback' => function($value) {
158 // Allow up to 1000 for bulk operations like dropdowns
159 return is_numeric($value) && $value > 0 && $value <= 1000;
160 },
161 ],
162 'orderby' => [
163 'default' => 'id',
164 'sanitize_callback' => 'sanitize_text_field',
165 ],
166 'order' => [
167 'default' => 'DESC',
168 'sanitize_callback' => function($value) {
169 return strtoupper($value) === 'ASC' ? 'ASC' : 'DESC';
170 },
171 ],
172 'search' => [
173 'default' => '',
174 'sanitize_callback' => 'sanitize_text_field',
175 ],
176 ];
177 }
178
179 /**
180 * Get status filter argument
181 */
182 protected function getStatusArg(string $default = 'all'): array
183 {
184 return [
185 'status' => [
186 'default' => $default,
187 'sanitize_callback' => 'sanitize_text_field',
188 ],
189 ];
190 }
191
192 /**
193 * Extract pagination params from request
194 */
195 protected function getPaginationParams(WP_REST_Request $request): array
196 {
197 return [
198 'page' => (int) $request->get_param('page') ?: 1,
199 'per_page' => (int) $request->get_param('per_page') ?: 10,
200 'orderby' => $request->get_param('orderby') ?: 'id',
201 'order' => strtoupper($request->get_param('order') ?: 'DESC'),
202 'search' => $request->get_param('search') ?: '',
203 ];
204 }
205
206 /**
207 * Get items (GET /items)
208 */
209 public function get_items(WP_REST_Request $request)
210 {
211 return new WP_Error('not_implemented', __('Method not implemented', 'yatra'), ['status' => 501]);
212 }
213
214 /**
215 * Get single item (GET /items/{id})
216 */
217 public function get_item(WP_REST_Request $request)
218 {
219 return new WP_Error('not_implemented', __('Method not implemented', 'yatra'), ['status' => 501]);
220 }
221
222 /**
223 * Create item (POST /items)
224 */
225 public function create_item(WP_REST_Request $request)
226 {
227 return new WP_Error('not_implemented', __('Method not implemented', 'yatra'), ['status' => 501]);
228 }
229
230 /**
231 * Update item (PUT /items/{id})
232 */
233 public function update_item(WP_REST_Request $request)
234 {
235 return new WP_Error('not_implemented', __('Method not implemented', 'yatra'), ['status' => 501]);
236 }
237
238 /**
239 * Delete item (DELETE /items/{id})
240 */
241 public function delete_item(WP_REST_Request $request)
242 {
243 return new WP_Error('not_implemented', __('Method not implemented', 'yatra'), ['status' => 501]);
244 }
245
246 /**
247 * Check permissions
248 * Override in child classes for specific capability checks
249 */
250 public function check_permission(?WP_REST_Request $request = null): bool
251 {
252 return current_user_can('manage_options');
253 }
254
255 /**
256 * Check if user can view (read-only)
257 */
258 public function check_read_permission(?WP_REST_Request $request = null): bool
259 {
260 return current_user_can('manage_options');
261 }
262
263 /**
264 * Public access (no auth required)
265 */
266 public function public_access(?WP_REST_Request $request = null): bool
267 {
268 return true;
269 }
270
271 /**
272 * Public permission callback that bypasses WordPress cookie validation
273 *
274 * This is needed for endpoints that should work without authentication,
275 * especially for guest users. Using __return_true alone causes WordPress
276 * to validate cookies when they're present, leading to "Cookie check failed" errors.
277 *
278 * This method explicitly tells WordPress to skip cookie validation.
279 */
280 public function public_permission_callback(?WP_REST_Request $request = null): bool
281 {
282 // Remove cookie validation requirement for this endpoint
283 // This allows guest users to access the endpoint without nonce validation
284 remove_filter('rest_authentication_errors', 'rest_cookie_check_errors', 100);
285 return true;
286 }
287
288 /**
289 * Prepare item for response
290 */
291 protected function prepare_item_for_response($item, WP_REST_Request $request): array
292 {
293 return (array) $item;
294 }
295
296 /**
297 * Prepare collection for response
298 */
299 protected function prepare_collection_for_response(array $items, WP_REST_Request $request): array
300 {
301 return array_map(fn($item) => $this->prepare_item_for_response($item, $request), $items);
302 }
303
304 /**
305 * Send success response
306 */
307 protected function success_response($data, int $status = 200): WP_REST_Response
308 {
309 return new WP_REST_Response($data, $status);
310 }
311
312 /**
313 * Send paginated response
314 */
315 protected function paginated_response(array $items, int $total, int $page, int $perPage): WP_REST_Response
316 {
317 $response = new WP_REST_Response([
318 'data' => $items,
319 'total' => $total,
320 'pages' => (int) ceil($total / $perPage),
321 'page' => $page,
322 'per_page' => $perPage,
323 ], 200);
324
325 // Add pagination headers
326 $response->header('X-WP-Total', $total);
327 $response->header('X-WP-TotalPages', (int) ceil($total / $perPage));
328
329 return $response;
330 }
331
332 /**
333 * Send error response
334 */
335 protected function error_response(string $message, int $status = 400, array $data = []): WP_Error
336 {
337 return new WP_Error('yatra_error', $message, array_merge(['status' => $status], $data));
338 }
339
340 /**
341 * Handle Yatra exceptions and convert to appropriate WP_Error
342 */
343 protected function handle_exception(\Exception $e): WP_Error
344 {
345 if ($e instanceof ValidationException) {
346 return new WP_Error(
347 $e->getErrorCode(),
348 $e->getMessage(),
349 [
350 'status' => $e->getCode(),
351 'validation_errors' => $e->getErrors(),
352 'context' => $e->getContext()
353 ]
354 );
355 }
356
357 if ($e instanceof YatraException) {
358 return new WP_Error(
359 $e->getErrorCode(),
360 $e->getMessage(),
361 [
362 'status' => $e->getCode(),
363 'context' => $e->getContext()
364 ]
365 );
366 }
367
368 // Generic exception handling
369 $status = method_exists($e, 'getCode') && $e->getCode() > 0 ? $e->getCode() : 500;
370 return new WP_Error('server_error', $e->getMessage(), ['status' => $status]);
371 }
372
373 /**
374 * Send not found response
375 */
376 protected function not_found(string $message = ''): WP_Error
377 {
378 return $this->error_response($message ?: __('Resource not found', 'yatra'), 404);
379 }
380
381 /**
382 * Send validation error response
383 */
384 protected function validation_error(string $message, array $errors = []): WP_Error
385 {
386 return $this->error_response($message, 422, ['errors' => $errors]);
387 }
388
389 /**
390 * Convert attachment ID to URL in icon field
391 *
392 * @return mixed
393 */
394 protected function convert_icon_attachment_id_to_url($icon)
395 {
396 if (empty($icon)) {
397 return $icon;
398 }
399
400 if (is_string($icon)) {
401 return $icon;
402 }
403
404 if (is_array($icon) && isset($icon['type'], $icon['value'])) {
405 if ($icon['type'] === 'image' && is_numeric($icon['value'])) {
406 $url = wp_get_attachment_image_url((int) $icon['value'], 'full');
407 if ($url) {
408 $icon['value'] = $url;
409 }
410 }
411 }
412
413 return $icon;
414 }
415
416 /**
417 * Get ID from request
418 */
419 protected function getId(WP_REST_Request $request): int
420 {
421 return (int) $request->get_param('id');
422 }
423
424 /**
425 * Get JSON body from request
426 */
427 protected function getBody(WP_REST_Request $request): array
428 {
429 return $request->get_json_params() ?: [];
430 }
431 }
432