\WP_REST_Server::READABLE, 'callback' => [$this, 'get_modules'], 'permission_callback' => [$this, 'check_permission'], ], ]); register_rest_route($namespace, '/modules/(?P[a-z0-9_\-]+)/toggle', [ [ 'methods' => \WP_REST_Server::CREATABLE, 'callback' => [$this, 'toggle_module'], 'permission_callback' => [$this, 'check_permission'], 'args' => [ 'slug' => [ 'required' => true, 'sanitize_callback' => 'sanitize_key', ], 'enabled' => [ 'required' => true, 'validate_callback' => function ($value) { return is_bool($value) || $value === 'true' || $value === 'false' || $value === 1 || $value === 0; }, ], ], ], ]); register_rest_route($namespace, '/modules/bulk-toggle', [ [ 'methods' => \WP_REST_Server::CREATABLE, 'callback' => [$this, 'bulk_toggle'], 'permission_callback' => [$this, 'check_permission'], ], ]); } /** * Module toggle — critical-sensitivity cap. Enabling or disabling * a Pro module flips significant feature surfaces (Webhooks, * Channel Manager, Team & Access, etc.) on or off, so only the * Owner role holds `yatra_manage_modules` by default. The * previous implementation gated this on `yatra_edit_trips` which * meant any trip editor could toggle modules — that was the wrong * cap entirely. WP admins pass via the Team module's admin- * fallback filter. */ public function check_permission(?WP_REST_Request $request = null): bool { if (!is_user_logged_in()) { return false; } return current_user_can('yatra_manage_modules'); } public function get_modules(): WP_REST_Response { return $this->success_response([ 'data' => ModuleManager::getModules(), ]); } public function toggle_module(WP_REST_Request $request) { $slug = sanitize_key($request->get_param('slug')); $enabled = filter_var($request->get_param('enabled'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); if ($enabled === null) { return $this->error_response(__('Enabled flag is required.', 'yatra'), 400); } $modules = ModuleManager::getModules(); $module_exists = false; $target_module = null; foreach ($modules as $module) { if ($module['slug'] === $slug) { $module_exists = true; $target_module = $module; break; } } if (!$module_exists || !$target_module) { return $this->error_response(__('Module not found.', 'yatra'), 404); } // Check if trying to enable a premium module without Pro if ($enabled && !empty($target_module['is_premium']) && !$target_module['is_available']) { return $this->error_response( sprintf( /* translators: %s: module name. */ __('%s is a premium module. Yatra Pro is required to enable this module.', 'yatra'), $target_module['name'] ), 403 ); } // Check if trying to enable a module that requires Pro but Pro is not active if ($enabled && !empty($target_module['requires_pro'])) { $pro_active = apply_filters('yatra_is_pro_active', false); if (!$pro_active) { return $this->error_response( sprintf( /* translators: %s: module name. */ __('%s requires Yatra Pro. Please install and activate Yatra Pro to enable this module.', 'yatra'), $target_module['name'] ), 403 ); } } // Agency-tier gate: even with Pro active, the white-label/agency-only // modules need an Agency Yearly or Lifetime license. if ($enabled && !empty($target_module['requires_agency'])) { if (!apply_filters('yatra_is_agency_active', false)) { return $this->error_response( sprintf( /* translators: %s: module name. */ __('%s is available on the Yatra Pro Scale plan only. Upgrade your license to enable it.', 'yatra'), $target_module['name'] ), 403 ); } } // Growth-or-Agency gate (e.g. AI Assistant). if ($enabled && !empty($target_module['requires_growth_or_agency'])) { if (!apply_filters('yatra_is_ai_eligible', false)) { return $this->error_response( sprintf( /* translators: %s: module name. */ __('%s requires a Growth or Scale license. Upgrade your plan to enable it.', 'yatra'), $target_module['name'] ), 403 ); } } $updated = ModuleManager::setModuleStatus($slug, (bool) $enabled); return $this->success_response([ 'data' => $updated, ]); } public function bulk_toggle(WP_REST_Request $request) { $items = $request->get_param('items'); if (!is_array($items) || empty($items)) { return $this->error_response(__('No module changes supplied.', 'yatra'), 400); } $modules = ModuleManager::getModules(); $module_map = []; foreach ($modules as $module) { $module_map[$module['slug']] = $module; } $sanitized = []; $blocked_modules = []; foreach ($items as $item) { if (empty($item['slug'])) { continue; } $slug = sanitize_key($item['slug']); $enabled = filter_var($item['enabled'] ?? false, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); if ($enabled === null) { continue; } // Check if module exists if (!isset($module_map[$slug])) { continue; } $target_module = $module_map[$slug]; // Check if trying to enable a premium module without Pro if ($enabled && !empty($target_module['is_premium']) && !$target_module['is_available']) { $blocked_modules[] = $target_module['name']; continue; } // Check if trying to enable a module that requires Pro but Pro is not active if ($enabled && !empty($target_module['requires_pro'])) { $pro_active = apply_filters('yatra_is_pro_active', false); if (!$pro_active) { $blocked_modules[] = $target_module['name']; continue; } } if ($enabled && !empty($target_module['requires_agency'])) { if (!apply_filters('yatra_is_agency_active', false)) { $blocked_modules[] = $target_module['name']; continue; } } if ($enabled && !empty($target_module['requires_growth_or_agency'])) { if (!apply_filters('yatra_is_ai_eligible', false)) { $blocked_modules[] = $target_module['name']; continue; } } $sanitized[] = [ 'slug' => $slug, 'enabled' => (bool) $enabled, ]; } if (empty($sanitized) && !empty($blocked_modules)) { return $this->error_response( sprintf( /* translators: %s: comma-separated list of module names. */ __('The following modules require Yatra Pro: %s', 'yatra'), implode(', ', $blocked_modules) ), 403 ); } if (!empty($blocked_modules)) { // Partial success - some modules processed, some blocked $updated = ModuleManager::setMultipleStatuses($sanitized); return $this->success_response([ 'data' => $updated, 'message' => sprintf( /* translators: %s: comma-separated list of module names. */ __('Some modules were skipped because they require Yatra Pro: %s', 'yatra'), implode(', ', $blocked_modules) ), ]); } if (empty($sanitized)) { return $this->error_response(__('No valid module changes supplied.', 'yatra'), 400); } $updated = ModuleManager::setMultipleStatuses($sanitized); return $this->success_response([ 'data' => $updated, ]); } }