namespace = SOFTMINAL_CACHE_API_NAMESPACE; } public function init() { // The route definitions live in the app layer; the path is filterable so // the framework stays plugin-agnostic. $routesFile = apply_filters('softminal_cache_routes_file', SOFTMINAL_CACHE_DIR . 'app/Http/routes.php'); if (is_string($routesFile) && file_exists($routesFile)) { require_once $routesFile; } add_action('rest_api_init', [$this, 'registerRoutes']); add_action('init', [$this, 'handleCors'], 1); } /** * Origins that may call the API with credentials from a browser. * * No origins are allowed by default: cross-site access is an explicit, per-plugin * decision made through the `softminal_cache_cors_allowed_origins` filter. Never add localhost * or wildcard entries unconditionally — a credentialed allow-list entry for * localhost applies on every visitor's own machine. * * @return string[] */ public static function allowedOrigins(): array { return array_values(array_filter(array_map('strval', (array) apply_filters('softminal_cache_cors_allowed_origins', [])))); } public function handleCors() { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Origin header is validated against an explicit allowlist below; sanitization would break valid URLs $origin = isset($_SERVER['HTTP_ORIGIN']) ? sanitize_url(wp_unslash($_SERVER['HTTP_ORIGIN'])) : ''; $allowed = self::allowedOrigins(); if ($origin === '' || !in_array($origin, $allowed, true)) { return; } header('Access-Control-Allow-Origin: ' . $origin); header('Vary: Origin'); header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS'); header('Access-Control-Allow-Credentials: true'); header('Access-Control-Allow-Headers: Authorization, Content-Type, X-WP-Nonce, X-WP-Secret'); // Handle preflight and exit immediately if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'OPTIONS') { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- Comparison against a fixed string; no unsanitized use status_header(200); exit; } } public function registerRoutes() { if (empty($this->namespace)) { return; } try { Route::registerRoutes($this->namespace); // Diagnostic endpoint: off unless the plugin opts in. if (apply_filters('softminal_cache_register_test_route', false)) { register_rest_route($this->namespace, '/test', [ 'methods' => 'GET', 'callback' => function() { return [ 'status' => 'success', 'message' => 'API is working correctly', ]; }, 'permission_callback' => function() { return is_user_logged_in() && current_user_can('manage_options'); } ]); } } catch (\Throwable $e) { // A broken route definition must be visible, not silently dropped. do_action('softminal_cache_routes_error', $e); if (defined('WP_DEBUG') && WP_DEBUG) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- debug-only diagnostics error_log('[softminal_cache_routes] ' . $e->getMessage()); } } } }