PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.0.2
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.0.2
1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 All 29 releases
xspeed / includes / class-rest-manager.php

class-rest-manager.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.0.2, at includes/class-rest-manager.php

121 lines 3.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Rest_Manager — registers a module's REST routes under
4 * `/xspeed/v1/<module-slug>/...` and wraps every callback with:
5 * - A final capability check (defaults to `manage_options`).
6 * - A tier gate that returns 404 if the module is unavailable on this
7 * install (Pro module without active Pro plugin → 404, not 403, so
8 * the route looks like it doesn't exist).
9 * - A conflict gate (refuse strategy → 409 with conflict details).
10 *
11 * Modules declare routes via Module::rest_routes(). They don't repeat the
12 * cap check / tier gate / conflict gate — Rest_Manager always enforces.
13 *
14 * The pre-existing /xspeed/v1/status, /settings, /cache/purge,
15 * /cache/toggle, /onboarding/* routes (registered by Rest_Api and
16 * Onboarding) continue to work alongside per-module routes. They'll move
17 * onto the module pattern when those features are refactored.
18 *
19 * @package XSpeed
20 */
21
22 namespace XSpeed;
23
24 defined( 'ABSPATH' ) || exit;
25
26 final class Rest_Manager {
27
28 public const NAMESPACE_V1 = 'xspeed/v1';
29
30 /**
31 * Register every route a module declared, prefixed with its slug.
32 */
33 public static function register_module( Module $module ): void {
34 $routes = $module->rest_routes();
35 if ( empty( $routes ) ) {
36 return;
37 }
38
39 add_action(
40 'rest_api_init',
41 static function () use ( $module, $routes ) {
42 $slug = $module->slug();
43 foreach ( $routes as $route ) {
44 $path = '/' . trim( $slug, '/' ) . '/' . ltrim( $route['path'] ?? '', '/' );
45 $path = rtrim( $path, '/' );
46
47 $args = array(
48 'methods' => $route['methods'] ?? 'GET',
49 'callback' => self::wrap_callback( $module, $route ),
50 'permission_callback' => self::wrap_permission( $module, $route ),
51 );
52 if ( isset( $route['args'] ) ) {
53 $args['args'] = $route['args'];
54 }
55
56 register_rest_route( self::NAMESPACE_V1, $path, $args );
57 }
58 }
59 );
60 }
61
62 /**
63 * Wrap a module's callback with the tier + conflict gates.
64 */
65 private static function wrap_callback( Module $module, array $route ): callable {
66 $callback = $route['callback'] ?? null;
67 $feature = $route['feature'] ?? null; // optional sub-feature key for conflict resolution.
68
69 return static function ( \WP_REST_Request $request ) use ( $module, $callback, $feature ) {
70 // Tier gate — Pro route without active Pro plugin looks like it doesn't exist.
71 if ( ! Tier_Registry::is_available( $module ) ) {
72 return new \WP_Error(
73 'rest_no_route',
74 __( 'No route was found matching the URL and request method.', 'xspeed' ),
75 array( 'status' => 404 )
76 );
77 }
78
79 // Conflict gate — refuse-strategy → 409.
80 if ( $feature ) {
81 $reason = Conflict_Registry::why_blocked( $module->slug(), $feature );
82 if ( $reason ) {
83 return new \WP_Error(
84 'xspeed_conflict_refused',
85 $reason,
86 array( 'status' => 409 )
87 );
88 }
89 }
90
91 if ( ! is_callable( $callback ) ) {
92 return new \WP_Error( 'xspeed_no_callback', 'Module REST callback is not callable.', array( 'status' => 500 ) );
93 }
94 return call_user_func( $callback, $request );
95 };
96 }
97
98 /**
99 * Wrap permission_callback with the always-on cap check. A module may
100 * declare its own permission_callback for an extra-strict gate; both
101 * must pass.
102 */
103 private static function wrap_permission( Module $module, array $route ): callable {
104 $declared = $route['permission_callback'] ?? null;
105 $capability = $route['capability'] ?? 'manage_options';
106
107 return static function ( \WP_REST_Request $request ) use ( $declared, $capability ) {
108 if ( ! current_user_can( $capability ) ) {
109 return false;
110 }
111 if ( is_callable( $declared ) ) {
112 $result = call_user_func( $declared, $request );
113 if ( true !== $result ) {
114 return $result;
115 }
116 }
117 return true;
118 };
119 }
120 }
121