PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.5
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | _inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-mcp-settings.php +306 -0 16.2-beta → 16.3-a.5 View file →
@@ -1,0 +1,306 @@
1 +<?php
2 +/**
3 + * REST API endpoint for Jetpack AI MCP settings.
4 + *
5 + * GET — proxies to WPCOM wpcom/v2/sites/{blog_id}/mcp-abilities (wpcom#205108) using
6 + * user token auth so that WPCOM can resolve the requesting user's account-level
7 + * MCP state. Reshapes the response into the { account, site, sites,
8 + * site_level_enabled_default } structure the client utilities expect.
9 + * POST — proxies to WPCOM POST /sites/{blog_id}/mcp-abilities (user token auth) which
10 + * persists { site_level_enabled, abilities } to user settings via SettingsHelper,
11 + * keeping Jetpack and Calypso in sync.
12 + *
13 + * @package automattic/jetpack
14 + */
15 +
16 +use Automattic\Jetpack\Connection\Client;
17 +use Automattic\Jetpack\Connection\Manager;
18 +
19 +if ( ! defined( 'ABSPATH' ) ) {
20 + exit( 0 );
21 +}
22 +
23 +/**
24 + * Class WPCOM_REST_API_V2_Endpoint_MCP_Settings
25 + */
26 +class WPCOM_REST_API_V2_Endpoint_MCP_Settings extends WP_REST_Controller {
27 + /**
28 + * Namespace prefix.
29 + *
30 + * @var string
31 + */
32 + public $namespace = 'wpcom/v2';
33 +
34 + /**
35 + * Endpoint base route.
36 + *
37 + * @var string
38 + */
39 + public $rest_base = 'jetpack-ai/mcp-settings';
40 +
41 + /**
42 + * Constructor.
43 + */
44 + public function __construct() {
45 + add_action( 'rest_api_init', array( $this, 'register_routes' ) );
46 + }
47 +
48 + /**
49 + * Register routes.
50 + */
51 + public function register_routes() {
52 + register_rest_route(
53 + $this->namespace,
54 + '/' . $this->rest_base,
55 + array(
56 + array(
57 + 'methods' => WP_REST_Server::READABLE,
58 + 'callback' => array( $this, 'get_mcp_settings' ),
59 + 'permission_callback' => array( $this, 'permissions_check' ),
60 + ),
61 + array(
62 + 'methods' => WP_REST_Server::EDITABLE,
63 + 'callback' => array( $this, 'update_mcp_settings' ),
64 + 'permission_callback' => array( $this, 'permissions_check' ),
65 + 'args' => array(
66 + 'mcp_abilities' => array(
67 + 'type' => 'object',
68 + 'required' => true,
69 + ),
70 + ),
71 + ),
72 + )
73 + );
74 + }
75 +
76 + /**
77 + * Check permissions.
78 + *
79 + * @return bool|WP_Error
80 + */
81 + public function permissions_check() {
82 + if ( ! current_user_can( 'manage_options' ) ) {
83 + return new WP_Error(
84 + 'rest_forbidden',
85 + __( 'You do not have permission to manage MCP settings.', 'jetpack' ),
86 + array( 'status' => rest_authorization_required_code() )
87 + );
88 + }
89 +
90 + return true;
91 + }
92 +
93 + /**
94 + * Fetch MCP abilities from the WPCOM wpcom/v2/sites/{blog_id}/mcp-abilities endpoint.
95 + *
96 + * The WPCOM endpoint accepts user token auth and returns an array of abilities.
97 + * The response is shaped to match the format the client utilities expect:
98 + * { account: { tool-id: {...} }, site: { tool-id: {...} },
99 + * sites: [{ blog_id, site_level_enabled, abilities }],
100 + * site_level_enabled_default: bool }
101 + *
102 + * All state is owned by WPCOM and persisted via POST /sites/{blog_id}/mcp-abilities.
103 + *
104 + * @return WP_REST_Response|WP_Error
105 + */
106 + public function get_mcp_settings() {
107 + $blog_id = Manager::get_site_id();
108 +
109 + if ( is_wp_error( $blog_id ) ) {
110 + return rest_ensure_response( $blog_id );
111 + }
112 +
113 + $response = Client::wpcom_json_api_request_as_user(
114 + sprintf( '/sites/%d/mcp-abilities', (int) $blog_id ),
115 + '2',
116 + array( 'method' => 'GET' ),
117 + null,
118 + 'wpcom'
119 + );
120 +
121 + if ( is_wp_error( $response ) ) {
122 + return $response;
123 + }
124 +
125 + $body = json_decode( wp_remote_retrieve_body( $response ), true );
126 +
127 + // has_mcp_plan is set explicitly by the WPCOM endpoint using PaidPlanMiddleware.
128 + // Fall back to 402/403 status handling for older WPCOM builds that predate the field.
129 + $http_status = wp_remote_retrieve_response_code( $response );
130 + $has_mcp_plan = isset( $body['has_mcp_plan'] )
131 + ? (bool) $body['has_mcp_plan']
132 + : ! in_array( $http_status, array( 402, 403 ), true );
133 +
134 + if ( ! $has_mcp_plan ) {
135 + return rest_ensure_response(
136 + array(
137 + 'has_mcp_access' => false,
138 + 'mcp_abilities' => array(),
139 + )
140 + );
141 + }
142 +
143 + // Transform [ {name, title, readonly, site_context, enabled, …}, … ] → { name: {…}, … }.
144 + // readonly and site_context are now provided by the WPCOM endpoint (wpcom#205108).
145 + // Fall back to name-suffix heuristic for readonly only if the field is absent, so
146 + // this endpoint remains functional against older WPCOM builds.
147 + $account_abilities = new stdClass();
148 + $site_abilities = array();
149 + if ( ! empty( $body['abilities'] ) && is_array( $body['abilities'] ) ) {
150 + $account_abilities = array();
151 + foreach ( $body['abilities'] as $ability ) {
152 + if ( empty( $ability['name'] ) ) {
153 + continue;
154 + }
155 + if ( ! array_key_exists( 'readonly', $ability ) ) {
156 + $ability['readonly'] = ! (bool) preg_match( '/-(create|update|delete)$/i', $ability['name'] );
157 + }
158 + $account_abilities[ (string) $ability['name'] ] = $ability;
159 +
160 + // `site` subset: tools marked site_context=true by WPCOM are the only ones
161 + // relevant to the site-level settings UI. getSiteContextToolIds() in JS uses
162 + // this to filter out account/notifications/billing/domains tools.
163 + if ( ! empty( $ability['site_context'] ) ) {
164 + $site_abilities[ $ability['name'] ] = $ability;
165 + }
166 + }
167 + }
168 +
169 + // site_level_enabled comes directly from WPCOM — it is the authoritative effective
170 + // state for this site (account default applied, site exceptions factored in).
171 + // Fall back to the enabled-abilities heuristic only if the field is absent.
172 + $site_level_enabled = isset( $body['site_level_enabled'] )
173 + ? (bool) $body['site_level_enabled']
174 + : ! empty(
175 + array_filter(
176 + (array) $account_abilities,
177 + function ( $a ) {
178 + return ! empty( $a['enabled'] );
179 + }
180 + )
181 + );
182 +
183 + // site_level_enabled_default mirrors Calypso: same value as site_level_enabled
184 + // when derived from WPCOM (no per-site override concept at this layer).
185 + $site_level_enabled_default = $site_level_enabled;
186 +
187 + // Group descriptors (AIINT-469) — pass through if the WPCOM endpoint returns them.
188 + $groups = array();
189 + if ( isset( $body['groups'] ) && is_array( $body['groups'] ) ) {
190 + $groups = $body['groups'];
191 + }
192 +
193 + // Use only the explicit per-site user overrides returned by WPCOM in user_overrides.abilities.
194 + // These are the raw values stored via SettingsHelper — not the computed effective states
195 + // (account defaults merged with overrides). Keeping only explicit overrides here lets the
196 + // JS fall back to site_level_enabled as the default for any tool not yet overridden,
197 + // matching Calypso's display behaviour (all tools on when site_level_enabled:true and no
198 + // per-tool overrides exist).
199 + $site_tool_abilities = array();
200 + if ( isset( $body['user_overrides']['abilities'] ) && is_array( $body['user_overrides']['abilities'] ) ) {
201 + foreach ( $body['user_overrides']['abilities'] as $name => $value ) {
202 + $site_tool_abilities[ (string) $name ] = (bool) $value;
203 + }
204 + }
205 +
206 + return rest_ensure_response(
207 + array(
208 + 'has_mcp_access' => true,
209 + 'mcp_abilities' => array(
210 + 'account' => $account_abilities,
211 + 'site' => $site_abilities,
212 + 'sites' => array(
213 + array(
214 + 'blog_id' => (int) $blog_id,
215 + 'site_level_enabled' => $site_level_enabled,
216 + 'abilities' => (object) $site_tool_abilities,
217 + ),
218 + ),
219 + 'site_level_enabled_default' => $site_level_enabled_default,
220 + 'groups' => $groups,
221 + ),
222 + )
223 + );
224 + }
225 +
226 + /**
227 + * Proxy mcp_abilities update to WPCOM POST /sites/{blog_id}/mcp-abilities.
228 + *
229 + * Accepts the sites[] format used by the client:
230 + * { sites: [{ blog_id, site_level_enabled?, abilities?: { tool_id: bool } }] }
231 + *
232 + * Extracts site_level_enabled and abilities from sites[0] and forwards them
233 + * to the WPCOM endpoint which persists them to user settings via SettingsHelper.
234 + * This keeps Jetpack and Calypso in sync — both read/write the same store.
235 + *
236 + * @param WP_REST_Request $request Full details about the request.
237 + * @return WP_REST_Response|WP_Error
238 + */
239 + public function update_mcp_settings( $request ) {
240 + $blog_id = Manager::get_site_id();
241 +
242 + if ( is_wp_error( $blog_id ) ) {
243 + return rest_ensure_response( $blog_id );
244 + }
245 +
246 + $incoming = $request->get_param( 'mcp_abilities' );
247 +
248 + if ( is_object( $incoming ) ) {
249 + $incoming = get_object_vars( $incoming );
250 + } elseif ( ! is_array( $incoming ) ) {
251 + $incoming = array();
252 + }
253 +
254 + // Unpack sites[0] into the flat format WPCOM POST /sites/{id}/mcp-abilities expects.
255 + $wpcom_body = array();
256 +
257 + if ( ! empty( $incoming['sites'] ) && is_array( $incoming['sites'] ) ) {
258 + $site_update = $incoming['sites'][0];
259 + if ( is_object( $site_update ) ) {
260 + $site_update = get_object_vars( $site_update );
261 + }
262 +
263 + if ( isset( $site_update['site_level_enabled'] ) ) {
264 + $wpcom_body['site_level_enabled'] = (bool) $site_update['site_level_enabled'];
265 + }
266 +
267 + if ( isset( $site_update['abilities'] ) ) {
268 + $abilities = is_object( $site_update['abilities'] )
269 + ? get_object_vars( $site_update['abilities'] )
270 + : (array) $site_update['abilities'];
271 + $normalised = array();
272 + foreach ( $abilities as $name => $value ) {
273 + $normalised[ $name ] = (bool) $value;
274 + }
275 + $wpcom_body['abilities'] = $normalised;
276 + }
277 + }
278 +
279 + if ( ! empty( $wpcom_body ) ) {
280 + $response = Client::wpcom_json_api_request_as_user(
281 + sprintf( '/sites/%d/mcp-abilities', (int) $blog_id ),
282 + '2',
283 + array( 'method' => 'POST' ),
284 + $wpcom_body,
285 + 'wpcom'
286 + );
287 +
288 + if ( is_wp_error( $response ) ) {
289 + return $response;
290 + }
291 +
292 + $status = wp_remote_retrieve_response_code( $response );
293 + if ( $status < 200 || $status >= 300 ) {
294 + return new WP_Error(
295 + 'wpcom_update_failed',
296 + __( 'Failed to save MCP settings on WordPress.com.', 'jetpack' ),
297 + array( 'status' => 502 )
298 + );
299 + }
300 + }
301 +
302 + return $this->get_mcp_settings();
303 + }
304 +}
305 +
306 +wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_MCP_Settings' );