PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.5.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.5.0
2.5.0 2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 All 34 releases
← All changes | app/Modules/MCP/Support/RestBridge.php +16 -39 2.4.0 → 2.5.0 View file →
@@ -6,27 +6,15 @@
6 6
7 7 defined('ABSPATH') || exit;
8 8
9 9 /**
10 - * Dispatches an MCP write through the plugin's own REST API.
10 + * Dispatches an MCP config write (event types, availability schedules) through
11 + * the same fluent-booking/v2 routes the admin SPA calls, so the policies and
12 + * validation rules run unchanged instead of being duplicated here.
11 13 *
12 - * Configuration writes — event types, availability schedules — carry a lot of
13 - * validation: slug uniqueness, timezone conversion of weekly hours, duration
14 - * lists, host assignment rules, refusal to delete a schedule still in use.
15 - * Reimplementing any of it in the MCP layer would create a second copy that
16 - * drifts, and the first drift is a schedule an agent saved that the admin UI
17 - * then refuses to load.
14 + * Reads don't go through here: admin responses carry UI scaffolding an agent
15 + * doesn't need, so reads use hand-built projections.
18 16 *
19 - * So the writes go through `rest_do_request()` against the same
20 - * `fluent-booking/v2` routes the admin SPA calls. The policy layer runs
21 - * unchanged — verified: a host without permission gets 403 from an internal
22 - * dispatch exactly as they would over HTTP — and so does every validation rule.
23 - *
24 - * Reads deliberately do NOT go through here. Admin responses are shaped for a
25 - * UI and carry scaffolding an agent has no use for; paying for that in context
26 - * on every call is the thing this whole design exists to avoid. Reads get
27 - * hand-built projections instead.
28 - *
29 17 * @since 2.2.6
30 18 */
31 19 class RestBridge
32 20 {
@@ -41,14 +29,11 @@
41 29 * an MCP error the agent can act on.
42 30 */
43 31 public static function call($method, $route, $body = [])
44 32 {
45 - // The framework's Controller::validate() only re-throws its
46 - // ValidationException when REST_REQUEST is set; without it the
47 - // exception is swallowed and the controller runs on with invalid data.
48 - // MCP always serves over /wp-json/ so the constant is always there —
49 - // but a silent validation bypass is not a failure to discover in
50 - // production, so refuse loudly instead of writing unvalidated data.
33 + // Controller::validate() only re-throws its ValidationException when
34 + // REST_REQUEST is set. Without it the controller runs on with invalid
35 + // data, so refuse rather than write unvalidated input.
51 36 if (!defined('REST_REQUEST') || !REST_REQUEST) {
52 37 return MCPHelper::error(
53 38 'not_a_rest_request',
54 39 __('Configuration writes are only available over the REST transport, because that is where validation runs.', 'fluent-booking')
@@ -60,11 +45,10 @@
60 45 foreach ($body as $key => $value) {
61 46 $request->set_param($key, $value);
62 47 }
63 48
64 - // The plugin's controllers read the body for non-GET verbs; setting it
65 - // as JSON keeps nested arrays (weekly schedules, booking fields) intact
66 - // rather than flattening them the way a form-encoded body would.
49 + // A JSON body keeps nested arrays (weekly schedules, booking fields)
50 + // intact; a form-encoded body would flatten them.
67 51 if ($method !== 'GET') {
68 52 $request->set_header('content-type', 'application/json');
69 53 $request->set_body(wp_json_encode($body));
70 54 }
@@ -78,11 +62,10 @@
78 62 return (array) $response->get_data();
79 63 }
80 64
81 65 /**
82 - * Turn a REST failure into an MCP error whose message is the one the admin
83 - * UI would have shown, so the agent gets the real reason rather than
84 - * "request failed".
66 + * Turn a REST failure into an MCP error carrying the message the admin UI
67 + * would have shown.
85 68 *
86 69 * @return \WP_Error
87 70 */
88 71 private static function translateError($response)
@@ -89,23 +72,18 @@
89 72 {
90 73 $status = $response->get_status();
91 74 $data = (array) $response->get_data();
92 75
93 - // Three payload shapes reach here, and none of them is the other two:
94 - //
76 + // Three payload shapes arrive here:
95 77 // {message, errors} Controller::sendError()
96 78 // {code, message, data} a WP_Error, e.g. from a policy
97 79 // {field: {rule: message}} a 422 from the framework's validator
98 - //
99 - // WP_Error::as_error() only understands the second and warns on the
100 - // others, so the payload is read directly.
80 + // WP_Error::as_error() only handles the second, so read the payload directly.
101 81 $message = (string) Arr::get($data, 'message', '');
102 82 $fieldErrors = self::flattenFieldErrors($data);
103 83
104 84 if (!$message && $fieldErrors) {
105 - // Lead with the real reasons rather than "the request failed" —
106 - // an agent that is told "Event title field is required" fixes its
107 - // call in one step.
85 + // The field reasons let the agent fix its call in one step.
108 86 $message = implode(' ', array_values($fieldErrors));
109 87 }
110 88
111 89 if (!$message) {
@@ -138,10 +116,9 @@
138 116 {
139 117 $errors = Arr::get($data, 'errors');
140 118
141 119 if (!is_array($errors)) {
142 - // A bare validator payload: every key is a field, and `message` is
143 - // the only key that is not.
120 + // A bare validator payload: every key except these is a field.
144 121 $errors = $data;
145 122 unset($errors['message'], $errors['code'], $errors['data']);
146 123 }
147 124